Add side by side diff (partial)

Create the diff -y utility, this time introducing tests and changes focused
    mainly on the construction of the utility and issues related to alignment
    and response tabulation. New parameters were introduced such as the size
    of the total width of the output in the parameters. A new calculation was
    introduced to determine the size of the output columns and the maximum
    total column size. The tab and spacing mechanism has the same behavior
     as the original diff, with tabs and spaces formatted in the same way.

    - Introducing tests for the diff 'main' function
    - Introducing fuzzing for side diff utility
    - Introducing tests for internal mechanisms
    - Modular functions that allow consistent changes across the entire project
This commit is contained in:
Sami Daniel (Tsoi)
2025-05-26 08:26:10 -03:00
parent dff98a2969
commit 1ef6923b7d
6 changed files with 1325 additions and 134 deletions
+5 -1
View File
@@ -47,4 +47,8 @@ path = "fuzz_targets/fuzz_ed.rs"
test = false
doc = false
[[bin]]
name = "fuzz_side"
path = "fuzz_targets/fuzz_side.rs"
test = false
doc = false
+42
View File
@@ -0,0 +1,42 @@
#![no_main]
#[macro_use]
extern crate libfuzzer_sys;
use diffutilslib::side_diff;
use std::fs::File;
use std::io::Write;
use diffutilslib::params::Params;
fuzz_target!(|x: (Vec<u8>, Vec<u8>, /* usize, usize */ bool)| {
let (original, new, /* width, tabsize, */ expand) = x;
// if width == 0 || tabsize == 0 {
// return;
// }
let params = Params {
// width,
// tabsize,
expand_tabs: expand,
..Default::default()
};
let mut output_buf = vec![];
side_diff::diff(&original, &new, &mut output_buf, &params);
File::create("target/fuzz.file.original")
.unwrap()
.write_all(&original)
.unwrap();
File::create("target/fuzz.file.new")
.unwrap()
.write_all(&new)
.unwrap();
File::create("target/fuzz.file")
.unwrap()
.write_all(&original)
.unwrap();
File::create("target/fuzz.diff")
.unwrap()
.write_all(&output_buf)
.unwrap();
});
+5 -2
View File
@@ -9,7 +9,7 @@ use crate::{context_diff, ed_diff, normal_diff, side_diff, unified_diff};
use std::env::ArgsOs;
use std::ffi::OsString;
use std::fs;
use std::io::{self, Read, Write};
use std::io::{self, stdout, Read, Write};
use std::iter::Peekable;
use std::process::{exit, ExitCode};
@@ -79,7 +79,10 @@ pub fn main(opts: Peekable<ArgsOs>) -> ExitCode {
eprintln!("{error}");
exit(2);
}),
Format::SideBySide => side_diff::diff(&from_content, &to_content),
Format::SideBySide => {
let mut output = stdout().lock();
side_diff::diff(&from_content, &to_content, &mut output, &params)
}
};
if params.brief && !result.is_empty() {
println!(
+34 -3
View File
@@ -25,6 +25,7 @@ pub struct Params {
pub brief: bool,
pub expand_tabs: bool,
pub tabsize: usize,
pub width: usize,
}
impl Default for Params {
@@ -39,6 +40,7 @@ impl Default for Params {
brief: false,
expand_tabs: false,
tabsize: 8,
width: 130,
}
}
}
@@ -58,6 +60,7 @@ pub fn parse_params<I: Iterator<Item = OsString>>(mut opts: Peekable<I>) -> Resu
let mut format = None;
let mut context = None;
let tabsize_re = Regex::new(r"^--tabsize=(?<num>\d+)$").unwrap();
let width_re = Regex::new(r"--width=(?P<long>\d+)$").unwrap();
while let Some(param) = opts.next() {
let next_param = opts.peek();
if param == "--" {
@@ -109,6 +112,27 @@ pub fn parse_params<I: Iterator<Item = OsString>>(mut opts: Peekable<I>) -> Resu
format = Some(Format::SideBySide);
continue;
}
if width_re.is_match(param.to_string_lossy().as_ref()) {
let param = param.into_string().unwrap();
let width_str: &str = width_re
.captures(param.as_str())
.unwrap()
.name("long")
.unwrap()
.as_str();
params.width = match width_str.parse::<usize>() {
Ok(num) => {
if num == 0 {
return Err("invalid width «0»".to_string());
}
num
}
Err(_) => return Err(format!("invalid width «{width_str}»")),
};
continue;
}
if tabsize_re.is_match(param.to_string_lossy().as_ref()) {
// Because param matches the regular expression,
// it is safe to assume it is valid UTF-8.
@@ -120,9 +144,16 @@ pub fn parse_params<I: Iterator<Item = OsString>>(mut opts: Peekable<I>) -> Resu
.unwrap()
.as_str();
params.tabsize = match tabsize_str.parse::<usize>() {
Ok(num) => num,
Ok(num) => {
if num == 0 {
return Err("invalid tabsize «0»".to_string());
}
num
}
Err(_) => return Err(format!("invalid tabsize «{tabsize_str}»")),
};
continue;
}
match match_context_diff_params(&param, next_param, format) {
@@ -712,11 +743,11 @@ mod tests {
executable: os("diff"),
from: os("foo"),
to: os("bar"),
tabsize: 0,
tabsize: 1,
..Default::default()
}),
parse_params(
[os("diff"), os("--tabsize=0"), os("foo"), os("bar")]
[os("diff"), os("--tabsize=1"), os("foo"), os("bar")]
.iter()
.cloned()
.peekable()
+1239 -59
View File
File diff suppressed because it is too large Load Diff
-69
View File
@@ -98,15 +98,6 @@ pub fn report_failure_to_read_input_file(
);
}
/// Limits a string at a certain limiter position. This can break the
/// encoding of a specific char where it has been cut.
#[must_use]
pub fn limited_string(orig: &[u8], limiter: usize) -> &[u8] {
// TODO: Verify if we broke the encoding of the char
// when we cut it.
&orig[..orig.len().min(limiter)]
}
#[cfg(test)]
mod tests {
use super::*;
@@ -213,64 +204,4 @@ mod tests {
assert!(m_time > current_time);
}
}
mod limited_string {
use super::*;
use std::str;
#[test]
fn empty_orig_returns_empty() {
let orig: &[u8] = b"";
let result = limited_string(&orig, 10);
assert!(result.is_empty());
}
#[test]
fn zero_limit_returns_empty() {
let orig: &[u8] = b"foo";
let result = limited_string(&orig, 0);
assert!(result.is_empty());
}
#[test]
fn limit_longer_than_orig_returns_full() {
let orig: &[u8] = b"foo";
let result = limited_string(&orig, 10);
assert_eq!(result, orig);
}
#[test]
fn ascii_limit_in_middle() {
let orig: &[u8] = b"foobar";
let result = limited_string(&orig, 3);
assert_eq!(result, b"foo");
assert!(str::from_utf8(&result).is_ok()); // All are ascii chars, we do not broke the enconding
}
#[test]
fn utf8_multibyte_cut_invalidates() {
let orig = "áéíóú".as_bytes();
let result = limited_string(&orig, 1);
// should contain only the first byte of mult-byte char
assert_eq!(result, vec![0xC3]);
assert!(str::from_utf8(&result).is_err());
}
#[test]
fn utf8_limit_at_codepoint_boundary() {
let orig = "áéí".as_bytes();
let bytes = &orig;
let result = limited_string(&orig, bytes.len());
assert_eq!(result, *bytes);
assert!(str::from_utf8(&result).is_ok());
}
#[test]
fn works_with_byte_vec_input() {
let orig_bytes = b"hello".to_vec();
let result = limited_string(&orig_bytes, 3);
assert_eq!(result, b"hel");
}
}
}