From da05a5254be8e909163e05477ff0ff2a2ca5d346 Mon Sep 17 00:00:00 2001 From: Michael Howell Date: Sat, 3 Apr 2021 09:42:35 -0700 Subject: [PATCH 001/194] Fix missingnl behavior on normal-diff --- lib/normal-diff/fuzz/.gitignore | 4 + lib/normal-diff/fuzz/Cargo.toml | 27 ++++ .../fuzz/fuzz_targets/fuzz_patch.rs | 57 +++++++ lib/normal-diff/src/lib.rs | 149 ++++++++++++++++-- 4 files changed, 225 insertions(+), 12 deletions(-) create mode 100644 lib/normal-diff/fuzz/.gitignore create mode 100644 lib/normal-diff/fuzz/Cargo.toml create mode 100644 lib/normal-diff/fuzz/fuzz_targets/fuzz_patch.rs diff --git a/lib/normal-diff/fuzz/.gitignore b/lib/normal-diff/fuzz/.gitignore new file mode 100644 index 0000000..572e03b --- /dev/null +++ b/lib/normal-diff/fuzz/.gitignore @@ -0,0 +1,4 @@ + +target +corpus +artifacts diff --git a/lib/normal-diff/fuzz/Cargo.toml b/lib/normal-diff/fuzz/Cargo.toml new file mode 100644 index 0000000..03fd540 --- /dev/null +++ b/lib/normal-diff/fuzz/Cargo.toml @@ -0,0 +1,27 @@ + +[package] +name = "normal-diff-fuzz" +version = "0.0.0" +authors = ["Automatically generated"] +publish = false +edition = "2018" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.3" + +[dependencies.normal-diff] +path = ".." + +# Prevent this from interfering with workspaces +[workspace] +members = ["."] + +[[bin]] +name = "fuzz_patch" +path = "fuzz_targets/fuzz_patch.rs" +test = false +doc = false + diff --git a/lib/normal-diff/fuzz/fuzz_targets/fuzz_patch.rs b/lib/normal-diff/fuzz/fuzz_targets/fuzz_patch.rs new file mode 100644 index 0000000..8816b98 --- /dev/null +++ b/lib/normal-diff/fuzz/fuzz_targets/fuzz_patch.rs @@ -0,0 +1,57 @@ +#![no_main] +#[macro_use] extern crate libfuzzer_sys; +extern crate normal_diff; + +use std::fs::{self, File}; +use std::io::Write; +use std::process::Command; + +fuzz_target!(|x: (Vec, Vec)| { + let (from, to) = x; + /*if let Ok(s) = String::from_utf8(from.clone()) { + if !s.is_ascii() { return } + if s.find(|x| x < ' ' && x != '\n').is_some() { return } + } else { + return + } + if let Ok(s) = String::from_utf8(to.clone()) { + if !s.is_ascii() { return } + if s.find(|x| x < ' ' && x != '\n').is_some() { return } + } else { + return + }*/ + let diff = normal_diff::diff(&from, &to); + File::create("target/fuzz.file.original") + .unwrap() + .write_all(&from) + .unwrap(); + File::create("target/fuzz.file.expected") + .unwrap() + .write_all(&to) + .unwrap(); + File::create("target/fuzz.file") + .unwrap() + .write_all(&from) + .unwrap(); + File::create("target/fuzz.diff") + .unwrap() + .write_all(&diff) + .unwrap(); + let output = Command::new("patch") + .arg("-p0") + .arg("--binary") + .arg("--fuzz=0") + .arg("--normal") + .arg("target/fuzz.file") + .stdin(File::open("target/fuzz.diff").unwrap()) + .output() + .unwrap(); + if !output.status.success() { + panic!("STDOUT:\n{}\nSTDERR:\n{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr)); + } + let result = fs::read("target/fuzz.file").unwrap(); + if result != to { + panic!("STDOUT:\n{}\nSTDERR:\n{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr)); + } +}); + diff --git a/lib/normal-diff/src/lib.rs b/lib/normal-diff/src/lib.rs index d102faf..14ee175 100644 --- a/lib/normal-diff/src/lib.rs +++ b/lib/normal-diff/src/lib.rs @@ -6,6 +6,8 @@ struct Mismatch { pub line_number_actual: usize, pub expected: Vec>, pub actual: Vec>, + pub expected_missing_nl: bool, + pub actual_missing_nl: bool, } impl Mismatch { @@ -15,6 +17,8 @@ impl Mismatch { line_number_actual, expected: Vec::new(), actual: Vec::new(), + expected_missing_nl: false, + actual_missing_nl: false, } } } @@ -31,8 +35,8 @@ fn make_diff(expected: &[u8], actual: &[u8]) -> Vec { debug_assert_eq!(b"".split(|&c| c == b'\n').count(), 1); // ^ means that underflow here is impossible - let _expected_lines_count = expected_lines.len() - 1; - let _actual_lines_count = actual_lines.len() - 1; + let expected_lines_count = expected_lines.len() - 1; + let actual_lines_count = actual_lines.len() - 1; if expected_lines.last() == Some(&&b""[..]) { expected_lines.pop(); @@ -45,26 +49,46 @@ fn make_diff(expected: &[u8], actual: &[u8]) -> Vec { for result in diff::slice(&expected_lines, &actual_lines) { match result { diff::Result::Left(str) => { - if mismatch.actual.len() != 0 { + if mismatch.actual.len() != 0 && !mismatch.actual_missing_nl { results.push(mismatch); mismatch = Mismatch::new(line_number_expected, line_number_actual); } mismatch.expected.push(str.to_vec()); + mismatch.expected_missing_nl = line_number_expected > expected_lines_count; line_number_expected += 1; } diff::Result::Right(str) => { mismatch.actual.push(str.to_vec()); + mismatch.actual_missing_nl = line_number_actual > actual_lines_count; line_number_actual += 1; } - diff::Result::Both(_str, _) => { - line_number_expected += 1; - line_number_actual += 1; - if mismatch.actual.len() != 0 || mismatch.expected.len() != 0 { - results.push(mismatch); - mismatch = Mismatch::new(line_number_expected, line_number_actual); - } else { - mismatch.line_number_expected = line_number_expected; - mismatch.line_number_actual = line_number_actual; + diff::Result::Both(str, _) => { + match (line_number_expected > expected_lines_count, line_number_actual > actual_lines_count) { + (true, false) => { + line_number_expected += 1; + line_number_actual += 1; + mismatch.expected.push(str.to_vec()); + mismatch.expected_missing_nl = true; + mismatch.actual.push(str.to_vec()); + } + (false, true) => { + line_number_expected += 1; + line_number_actual += 1; + mismatch.actual.push(str.to_vec()); + mismatch.actual_missing_nl = true; + mismatch.expected.push(str.to_vec()); + } + (true, true) | (false, false) => { + line_number_expected += 1; + line_number_actual += 1; + if mismatch.actual.len() != 0 || mismatch.expected.len() != 0 { + results.push(mismatch); + mismatch = Mismatch::new(line_number_expected, line_number_actual); + } else { + mismatch.line_number_expected = line_number_expected; + mismatch.line_number_actual = line_number_actual; + } + }, } } } @@ -118,6 +142,9 @@ pub fn diff(expected: &[u8], actual: &[u8]) -> Vec { output.write_all(expected).unwrap(); writeln!(&mut output, "").unwrap(); } + if result.expected_missing_nl { + writeln!(&mut output, r"\ No newline at end of file").unwrap(); + } if expected_count != 0 && actual_count != 0 { writeln!(&mut output, "---").unwrap(); } @@ -126,6 +153,9 @@ pub fn diff(expected: &[u8], actual: &[u8]) -> Vec { output.write_all(actual).unwrap(); writeln!(&mut output, "").unwrap(); } + if result.actual_missing_nl { + writeln!(&mut output, r"\ No newline at end of file").unwrap(); + } } output } @@ -209,6 +239,101 @@ fn test_permutations() { } } +#[test] +fn test_permutations_missing_line_ending() { + // test all possible six-line files with missing newlines. + let _ = std::fs::create_dir("target"); + for &a in &[0, 1, 2] { + for &b in &[0, 1, 2] { + for &c in &[0, 1, 2] { + for &d in &[0, 1, 2] { + for &e in &[0, 1, 2] { + for &f in &[0, 1, 2] { + for &g in &[0, 1, 2] { + use std::fs::{self, File}; + use std::io::Write; + use std::process::Command; + let mut alef = Vec::new(); + let mut bet = Vec::new(); + alef.write_all(if a == 0 { b"a\n" } else { b"b\n" }) + .unwrap(); + if a != 2 { + bet.write_all(b"b\n").unwrap(); + } + alef.write_all(if b == 0 { b"c\n" } else { b"d\n" }) + .unwrap(); + if b != 2 { + bet.write_all(b"d\n").unwrap(); + } + alef.write_all(if c == 0 { b"e\n" } else { b"f\n" }) + .unwrap(); + if c != 2 { + bet.write_all(b"f\n").unwrap(); + } + alef.write_all(if d == 0 { b"g\n" } else { b"h\n" }) + .unwrap(); + if d != 2 { + bet.write_all(b"h\n").unwrap(); + } + alef.write_all(if e == 0 { b"i\n" } else { b"j\n" }) + .unwrap(); + if e != 2 { + bet.write_all(b"j\n").unwrap(); + } + alef.write_all(if f == 0 { b"k\n" } else { b"l\n" }) + .unwrap(); + if f != 2 { + bet.write_all(b"l\n").unwrap(); + } + match g { + 0 => { + alef.pop(); + } + 1 => { + bet.pop(); + } + 2 => { + alef.pop(); + bet.pop(); + } + _ => unreachable!(), + } + // This test diff is intentionally reversed. + // We want it to turn the alef into bet. + let diff = diff(&alef, &bet); + File::create("target/abn.diff") + .unwrap() + .write_all(&diff) + .unwrap(); + let mut fa = File::create("target/alefn").unwrap(); + fa.write_all(&alef[..]).unwrap(); + let mut fb = File::create("target/betn").unwrap(); + fb.write_all(&bet[..]).unwrap(); + let _ = fa; + let _ = fb; + let output = Command::new("patch") + .arg("-p0") + .arg("--normal") + .arg("target/alefn") + .stdin(File::open("target/abn.diff").unwrap()) + .output() + .unwrap(); + if !output.status.success() { + panic!("{:?}", output); + } + //println!("{}", String::from_utf8_lossy(&output.stdout)); + //println!("{}", String::from_utf8_lossy(&output.stderr)); + let alef = fs::read("target/alefn").unwrap(); + assert_eq!(alef, bet); + } + } + } + } + } + } + } +} + #[test] fn test_permutations_empty_lines() { // test all possible six-line files with missing newlines. From 582259a867bb4b0c4eac326b11ee9484031c0413 Mon Sep 17 00:00:00 2001 From: Michael Howell Date: Wed, 21 Jul 2021 13:40:52 -0700 Subject: [PATCH 002/194] Refactor into separate bin and lib folders --- Cargo.toml | 15 +++++++++++++++ README.md | 2 +- bin/diff/Cargo.toml | 12 ------------ bin/diff/{src => }/main.rs | 0 bin/diff/{src => }/params.rs | 0 lib/context-diff/Cargo.toml | 2 +- lib/normal-diff/Cargo.toml | 2 +- 7 files changed, 18 insertions(+), 15 deletions(-) delete mode 100644 bin/diff/Cargo.toml rename bin/diff/{src => }/main.rs (100%) rename bin/diff/{src => }/params.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index 62bfa42..359e342 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,3 +5,18 @@ members = [ "lib/normal-diff", "bin/diff", ] + +[package] +name = "diffutils" +version = "0.3.0" +authors = ["Michael Howell "] +edition = "2018" + +[dependencies] +context-diff = { path = "lib/context-diff", version = "0.3.0" } +normal-diff = { path = "lib/normal-diff", version = "0.3.0" } +unified-diff = { path = "lib/unified-diff", version = "0.3.0" } + +[[bin]] +name = "diffutils" +path = "bin/main.rs" diff --git a/README.md b/README.md index a964abc..4fe36b1 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ A package (currently just `diff`, but eventually more) of programs related to fi Based on the incomplete diff generator in https://github.com/rust-lang/rust/blob/master/src/tools/compiletest/src/runtest.rs, and made to be compatible with GNU's diff and patch tools. ``` -~/diffutils$ cargo run -- -u3 Cargo.lock Cargo.toml +~/diffutils$ cargo run -- diff -u3 Cargo.lock Cargo.toml Finished dev [unoptimized + debuginfo] target(s) in 0.00s Running `target/debug/diff -u3 Cargo.lock Cargo.toml` --- Cargo.lock diff --git a/bin/diff/Cargo.toml b/bin/diff/Cargo.toml deleted file mode 100644 index 7f3cd7c..0000000 --- a/bin/diff/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "diff" -version = "0.1.0" -authors = ["Michael Howell "] -edition = "2018" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -context-diff = { path = "../../lib/context-diff" } -normal-diff = { path = "../../lib/normal-diff" } -unified-diff = { path = "../../lib/unified-diff" } diff --git a/bin/diff/src/main.rs b/bin/diff/main.rs similarity index 100% rename from bin/diff/src/main.rs rename to bin/diff/main.rs diff --git a/bin/diff/src/params.rs b/bin/diff/params.rs similarity index 100% rename from bin/diff/src/params.rs rename to bin/diff/params.rs diff --git a/lib/context-diff/Cargo.toml b/lib/context-diff/Cargo.toml index 5bf1dd4..3b0ae82 100644 --- a/lib/context-diff/Cargo.toml +++ b/lib/context-diff/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "context-diff" -version = "0.1.0" +version = "0.3.0" authors = [ "Michael Howell ", "The Rust Project Developers" diff --git a/lib/normal-diff/Cargo.toml b/lib/normal-diff/Cargo.toml index 0bde96c..ae73c3d 100644 --- a/lib/normal-diff/Cargo.toml +++ b/lib/normal-diff/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "normal-diff" -version = "0.1.0" +version = "0.3.0" authors = [ "Michael Howell ", "The Rust Project Developers" From ea9376aaafc2ae596c3c95d4e10efd2cb2b9f74a Mon Sep 17 00:00:00 2001 From: Michael Howell Date: Wed, 15 Sep 2021 19:06:14 -0700 Subject: [PATCH 003/194] Add ed-formatted diff --- .gitignore | 1 + Cargo.toml | 18 +- bin/diffutils/Cargo.toml | 17 + bin/{diff => diffutils}/main.rs | 4 + bin/{diff => diffutils}/params.rs | 19 ++ lib/context-diff/Cargo.toml | 4 - lib/ed-diff/Cargo.toml | 11 + lib/ed-diff/fuzz/.gitignore | 4 + lib/ed-diff/fuzz/Cargo.toml | 26 ++ lib/ed-diff/fuzz/fuzz_targets/fuzz_ed.rs | 55 ++++ lib/ed-diff/src/lib.rs | 382 +++++++++++++++++++++++ lib/normal-diff/Cargo.toml | 4 - lib/normal-diff/fuzz/Cargo.toml | 1 - lib/unified-diff/Cargo.toml | 4 - lib/unified-diff/fuzz/Cargo.toml | 1 - 15 files changed, 521 insertions(+), 30 deletions(-) create mode 100644 bin/diffutils/Cargo.toml rename bin/{diff => diffutils}/main.rs (92%) rename bin/{diff => diffutils}/params.rs (92%) create mode 100644 lib/ed-diff/Cargo.toml create mode 100644 lib/ed-diff/fuzz/.gitignore create mode 100644 lib/ed-diff/fuzz/Cargo.toml create mode 100644 lib/ed-diff/fuzz/fuzz_targets/fuzz_ed.rs create mode 100644 lib/ed-diff/src/lib.rs diff --git a/.gitignore b/.gitignore index e90ad7d..3991d07 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /target /lib/normal-diff/target +/lib/ed-diff/target /lib/context-diff/target /lib/unified-diff/target Cargo.lock diff --git a/Cargo.toml b/Cargo.toml index 359e342..f2c907e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,20 +3,6 @@ members = [ "lib/unified-diff", "lib/context-diff", "lib/normal-diff", - "bin/diff", + "lib/ed-diff", + "bin/diffutils", ] - -[package] -name = "diffutils" -version = "0.3.0" -authors = ["Michael Howell "] -edition = "2018" - -[dependencies] -context-diff = { path = "lib/context-diff", version = "0.3.0" } -normal-diff = { path = "lib/normal-diff", version = "0.3.0" } -unified-diff = { path = "lib/unified-diff", version = "0.3.0" } - -[[bin]] -name = "diffutils" -path = "bin/main.rs" diff --git a/bin/diffutils/Cargo.toml b/bin/diffutils/Cargo.toml new file mode 100644 index 0000000..a152cf3 --- /dev/null +++ b/bin/diffutils/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "diffutils" +version = "0.3.0" +edition = "2018" +description = "A CLI app for generating diff files" +license = "MIT OR Apache-2.0" +repository = "https://github.com/notriddle/diffutils" + +[[bin]] +name = "diffutils" +path = "main.rs" + +[dependencies] +unified-diff = { path = "../../lib/unified-diff/" } +context-diff = { path = "../../lib/context-diff/" } +normal-diff = { path = "../../lib/normal-diff/" } +ed-diff = { path = "../../lib/ed-diff/" } diff --git a/bin/diff/main.rs b/bin/diffutils/main.rs similarity index 92% rename from bin/diff/main.rs rename to bin/diffutils/main.rs index cc36661..d90dfc8 100644 --- a/bin/diff/main.rs +++ b/bin/diffutils/main.rs @@ -44,6 +44,10 @@ fn main() -> Result<(), String> { &to.to_string_lossy(), context_count, ), + Format::Ed => ed_diff::diff( + &from_content, + &to_content, + )?, }; io::stdout().write_all(&result).unwrap(); Ok(()) diff --git a/bin/diff/params.rs b/bin/diffutils/params.rs similarity index 92% rename from bin/diff/params.rs rename to bin/diffutils/params.rs index f44aeeb..1c0dcf1 100644 --- a/bin/diff/params.rs +++ b/bin/diffutils/params.rs @@ -5,6 +5,7 @@ pub enum Format { Normal, Unified, Context, + Ed, } #[cfg(unix)] @@ -73,6 +74,12 @@ pub fn parse_params>(opts: I) -> Result { + if format.is_some() && format != Some(Format::Ed) { + return Err(format!("Conflicting output style options")); + } + format = Some(Format::Ed); + } b'u' => { if format.is_some() && format != Some(Format::Unified) { return Err(format!("Conflicting output style options")); @@ -151,6 +158,18 @@ mod tests { ); } #[test] + fn basics_ed() { + assert_eq!( + Ok(Params { + from: os("foo"), + to: os("bar"), + format: Format::Ed, + context_count: 3, + }), + parse_params([os("diff"), os("-e"), os("foo"), os("bar")].iter().cloned()) + ); + } + #[test] fn context_count() { assert_eq!( Ok(Params { diff --git a/lib/context-diff/Cargo.toml b/lib/context-diff/Cargo.toml index 3b0ae82..4c8f714 100644 --- a/lib/context-diff/Cargo.toml +++ b/lib/context-diff/Cargo.toml @@ -1,10 +1,6 @@ [package] name = "context-diff" version = "0.3.0" -authors = [ - "Michael Howell ", - "The Rust Project Developers" -] edition = "2018" description = "An implementation of the GNU unified diff format" license = "MIT OR Apache-2.0" diff --git a/lib/ed-diff/Cargo.toml b/lib/ed-diff/Cargo.toml new file mode 100644 index 0000000..0b2e90d --- /dev/null +++ b/lib/ed-diff/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "ed-diff" +version = "0.3.0" +edition = "2018" +description = "An implementation of the GNU unified diff format" +license = "MIT OR Apache-2.0" +repository = "https://github.com/notriddle/diffutils" +exclude = [ "fuzz" ] + +[dependencies] +diff = "0.1.10" diff --git a/lib/ed-diff/fuzz/.gitignore b/lib/ed-diff/fuzz/.gitignore new file mode 100644 index 0000000..572e03b --- /dev/null +++ b/lib/ed-diff/fuzz/.gitignore @@ -0,0 +1,4 @@ + +target +corpus +artifacts diff --git a/lib/ed-diff/fuzz/Cargo.toml b/lib/ed-diff/fuzz/Cargo.toml new file mode 100644 index 0000000..2f3460d --- /dev/null +++ b/lib/ed-diff/fuzz/Cargo.toml @@ -0,0 +1,26 @@ + +[package] +name = "ed-diff-fuzz" +version = "0.0.0" +publish = false +edition = "2018" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.3" + +[dependencies.ed-diff] +path = ".." + +# Prevent this from interfering with workspaces +[workspace] +members = ["."] + +[[bin]] +name = "fuzz_ed" +path = "fuzz_targets/fuzz_ed.rs" +test = false +doc = false + diff --git a/lib/ed-diff/fuzz/fuzz_targets/fuzz_ed.rs b/lib/ed-diff/fuzz/fuzz_targets/fuzz_ed.rs new file mode 100644 index 0000000..febd1f7 --- /dev/null +++ b/lib/ed-diff/fuzz/fuzz_targets/fuzz_ed.rs @@ -0,0 +1,55 @@ +#![no_main] +#[macro_use] extern crate libfuzzer_sys; +extern crate ed_diff; + +use std::fs::{self, File}; +use std::io::Write; +use std::process::Command; + +fuzz_target!(|x: (Vec, Vec)| { + let (mut from, mut to) = x; + from.push(b'\n'); + to.push(b'\n'); + if let Ok(s) = String::from_utf8(from.clone()) { + if !s.is_ascii() { return } + if s.find(|x| x < ' ' && x != '\n').is_some() { return } + } else { + return + } + if let Ok(s) = String::from_utf8(to.clone()) { + if !s.is_ascii() { return } + if s.find(|x| x < ' ' && x != '\n').is_some() { return } + } else { + return + } + let diff = ed_diff::diff_w(&from, &to, "target/fuzz.file").unwrap(); + File::create("target/fuzz.file.original") + .unwrap() + .write_all(&from) + .unwrap(); + File::create("target/fuzz.file.expected") + .unwrap() + .write_all(&to) + .unwrap(); + File::create("target/fuzz.file") + .unwrap() + .write_all(&from) + .unwrap(); + File::create("target/fuzz.ed") + .unwrap() + .write_all(&diff) + .unwrap(); + let output = Command::new("ed") + .arg("target/fuzz.file") + .stdin(File::open("target/fuzz.ed").unwrap()) + .output() + .unwrap(); + if !output.status.success() { + panic!("STDOUT:\n{}\nSTDERR:\n{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr)); + } + let result = fs::read("target/fuzz.file").unwrap(); + if result != to { + panic!("STDOUT:\n{}\nSTDERR:\n{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr)); + } +}); + diff --git a/lib/ed-diff/src/lib.rs b/lib/ed-diff/src/lib.rs new file mode 100644 index 0000000..7bc8b2c --- /dev/null +++ b/lib/ed-diff/src/lib.rs @@ -0,0 +1,382 @@ +use std::io::Write; + +#[derive(Debug, PartialEq)] +struct Mismatch { + pub line_number_expected: usize, + pub line_number_actual: usize, + pub expected: Vec>, + pub actual: Vec>, +} + +#[derive(Debug, PartialEq, Eq)] +pub enum DiffError { + MissingNL, +} + +impl std::fmt::Display for DiffError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + std::fmt::Display::fmt("No newline at end of file", f) + } +} + +impl From for String { + fn from(_: DiffError) -> String { + "No newline at end of file".into() + } +} + +impl Mismatch { + fn new(line_number_expected: usize, line_number_actual: usize) -> Mismatch { + Mismatch { + line_number_expected, + line_number_actual, + expected: Vec::new(), + actual: Vec::new(), + } + } +} + +// Produces a diff between the expected output and actual output. +fn make_diff(expected: &[u8], actual: &[u8]) -> Result, DiffError> { + let mut line_number_expected = 1; + let mut line_number_actual = 1; + let mut results = Vec::new(); + let mut mismatch = Mismatch::new(line_number_expected, line_number_actual); + + let mut expected_lines: Vec<&[u8]> = expected.split(|&c| c == b'\n').collect(); + let mut actual_lines: Vec<&[u8]> = actual.split(|&c| c == b'\n').collect(); + + debug_assert_eq!(b"".split(|&c| c == b'\n').count(), 1); + // ^ means that underflow here is impossible + let expected_lines_count = expected_lines.len() - 1; + let actual_lines_count = actual_lines.len() - 1; + + if expected_lines.last() == Some(&&b""[..]) { + expected_lines.pop(); + } else { + return Err(DiffError::MissingNL); + } + + if actual_lines.last() == Some(&&b""[..]) { + actual_lines.pop(); + } else { + return Err(DiffError::MissingNL); + } + + for result in diff::slice(&expected_lines, &actual_lines) { + match result { + diff::Result::Left(str) => { + if mismatch.actual.len() != 0 { + results.push(mismatch); + mismatch = Mismatch::new(line_number_expected, line_number_actual); + } + mismatch.expected.push(str.to_vec()); + line_number_expected += 1; + } + diff::Result::Right(str) => { + mismatch.actual.push(str.to_vec()); + line_number_actual += 1; + } + diff::Result::Both(str, _) => { + line_number_expected += 1; + line_number_actual += 1; + if mismatch.actual.len() != 0 || mismatch.expected.len() != 0 { + results.push(mismatch); + mismatch = Mismatch::new(line_number_expected, line_number_actual); + } else { + mismatch.line_number_expected = line_number_expected; + mismatch.line_number_actual = line_number_actual; + } + } + } + } + + if mismatch.actual.len() != 0 || mismatch.expected.len() != 0 { + results.push(mismatch); + } + + Ok(results) +} + +pub fn diff(expected: &[u8], actual: &[u8]) -> Result, DiffError> { + let mut output = Vec::new(); + let diff_results = make_diff(expected, actual)?; + let mut lines_offset = 0; + for result in diff_results { + let line_number_expected: isize = result.line_number_expected as isize + lines_offset; + let line_number_actual: isize = result.line_number_actual as isize + lines_offset; + let expected_count: isize = result.expected.len() as isize; + let actual_count: isize = result.actual.len() as isize; + match (expected_count, actual_count) { + (0, 0) => unreachable!(), + (0, _) => writeln!( + &mut output, + "{}a", + line_number_expected - 1 + ) + .unwrap(), + (_, 0) => writeln!( + &mut output, + "{},{}d", + line_number_expected, + expected_count + line_number_expected - 1 + ) + .unwrap(), + _ => writeln!( + &mut output, + "{},{}c", + line_number_expected, + expected_count + line_number_expected - 1 + ) + .unwrap(), + } + lines_offset += actual_count - expected_count; + if actual_count != 0 { + for actual in &result.actual { + if actual == b"." { + writeln!(&mut output, "..\n.\ns/.//\na").unwrap(); + } else { + output.write_all(actual).unwrap(); + writeln!(&mut output, "").unwrap(); + } + } + writeln!(&mut output, ".").unwrap(); + } + } + return Ok(output) +} + +pub fn diff_w(expected: &[u8], actual: &[u8], filename: &str) -> Result, DiffError> { + let mut output = diff(expected, actual)?; + writeln!(&mut output, "w {}", filename).unwrap(); + Ok(output) +} + +#[test] +fn test_permutations() { + // test all possible six-line files. + let _ = std::fs::create_dir("target"); + for &a in &[0, 1, 2] { + for &b in &[0, 1, 2] { + for &c in &[0, 1, 2] { + for &d in &[0, 1, 2] { + for &e in &[0, 1, 2] { + for &f in &[0, 1, 2] { + use std::fs::{self, File}; + use std::io::Write; + use std::process::Command; + let mut alef = Vec::new(); + let mut bet = Vec::new(); + alef.write_all(if a == 0 { b"a\n" } else { b"b\n" }) + .unwrap(); + if a != 2 { + bet.write_all(b"b\n").unwrap(); + } + alef.write_all(if b == 0 { b"c\n" } else { b"d\n" }) + .unwrap(); + if b != 2 { + bet.write_all(b"d\n").unwrap(); + } + alef.write_all(if c == 0 { b"e\n" } else { b"f\n" }) + .unwrap(); + if c != 2 { + bet.write_all(b"f\n").unwrap(); + } + alef.write_all(if d == 0 { b"g\n" } else { b"h\n" }) + .unwrap(); + if d != 2 { + bet.write_all(b"h\n").unwrap(); + } + alef.write_all(if e == 0 { b"i\n" } else { b"j\n" }) + .unwrap(); + if e != 2 { + bet.write_all(b"j\n").unwrap(); + } + alef.write_all(if f == 0 { b"k\n" } else { b"l\n" }) + .unwrap(); + if f != 2 { + bet.write_all(b"l\n").unwrap(); + } + // This test diff is intentionally reversed. + // We want it to turn the alef into bet. + let diff = diff_w(&alef, &bet, "target/alef").unwrap(); + File::create("target/ab.ed") + .unwrap() + .write_all(&diff) + .unwrap(); + let mut fa = File::create("target/alef").unwrap(); + fa.write_all(&alef[..]).unwrap(); + let mut fb = File::create("target/bet").unwrap(); + fb.write_all(&bet[..]).unwrap(); + let _ = fa; + let _ = fb; + let output = Command::new("ed") + .arg("target/alef") + .stdin(File::open("target/ab.ed").unwrap()) + .output() + .unwrap(); + if !output.status.success() { + panic!("{:?}", output); + } + //println!("{}", String::from_utf8_lossy(&output.stdout)); + //println!("{}", String::from_utf8_lossy(&output.stderr)); + let alef = fs::read("target/alef").unwrap(); + assert_eq!(alef, bet); + } + } + } + } + } + } +} + + +#[test] +fn test_permutations_empty_lines() { + // test all possible six-line files with missing newlines. + let _ = std::fs::create_dir("target"); + for &a in &[0, 1, 2] { + for &b in &[0, 1, 2] { + for &c in &[0, 1, 2] { + for &d in &[0, 1, 2] { + for &e in &[0, 1, 2] { + for &f in &[0, 1, 2] { + use std::fs::{self, File}; + use std::io::Write; + use std::process::Command; + let mut alef = Vec::new(); + let mut bet = Vec::new(); + alef.write_all(if a == 0 { b"\n" } else { b"b\n" }).unwrap(); + if a != 2 { + bet.write_all(b"b\n").unwrap(); + } + alef.write_all(if b == 0 { b"\n" } else { b"d\n" }).unwrap(); + if b != 2 { + bet.write_all(b"d\n").unwrap(); + } + alef.write_all(if c == 0 { b"\n" } else { b"f\n" }).unwrap(); + if c != 2 { + bet.write_all(b"f\n").unwrap(); + } + alef.write_all(if d == 0 { b"\n" } else { b"h\n" }).unwrap(); + if d != 2 { + bet.write_all(b"h\n").unwrap(); + } + alef.write_all(if e == 0 { b"\n" } else { b"j\n" }).unwrap(); + if e != 2 { + bet.write_all(b"j\n").unwrap(); + } + alef.write_all(if f == 0 { b"\n" } else { b"l\n" }).unwrap(); + if f != 2 { + bet.write_all(b"l\n").unwrap(); + } + // This test diff is intentionally reversed. + // We want it to turn the alef into bet. + let diff = diff_w(&alef, &bet, "target/alef_").unwrap(); + File::create("target/ab_.ed") + .unwrap() + .write_all(&diff) + .unwrap(); + let mut fa = File::create("target/alef_").unwrap(); + fa.write_all(&alef[..]).unwrap(); + let mut fb = File::create("target/bet_").unwrap(); + fb.write_all(&bet[..]).unwrap(); + let _ = fa; + let _ = fb; + let output = Command::new("ed") + .arg("target/alef_") + .stdin(File::open("target/ab_.ed").unwrap()) + .output() + .unwrap(); + if !output.status.success() { + panic!("{:?}", output); + } + //println!("{}", String::from_utf8_lossy(&output.stdout)); + //println!("{}", String::from_utf8_lossy(&output.stderr)); + let alef = fs::read("target/alef_").unwrap(); + assert_eq!(alef, bet); + } + } + } + } + } + } +} + +#[test] +fn test_permutations_reverse() { + // test all possible six-line files. + let _ = std::fs::create_dir("target"); + for &a in &[0, 1, 2] { + for &b in &[0, 1, 2] { + for &c in &[0, 1, 2] { + for &d in &[0, 1, 2] { + for &e in &[0, 1, 2] { + for &f in &[0, 1, 2] { + use std::fs::{self, File}; + use std::io::Write; + use std::process::Command; + let mut alef = Vec::new(); + let mut bet = Vec::new(); + alef.write_all(if a == 0 { b"a\n" } else { b"f\n" }) + .unwrap(); + if a != 2 { + bet.write_all(b"a\n").unwrap(); + } + alef.write_all(if b == 0 { b"b\n" } else { b"e\n" }) + .unwrap(); + if b != 2 { + bet.write_all(b"b\n").unwrap(); + } + alef.write_all(if c == 0 { b"c\n" } else { b"d\n" }) + .unwrap(); + if c != 2 { + bet.write_all(b"c\n").unwrap(); + } + alef.write_all(if d == 0 { b"d\n" } else { b"c\n" }) + .unwrap(); + if d != 2 { + bet.write_all(b"d\n").unwrap(); + } + alef.write_all(if e == 0 { b"e\n" } else { b"b\n" }) + .unwrap(); + if e != 2 { + bet.write_all(b"e\n").unwrap(); + } + alef.write_all(if f == 0 { b"f\n" } else { b"a\n" }) + .unwrap(); + if f != 2 { + bet.write_all(b"f\n").unwrap(); + } + // This test diff is intentionally reversed. + // We want it to turn the alef into bet. + let diff = diff_w(&alef, &bet, "target/alefr").unwrap(); + File::create("target/abr.ed") + .unwrap() + .write_all(&diff) + .unwrap(); + let mut fa = File::create("target/alefr").unwrap(); + fa.write_all(&alef[..]).unwrap(); + let mut fb = File::create("target/betr").unwrap(); + fb.write_all(&bet[..]).unwrap(); + let _ = fa; + let _ = fb; + let output = Command::new("ed") + .arg("target/alefr") + .stdin(File::open("target/abr.ed").unwrap()) + .output() + .unwrap(); + if !output.status.success() { + panic!("{:?}", output); + } + //println!("{}", String::from_utf8_lossy(&output.stdout)); + //println!("{}", String::from_utf8_lossy(&output.stderr)); + let alef = fs::read("target/alefr").unwrap(); + assert_eq!(alef, bet); + } + } + } + } + } + } +} diff --git a/lib/normal-diff/Cargo.toml b/lib/normal-diff/Cargo.toml index ae73c3d..f5add71 100644 --- a/lib/normal-diff/Cargo.toml +++ b/lib/normal-diff/Cargo.toml @@ -1,10 +1,6 @@ [package] name = "normal-diff" version = "0.3.0" -authors = [ - "Michael Howell ", - "The Rust Project Developers" -] edition = "2018" description = "An implementation of the GNU unified diff format" license = "MIT OR Apache-2.0" diff --git a/lib/normal-diff/fuzz/Cargo.toml b/lib/normal-diff/fuzz/Cargo.toml index 03fd540..00f0553 100644 --- a/lib/normal-diff/fuzz/Cargo.toml +++ b/lib/normal-diff/fuzz/Cargo.toml @@ -2,7 +2,6 @@ [package] name = "normal-diff-fuzz" version = "0.0.0" -authors = ["Automatically generated"] publish = false edition = "2018" diff --git a/lib/unified-diff/Cargo.toml b/lib/unified-diff/Cargo.toml index 42df1e2..fa4f693 100644 --- a/lib/unified-diff/Cargo.toml +++ b/lib/unified-diff/Cargo.toml @@ -1,10 +1,6 @@ [package] name = "unified-diff" version = "0.3.0" -authors = [ - "Michael Howell ", - "The Rust Project Developers" -] edition = "2018" description = "An implementation of the GNU unified diff format" license = "MIT OR Apache-2.0" diff --git a/lib/unified-diff/fuzz/Cargo.toml b/lib/unified-diff/fuzz/Cargo.toml index 3b9e70f..5c6aafb 100644 --- a/lib/unified-diff/fuzz/Cargo.toml +++ b/lib/unified-diff/fuzz/Cargo.toml @@ -2,7 +2,6 @@ [package] name = "unified-diff-fuzz" version = "0.0.0" -authors = ["Automatically generated"] publish = false edition = "2018" From 61cfe6eec46c1a13ee222ccbc89f793534f27321 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 22 Jan 2024 18:41:01 +0100 Subject: [PATCH 004/194] Fix some clippy warnings --- bin/diffutils/params.rs | 18 +++++++++--------- lib/context-diff/src/lib.rs | 20 ++++++++------------ lib/ed-diff/src/lib.rs | 18 +++++++++--------- lib/normal-diff/src/lib.rs | 10 +++++----- lib/unified-diff/src/lib.rs | 4 ++-- 5 files changed, 33 insertions(+), 37 deletions(-) diff --git a/bin/diffutils/params.rs b/bin/diffutils/params.rs index 1c0dcf1..9c1517a 100644 --- a/bin/diffutils/params.rs +++ b/bin/diffutils/params.rs @@ -33,7 +33,7 @@ pub fn parse_params>(opts: I) -> Result from, None => { - return Err(format!("Usage: ")); + return Err("Usage: ".to_string()); } }; let mut from = None; @@ -55,8 +55,8 @@ pub fn parse_params>(opts: I) -> Result>(opts: I) -> Result { context_count = (b - b'0') as usize; while let Some(b'0'..=b'9') = bit.peek() { - context_count = context_count * 10; + context_count *= 10; context_count += (bit.next().unwrap() - b'0') as usize; } } b'c' => { if format.is_some() && format != Some(Format::Context) { - return Err(format!("Conflicting output style options")); + return Err("Conflicting output style options".to_string()); } format = Some(Format::Context); } b'e' => { if format.is_some() && format != Some(Format::Ed) { - return Err(format!("Conflicting output style options")); + return Err("Conflicting output style options".to_string()); } format = Some(Format::Ed); } b'u' => { if format.is_some() && format != Some(Format::Unified) { - return Err(format!("Conflicting output style options")); + return Err("Conflicting output style options".to_string()); } format = Some(Format::Unified); } b'U' => { if format.is_some() && format != Some(Format::Unified) { - return Err(format!("Conflicting output style options")); + return Err("Conflicting output style options".to_string()); } format = Some(Format::Unified); let context_count_maybe = if bit.peek().is_some() { @@ -102,7 +102,7 @@ pub fn parse_params>(opts: I) -> Result return Err(format!("Unknown option: {}", String::from_utf8_lossy(&[b]))), diff --git a/lib/context-diff/src/lib.rs b/lib/context-diff/src/lib.rs index 4974c8d..38eee45 100644 --- a/lib/context-diff/src/lib.rs +++ b/lib/context-diff/src/lib.rs @@ -1,6 +1,6 @@ use std::collections::VecDeque; use std::io::Write; -use std::mem; + #[derive(Debug, PartialEq)] pub enum DiffLine { @@ -106,7 +106,7 @@ fn make_diff(expected: &[u8], actual: &[u8], context_size: usize) -> Vec Vec Vec Result, DiffError> debug_assert_eq!(b"".split(|&c| c == b'\n').count(), 1); // ^ means that underflow here is impossible - let expected_lines_count = expected_lines.len() - 1; - let actual_lines_count = actual_lines.len() - 1; + let _expected_lines_count = expected_lines.len() - 1; + let _actual_lines_count = actual_lines.len() - 1; if expected_lines.last() == Some(&&b""[..]) { expected_lines.pop(); @@ -66,7 +66,7 @@ fn make_diff(expected: &[u8], actual: &[u8]) -> Result, DiffError> for result in diff::slice(&expected_lines, &actual_lines) { match result { diff::Result::Left(str) => { - if mismatch.actual.len() != 0 { + if !mismatch.actual.is_empty() { results.push(mismatch); mismatch = Mismatch::new(line_number_expected, line_number_actual); } @@ -77,10 +77,10 @@ fn make_diff(expected: &[u8], actual: &[u8]) -> Result, DiffError> mismatch.actual.push(str.to_vec()); line_number_actual += 1; } - diff::Result::Both(str, _) => { + diff::Result::Both(_str, _) => { line_number_expected += 1; line_number_actual += 1; - if mismatch.actual.len() != 0 || mismatch.expected.len() != 0 { + if !mismatch.actual.is_empty() || !mismatch.expected.is_empty() { results.push(mismatch); mismatch = Mismatch::new(line_number_expected, line_number_actual); } else { @@ -91,7 +91,7 @@ fn make_diff(expected: &[u8], actual: &[u8]) -> Result, DiffError> } } - if mismatch.actual.len() != 0 || mismatch.expected.len() != 0 { + if !mismatch.actual.is_empty() || !mismatch.expected.is_empty() { results.push(mismatch); } @@ -104,7 +104,7 @@ pub fn diff(expected: &[u8], actual: &[u8]) -> Result, DiffError> { let mut lines_offset = 0; for result in diff_results { let line_number_expected: isize = result.line_number_expected as isize + lines_offset; - let line_number_actual: isize = result.line_number_actual as isize + lines_offset; + let _line_number_actual: isize = result.line_number_actual as isize + lines_offset; let expected_count: isize = result.expected.len() as isize; let actual_count: isize = result.actual.len() as isize; match (expected_count, actual_count) { @@ -137,13 +137,13 @@ pub fn diff(expected: &[u8], actual: &[u8]) -> Result, DiffError> { writeln!(&mut output, "..\n.\ns/.//\na").unwrap(); } else { output.write_all(actual).unwrap(); - writeln!(&mut output, "").unwrap(); + writeln!(&mut output).unwrap(); } } writeln!(&mut output, ".").unwrap(); } } - return Ok(output) + Ok(output) } pub fn diff_w(expected: &[u8], actual: &[u8], filename: &str) -> Result, DiffError> { diff --git a/lib/normal-diff/src/lib.rs b/lib/normal-diff/src/lib.rs index 14ee175..5b5d5bc 100644 --- a/lib/normal-diff/src/lib.rs +++ b/lib/normal-diff/src/lib.rs @@ -49,7 +49,7 @@ fn make_diff(expected: &[u8], actual: &[u8]) -> Vec { for result in diff::slice(&expected_lines, &actual_lines) { match result { diff::Result::Left(str) => { - if mismatch.actual.len() != 0 && !mismatch.actual_missing_nl { + if !mismatch.actual.is_empty() && !mismatch.actual_missing_nl { results.push(mismatch); mismatch = Mismatch::new(line_number_expected, line_number_actual); } @@ -81,7 +81,7 @@ fn make_diff(expected: &[u8], actual: &[u8]) -> Vec { (true, true) | (false, false) => { line_number_expected += 1; line_number_actual += 1; - if mismatch.actual.len() != 0 || mismatch.expected.len() != 0 { + if !mismatch.actual.is_empty() || !mismatch.expected.is_empty() { results.push(mismatch); mismatch = Mismatch::new(line_number_expected, line_number_actual); } else { @@ -94,7 +94,7 @@ fn make_diff(expected: &[u8], actual: &[u8]) -> Vec { } } - if mismatch.actual.len() != 0 || mismatch.expected.len() != 0 { + if !mismatch.actual.is_empty() || !mismatch.expected.is_empty() { results.push(mismatch); } @@ -140,7 +140,7 @@ pub fn diff(expected: &[u8], actual: &[u8]) -> Vec { for expected in &result.expected { write!(&mut output, "< ").unwrap(); output.write_all(expected).unwrap(); - writeln!(&mut output, "").unwrap(); + writeln!(&mut output).unwrap(); } if result.expected_missing_nl { writeln!(&mut output, r"\ No newline at end of file").unwrap(); @@ -151,7 +151,7 @@ pub fn diff(expected: &[u8], actual: &[u8]) -> Vec { for actual in &result.actual { write!(&mut output, "> ").unwrap(); output.write_all(actual).unwrap(); - writeln!(&mut output, "").unwrap(); + writeln!(&mut output).unwrap(); } if result.actual_missing_nl { writeln!(&mut output, r"\ No newline at end of file").unwrap(); diff --git a/lib/unified-diff/src/lib.rs b/lib/unified-diff/src/lib.rs index a9f29a1..0caecf5 100644 --- a/lib/unified-diff/src/lib.rs +++ b/lib/unified-diff/src/lib.rs @@ -180,7 +180,7 @@ fn make_diff(expected: &[u8], actual: &[u8], context_size: usize) -> Vec Date: Mon, 22 Jan 2024 18:41:33 +0100 Subject: [PATCH 005/194] rustfmt the code --- bin/diffutils/main.rs | 5 +-- lib/context-diff/src/lib.rs | 7 ++-- lib/ed-diff/fuzz/fuzz_targets/fuzz_ed.rs | 36 +++++++++++++------ lib/ed-diff/src/lib.rs | 8 +---- .../fuzz/fuzz_targets/fuzz_patch.rs | 16 ++++++--- lib/normal-diff/src/lib.rs | 7 ++-- .../fuzz/fuzz_targets/fuzz_patch.rs | 24 ++++++++++--- 7 files changed, 68 insertions(+), 35 deletions(-) diff --git a/bin/diffutils/main.rs b/bin/diffutils/main.rs index d90dfc8..e083b28 100644 --- a/bin/diffutils/main.rs +++ b/bin/diffutils/main.rs @@ -44,10 +44,7 @@ fn main() -> Result<(), String> { &to.to_string_lossy(), context_count, ), - Format::Ed => ed_diff::diff( - &from_content, - &to_content, - )?, + Format::Ed => ed_diff::diff(&from_content, &to_content)?, }; io::stdout().write_all(&result).unwrap(); Ok(()) diff --git a/lib/context-diff/src/lib.rs b/lib/context-diff/src/lib.rs index 38eee45..033e8aa 100644 --- a/lib/context-diff/src/lib.rs +++ b/lib/context-diff/src/lib.rs @@ -1,7 +1,6 @@ use std::collections::VecDeque; use std::io::Write; - #[derive(Debug, PartialEq)] pub enum DiffLine { Context(Vec), @@ -232,13 +231,15 @@ fn make_diff(expected: &[u8], actual: &[u8], context_size: usize) -> Vec, Vec)| { from.push(b'\n'); to.push(b'\n'); if let Ok(s) = String::from_utf8(from.clone()) { - if !s.is_ascii() { return } - if s.find(|x| x < ' ' && x != '\n').is_some() { return } + if !s.is_ascii() { + return; + } + if s.find(|x| x < ' ' && x != '\n').is_some() { + return; + } } else { - return + return; } if let Ok(s) = String::from_utf8(to.clone()) { - if !s.is_ascii() { return } - if s.find(|x| x < ' ' && x != '\n').is_some() { return } + if !s.is_ascii() { + return; + } + if s.find(|x| x < ' ' && x != '\n').is_some() { + return; + } } else { - return + return; } let diff = ed_diff::diff_w(&from, &to, "target/fuzz.file").unwrap(); File::create("target/fuzz.file.original") @@ -45,11 +54,18 @@ fuzz_target!(|x: (Vec, Vec)| { .output() .unwrap(); if !output.status.success() { - panic!("STDOUT:\n{}\nSTDERR:\n{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr)); + panic!( + "STDOUT:\n{}\nSTDERR:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); } let result = fs::read("target/fuzz.file").unwrap(); if result != to { - panic!("STDOUT:\n{}\nSTDERR:\n{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr)); + panic!( + "STDOUT:\n{}\nSTDERR:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); } }); - diff --git a/lib/ed-diff/src/lib.rs b/lib/ed-diff/src/lib.rs index f83e2d1..bc59e51 100644 --- a/lib/ed-diff/src/lib.rs +++ b/lib/ed-diff/src/lib.rs @@ -109,12 +109,7 @@ pub fn diff(expected: &[u8], actual: &[u8]) -> Result, DiffError> { let actual_count: isize = result.actual.len() as isize; match (expected_count, actual_count) { (0, 0) => unreachable!(), - (0, _) => writeln!( - &mut output, - "{}a", - line_number_expected - 1 - ) - .unwrap(), + (0, _) => writeln!(&mut output, "{}a", line_number_expected - 1).unwrap(), (_, 0) => writeln!( &mut output, "{},{}d", @@ -230,7 +225,6 @@ fn test_permutations() { } } - #[test] fn test_permutations_empty_lines() { // test all possible six-line files with missing newlines. diff --git a/lib/normal-diff/fuzz/fuzz_targets/fuzz_patch.rs b/lib/normal-diff/fuzz/fuzz_targets/fuzz_patch.rs index 8816b98..d21beff 100644 --- a/lib/normal-diff/fuzz/fuzz_targets/fuzz_patch.rs +++ b/lib/normal-diff/fuzz/fuzz_targets/fuzz_patch.rs @@ -1,5 +1,6 @@ #![no_main] -#[macro_use] extern crate libfuzzer_sys; +#[macro_use] +extern crate libfuzzer_sys; extern crate normal_diff; use std::fs::{self, File}; @@ -47,11 +48,18 @@ fuzz_target!(|x: (Vec, Vec)| { .output() .unwrap(); if !output.status.success() { - panic!("STDOUT:\n{}\nSTDERR:\n{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr)); + panic!( + "STDOUT:\n{}\nSTDERR:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); } let result = fs::read("target/fuzz.file").unwrap(); if result != to { - panic!("STDOUT:\n{}\nSTDERR:\n{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr)); + panic!( + "STDOUT:\n{}\nSTDERR:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); } }); - diff --git a/lib/normal-diff/src/lib.rs b/lib/normal-diff/src/lib.rs index 5b5d5bc..f5d7cab 100644 --- a/lib/normal-diff/src/lib.rs +++ b/lib/normal-diff/src/lib.rs @@ -63,7 +63,10 @@ fn make_diff(expected: &[u8], actual: &[u8]) -> Vec { line_number_actual += 1; } diff::Result::Both(str, _) => { - match (line_number_expected > expected_lines_count, line_number_actual > actual_lines_count) { + match ( + line_number_expected > expected_lines_count, + line_number_actual > actual_lines_count, + ) { (true, false) => { line_number_expected += 1; line_number_actual += 1; @@ -88,7 +91,7 @@ fn make_diff(expected: &[u8], actual: &[u8]) -> Vec { mismatch.line_number_expected = line_number_expected; mismatch.line_number_actual = line_number_actual; } - }, + } } } } diff --git a/lib/unified-diff/fuzz/fuzz_targets/fuzz_patch.rs b/lib/unified-diff/fuzz/fuzz_targets/fuzz_patch.rs index f4cdb6b..1e8008b 100644 --- a/lib/unified-diff/fuzz/fuzz_targets/fuzz_patch.rs +++ b/lib/unified-diff/fuzz/fuzz_targets/fuzz_patch.rs @@ -1,5 +1,6 @@ #![no_main] -#[macro_use] extern crate libfuzzer_sys; +#[macro_use] +extern crate libfuzzer_sys; extern crate unified_diff; use std::fs::{self, File}; @@ -20,7 +21,13 @@ fuzz_target!(|x: (Vec, Vec, u8)| { } else { return }*/ - let diff = unified_diff::diff(&from, "a/fuzz.file", &to, "target/fuzz.file", context as usize); + let diff = unified_diff::diff( + &from, + "a/fuzz.file", + &to, + "target/fuzz.file", + context as usize, + ); File::create("target/fuzz.file.original") .unwrap() .write_all(&from) @@ -45,11 +52,18 @@ fuzz_target!(|x: (Vec, Vec, u8)| { .output() .unwrap(); if !output.status.success() { - panic!("STDOUT:\n{}\nSTDERR:\n{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr)); + panic!( + "STDOUT:\n{}\nSTDERR:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); } let result = fs::read("target/fuzz.file").unwrap(); if result != to { - panic!("STDOUT:\n{}\nSTDERR:\n{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr)); + panic!( + "STDOUT:\n{}\nSTDERR:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); } }); - From 029d747e145b0f27d12265ec35d6bc0cadb02022 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 22 Jan 2024 18:45:41 +0100 Subject: [PATCH 006/194] remove trailing spaces --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4fe36b1..fbfd5e5 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,11 @@ A package (currently just `diff`, but eventually more) of programs related to fi Based on the incomplete diff generator in https://github.com/rust-lang/rust/blob/master/src/tools/compiletest/src/runtest.rs, and made to be compatible with GNU's diff and patch tools. ``` -~/diffutils$ cargo run -- diff -u3 Cargo.lock Cargo.toml +~/diffutils$ cargo run -- diff -u3 Cargo.lock Cargo.toml Finished dev [unoptimized + debuginfo] target(s) in 0.00s Running `target/debug/diff -u3 Cargo.lock Cargo.toml` ---- Cargo.lock -+++ Cargo.toml +--- Cargo.lock ++++ Cargo.toml @@ -1,39 +1,7 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. @@ -55,4 +55,3 @@ Based on the incomplete diff generator in https://github.com/rust-lang/rust/blob + "bin/diff", ] ``` - From e9e69b86db7c368ce12061efab3821bd39c63781 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 22 Jan 2024 18:47:53 +0100 Subject: [PATCH 007/194] update of the README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index fbfd5e5..009f66a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -A package (currently just `diff`, but eventually more) of programs related to finding differences between files. +The goal of this package is to be a dropped in replacement for the [diffutils commands](https://www.gnu.org/software/diffutils/) in Rust. + Based on the incomplete diff generator in https://github.com/rust-lang/rust/blob/master/src/tools/compiletest/src/runtest.rs, and made to be compatible with GNU's diff and patch tools. From 12f3f167921f895316a3616ca9171490454c8735 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 22 Jan 2024 18:50:30 +0100 Subject: [PATCH 008/194] add github action --- .github/workflows/ci.yml | 167 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..33f1bc4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,167 @@ +on: [push, pull_request] + +name: Basic CI + +env: + CARGO_TERM_COLOR: always + +jobs: + check: + name: cargo check + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macOS-latest, windows-latest] + steps: + - uses: actions/checkout@v4 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + - uses: actions-rs/cargo@v1 + with: + command: check + + test: + name: cargo test + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macOS-latest, windows-latest] + steps: + - uses: actions/checkout@v4 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + - uses: actions-rs/cargo@v1 + with: + command: test + + fmt: + name: cargo fmt --all -- --check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + - run: rustup component add rustfmt + - uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + + clippy: + name: cargo clippy -- -D warnings + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macOS-latest, windows-latest] + steps: + - uses: actions/checkout@v4 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + - run: rustup component add clippy + - uses: actions-rs/cargo@v1 + with: + command: clippy + args: -- -D warnings + + coverage: + name: Code Coverage + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: true + matrix: + job: + - { os: ubuntu-latest , features: unix } + - { os: macos-latest , features: macos } + - { os: windows-latest , features: windows } + steps: + - uses: actions/checkout@v4 + - name: Initialize workflow variables + id: vars + shell: bash + run: | + ## VARs setup + outputs() { step_id="vars"; for var in "$@" ; do echo steps.${step_id}.outputs.${var}="${!var}"; echo "${var}=${!var}" >> $GITHUB_OUTPUT; done; } + # toolchain + TOOLCHAIN="nightly" ## default to "nightly" toolchain (required for certain required unstable compiler flags) ## !maint: refactor when stable channel has needed support + # * specify gnu-type TOOLCHAIN for windows; `grcov` requires gnu-style code coverage data files + case ${{ matrix.job.os }} in windows-*) TOOLCHAIN="$TOOLCHAIN-x86_64-pc-windows-gnu" ;; esac; + # * use requested TOOLCHAIN if specified + if [ -n "${{ matrix.job.toolchain }}" ]; then TOOLCHAIN="${{ matrix.job.toolchain }}" ; fi + outputs TOOLCHAIN + # target-specific options + # * CARGO_FEATURES_OPTION + CARGO_FEATURES_OPTION='--all -- --check' ; ## default to '--all-features' for code coverage + # * CODECOV_FLAGS + CODECOV_FLAGS=$( echo "${{ matrix.job.os }}" | sed 's/[^[:alnum:]]/_/g' ) + outputs CODECOV_FLAGS + + - name: rust toolchain ~ install + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ steps.vars.outputs.TOOLCHAIN }} + default: true + profile: minimal # minimal component installation (ie, no documentation) + - name: Test + uses: actions-rs/cargo@v1 + with: + command: test + args: ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} --no-fail-fast + env: + CARGO_INCREMENTAL: "0" + RUSTC_WRAPPER: "" + RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort" + RUSTDOCFLAGS: "-Cpanic=abort" + - name: "`grcov` ~ install" + id: build_grcov + shell: bash + run: | + git clone https://github.com/mozilla/grcov.git ~/grcov/ + cd ~/grcov + # Hardcode the version of crossbeam-epoch. See + # https://github.com/uutils/coreutils/issues/3680 + sed -i -e "s|tempfile =|crossbeam-epoch = \"=0.9.8\"\ntempfile =|" Cargo.toml + cargo install --path . + cd - +# Uncomment when the upstream issue +# https://github.com/mozilla/grcov/issues/849 is fixed +# uses: actions-rs/install@v0.1 +# with: +# crate: grcov +# version: latest +# use-tool-cache: false + - name: Generate coverage data (via `grcov`) + id: coverage + shell: bash + run: | + ## Generate coverage data + COVERAGE_REPORT_DIR="target/debug" + COVERAGE_REPORT_FILE="${COVERAGE_REPORT_DIR}/lcov.info" + mkdir -p "${COVERAGE_REPORT_DIR}" + # display coverage files + grcov . --output-type files --ignore build.rs --ignore "vendor/*" --ignore "/*" --ignore "[a-zA-Z]:/*" --excl-br-line "^\s*((debug_)?assert(_eq|_ne)?!|#\[derive\()" | sort --unique + # generate coverage report + grcov . --output-type lcov --output-path "${COVERAGE_REPORT_FILE}" --branch --ignore build.rs --ignore "vendor/*" --ignore "/*" --ignore "[a-zA-Z]:/*" --excl-br-line "^\s*((debug_)?assert(_eq|_ne)?!|#\[derive\()" + echo "report=${COVERAGE_REPORT_FILE}" >> $GITHUB_OUTPUT + - name: Upload coverage results (to Codecov.io) + uses: codecov/codecov-action@v3 + # if: steps.vars.outputs.HAS_CODECOV_TOKEN + with: + # token: ${{ secrets.CODECOV_TOKEN }} + file: ${{ steps.coverage.outputs.report }} + ## flags: IntegrationTests, UnitTests, ${{ steps.vars.outputs.CODECOV_FLAGS }} + flags: ${{ steps.vars.outputs.CODECOV_FLAGS }} + name: codecov-umbrella + fail_ci_if_error: false + From b55cbf2ca2f53b480d9b51563aafab2e8bba0235 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 22 Jan 2024 15:43:25 +0000 Subject: [PATCH 009/194] Add renovate.json --- renovate.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 renovate.json diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..5db72dd --- /dev/null +++ b/renovate.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:recommended" + ] +} From 045435b803a7247a8f2b4fd98d80b1bdf41c614b Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 22 Jan 2024 21:49:07 +0100 Subject: [PATCH 010/194] add missing license headers --- bin/diffutils/main.rs | 5 +++++ lib/context-diff/src/lib.rs | 5 +++++ lib/ed-diff/src/lib.rs | 5 +++++ lib/normal-diff/src/lib.rs | 5 +++++ lib/unified-diff/src/lib.rs | 5 +++++ 5 files changed, 25 insertions(+) diff --git a/bin/diffutils/main.rs b/bin/diffutils/main.rs index e083b28..39b97a3 100644 --- a/bin/diffutils/main.rs +++ b/bin/diffutils/main.rs @@ -1,3 +1,8 @@ +// This file is part of the uutils diffutils package. +// +// For the full copyright and license information, please view the LICENSE-* +// files that was distributed with this source code. + use crate::params::*; use std::env; diff --git a/lib/context-diff/src/lib.rs b/lib/context-diff/src/lib.rs index 033e8aa..b6348d4 100644 --- a/lib/context-diff/src/lib.rs +++ b/lib/context-diff/src/lib.rs @@ -1,3 +1,8 @@ +// This file is part of the uutils diffutils package. +// +// For the full copyright and license information, please view the LICENSE-* +// files that was distributed with this source code. + use std::collections::VecDeque; use std::io::Write; diff --git a/lib/ed-diff/src/lib.rs b/lib/ed-diff/src/lib.rs index bc59e51..5b686bf 100644 --- a/lib/ed-diff/src/lib.rs +++ b/lib/ed-diff/src/lib.rs @@ -1,3 +1,8 @@ +// This file is part of the uutils diffutils package. +// +// For the full copyright and license information, please view the LICENSE-* +// files that was distributed with this source code. + use std::io::Write; #[derive(Debug, PartialEq)] diff --git a/lib/normal-diff/src/lib.rs b/lib/normal-diff/src/lib.rs index f5d7cab..01d1a72 100644 --- a/lib/normal-diff/src/lib.rs +++ b/lib/normal-diff/src/lib.rs @@ -1,3 +1,8 @@ +// This file is part of the uutils diffutils package. +// +// For the full copyright and license information, please view the LICENSE-* +// files that was distributed with this source code. + use std::io::Write; #[derive(Debug, PartialEq)] diff --git a/lib/unified-diff/src/lib.rs b/lib/unified-diff/src/lib.rs index 0caecf5..9939134 100644 --- a/lib/unified-diff/src/lib.rs +++ b/lib/unified-diff/src/lib.rs @@ -1,3 +1,8 @@ +// This file is part of the uutils diffutils package. +// +// For the full copyright and license information, please view the LICENSE-* +// files that was distributed with this source code. + use std::collections::VecDeque; use std::io::Write; From 0b2505d2494bf8f0681f621f72a35cdd452d8393 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 22 Jan 2024 22:01:07 +0100 Subject: [PATCH 011/194] simplify the structure of the crate --- Cargo.toml | 22 ++++++++++++------- bin/diffutils/Cargo.toml | 6 ----- .../src/lib.rs => src/context_diff.rs | 0 lib/ed-diff/src/lib.rs => src/ed_diff.rs | 0 {bin/diffutils => src}/main.rs | 4 ++++ .../src/lib.rs => src/normal_diff.rs | 0 {bin/diffutils => src}/params.rs | 0 .../src/lib.rs => src/unified_diff.rs | 0 8 files changed, 18 insertions(+), 14 deletions(-) rename lib/context-diff/src/lib.rs => src/context_diff.rs (100%) rename lib/ed-diff/src/lib.rs => src/ed_diff.rs (100%) rename {bin/diffutils => src}/main.rs (95%) rename lib/normal-diff/src/lib.rs => src/normal_diff.rs (100%) rename {bin/diffutils => src}/params.rs (100%) rename lib/unified-diff/src/lib.rs => src/unified_diff.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index f2c907e..c0ae8bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,14 @@ -[workspace] -members = [ - "lib/unified-diff", - "lib/context-diff", - "lib/normal-diff", - "lib/ed-diff", - "bin/diffutils", -] +[package] +name = "diffutils" +version = "0.3.0" +edition = "2018" +description = "A CLI app for generating diff files" +license = "MIT OR Apache-2.0" +repository = "https://github.com/notriddle/diffutils" + +[[bin]] +name = "diffutils" +path = "src/main.rs" + +[dependencies] +diff = "0.1.10" diff --git a/bin/diffutils/Cargo.toml b/bin/diffutils/Cargo.toml index a152cf3..63f2f09 100644 --- a/bin/diffutils/Cargo.toml +++ b/bin/diffutils/Cargo.toml @@ -9,9 +9,3 @@ repository = "https://github.com/notriddle/diffutils" [[bin]] name = "diffutils" path = "main.rs" - -[dependencies] -unified-diff = { path = "../../lib/unified-diff/" } -context-diff = { path = "../../lib/context-diff/" } -normal-diff = { path = "../../lib/normal-diff/" } -ed-diff = { path = "../../lib/ed-diff/" } diff --git a/lib/context-diff/src/lib.rs b/src/context_diff.rs similarity index 100% rename from lib/context-diff/src/lib.rs rename to src/context_diff.rs diff --git a/lib/ed-diff/src/lib.rs b/src/ed_diff.rs similarity index 100% rename from lib/ed-diff/src/lib.rs rename to src/ed_diff.rs diff --git a/bin/diffutils/main.rs b/src/main.rs similarity index 95% rename from bin/diffutils/main.rs rename to src/main.rs index 39b97a3..8896074 100644 --- a/bin/diffutils/main.rs +++ b/src/main.rs @@ -10,6 +10,10 @@ use std::fs; use std::io::{self, Write}; mod params; +mod normal_diff; +mod unified_diff; +mod context_diff; +mod ed_diff; fn main() -> Result<(), String> { let opts = env::args_os(); diff --git a/lib/normal-diff/src/lib.rs b/src/normal_diff.rs similarity index 100% rename from lib/normal-diff/src/lib.rs rename to src/normal_diff.rs diff --git a/bin/diffutils/params.rs b/src/params.rs similarity index 100% rename from bin/diffutils/params.rs rename to src/params.rs diff --git a/lib/unified-diff/src/lib.rs b/src/unified_diff.rs similarity index 100% rename from lib/unified-diff/src/lib.rs rename to src/unified_diff.rs From f42fc82f18a8b76365434eecefd8b1f47ec815e8 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 22 Jan 2024 22:32:11 +0100 Subject: [PATCH 012/194] adjust the fuzzers to use the new structure --- Cargo.toml | 4 ++++ {lib/unified-diff/fuzz => fuzz}/.gitignore | 0 {lib/unified-diff/fuzz => fuzz}/Cargo.toml | 18 +++++++++++++++--- .../fuzz => fuzz}/fuzz_targets/fuzz_ed.rs | 3 +-- .../fuzz_targets/fuzz_normal.rs | 2 +- .../fuzz => fuzz}/fuzz_targets/fuzz_patch.rs | 3 +-- lib/normal-diff/fuzz/Cargo.toml | 12 +----------- src/lib.rs | 11 +++++++++++ src/main.rs | 1 + 9 files changed, 35 insertions(+), 19 deletions(-) rename {lib/unified-diff/fuzz => fuzz}/.gitignore (100%) rename {lib/unified-diff/fuzz => fuzz}/Cargo.toml (61%) rename {lib/ed-diff/fuzz => fuzz}/fuzz_targets/fuzz_ed.rs (97%) rename lib/normal-diff/fuzz/fuzz_targets/fuzz_patch.rs => fuzz/fuzz_targets/fuzz_normal.rs (97%) rename {lib/unified-diff/fuzz => fuzz}/fuzz_targets/fuzz_patch.rs (97%) create mode 100644 src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index c0ae8bf..7f33327 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,10 @@ description = "A CLI app for generating diff files" license = "MIT OR Apache-2.0" repository = "https://github.com/notriddle/diffutils" +[lib] +name = "diffutils" +path = "src/lib.rs" + [[bin]] name = "diffutils" path = "src/main.rs" diff --git a/lib/unified-diff/fuzz/.gitignore b/fuzz/.gitignore similarity index 100% rename from lib/unified-diff/fuzz/.gitignore rename to fuzz/.gitignore diff --git a/lib/unified-diff/fuzz/Cargo.toml b/fuzz/Cargo.toml similarity index 61% rename from lib/unified-diff/fuzz/Cargo.toml rename to fuzz/Cargo.toml index 5c6aafb..c1bebc9 100644 --- a/lib/unified-diff/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -10,9 +10,7 @@ cargo-fuzz = true [dependencies] libfuzzer-sys = "0.3" - -[dependencies.unified-diff] -path = ".." +diffutils = { path = "../" } # Prevent this from interfering with workspaces [workspace] @@ -24,3 +22,17 @@ path = "fuzz_targets/fuzz_patch.rs" test = false doc = false +[[bin]] +name = "fuzz_normal" +path = "fuzz_targets/fuzz_normal.rs" +test = false +doc = false + + +[[bin]] +name = "fuzz_ed" +path = "fuzz_targets/fuzz_ed.rs" +test = false +doc = false + + diff --git a/lib/ed-diff/fuzz/fuzz_targets/fuzz_ed.rs b/fuzz/fuzz_targets/fuzz_ed.rs similarity index 97% rename from lib/ed-diff/fuzz/fuzz_targets/fuzz_ed.rs rename to fuzz/fuzz_targets/fuzz_ed.rs index 50b64c5..95b5d4d 100644 --- a/lib/ed-diff/fuzz/fuzz_targets/fuzz_ed.rs +++ b/fuzz/fuzz_targets/fuzz_ed.rs @@ -1,8 +1,7 @@ #![no_main] #[macro_use] extern crate libfuzzer_sys; -extern crate ed_diff; - +use diffutils::{unified_diff, ed_diff, normal_diff}; use std::fs::{self, File}; use std::io::Write; use std::process::Command; diff --git a/lib/normal-diff/fuzz/fuzz_targets/fuzz_patch.rs b/fuzz/fuzz_targets/fuzz_normal.rs similarity index 97% rename from lib/normal-diff/fuzz/fuzz_targets/fuzz_patch.rs rename to fuzz/fuzz_targets/fuzz_normal.rs index d21beff..7af9852 100644 --- a/lib/normal-diff/fuzz/fuzz_targets/fuzz_patch.rs +++ b/fuzz/fuzz_targets/fuzz_normal.rs @@ -1,7 +1,7 @@ #![no_main] #[macro_use] extern crate libfuzzer_sys; -extern crate normal_diff; +use diffutils::{unified_diff, normal_diff}; use std::fs::{self, File}; use std::io::Write; diff --git a/lib/unified-diff/fuzz/fuzz_targets/fuzz_patch.rs b/fuzz/fuzz_targets/fuzz_patch.rs similarity index 97% rename from lib/unified-diff/fuzz/fuzz_targets/fuzz_patch.rs rename to fuzz/fuzz_targets/fuzz_patch.rs index 1e8008b..f964a8b 100644 --- a/lib/unified-diff/fuzz/fuzz_targets/fuzz_patch.rs +++ b/fuzz/fuzz_targets/fuzz_patch.rs @@ -1,8 +1,7 @@ #![no_main] #[macro_use] extern crate libfuzzer_sys; -extern crate unified_diff; - +use diffutils::{unified_diff, normal_diff}; use std::fs::{self, File}; use std::io::Write; use std::process::Command; diff --git a/lib/normal-diff/fuzz/Cargo.toml b/lib/normal-diff/fuzz/Cargo.toml index 00f0553..4274bbe 100644 --- a/lib/normal-diff/fuzz/Cargo.toml +++ b/lib/normal-diff/fuzz/Cargo.toml @@ -3,7 +3,7 @@ name = "normal-diff-fuzz" version = "0.0.0" publish = false -edition = "2018" +edition = "2021" [package.metadata] cargo-fuzz = true @@ -11,16 +11,6 @@ cargo-fuzz = true [dependencies] libfuzzer-sys = "0.3" -[dependencies.normal-diff] -path = ".." - # Prevent this from interfering with workspaces [workspace] members = ["."] - -[[bin]] -name = "fuzz_patch" -path = "fuzz_targets/fuzz_patch.rs" -test = false -doc = false - diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..64811e3 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,11 @@ + +pub mod unified_diff; +pub mod normal_diff; +pub mod ed_diff; +pub mod context_diff; + +// Re-export the public functions/types you need +pub use unified_diff::diff as unified_diff; +pub use normal_diff::diff as normal_diff; +pub use ed_diff::diff as ed_diff; +pub use context_diff::diff as context_diff; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 8896074..15abb8e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,7 @@ use std::env; use std::fs; use std::io::{self, Write}; + mod params; mod normal_diff; mod unified_diff; From 9084134f045e404e841101352d1079c68b5bfe7a Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 22 Jan 2024 22:32:32 +0100 Subject: [PATCH 013/194] rustfmt the code --- fuzz/fuzz_targets/fuzz_ed.rs | 2 +- fuzz/fuzz_targets/fuzz_normal.rs | 2 +- fuzz/fuzz_targets/fuzz_patch.rs | 2 +- src/lib.rs | 13 ++++++------- src/main.rs | 7 +++---- 5 files changed, 12 insertions(+), 14 deletions(-) diff --git a/fuzz/fuzz_targets/fuzz_ed.rs b/fuzz/fuzz_targets/fuzz_ed.rs index 95b5d4d..e46908d 100644 --- a/fuzz/fuzz_targets/fuzz_ed.rs +++ b/fuzz/fuzz_targets/fuzz_ed.rs @@ -1,7 +1,7 @@ #![no_main] #[macro_use] extern crate libfuzzer_sys; -use diffutils::{unified_diff, ed_diff, normal_diff}; +use diffutils::{ed_diff, normal_diff, unified_diff}; use std::fs::{self, File}; use std::io::Write; use std::process::Command; diff --git a/fuzz/fuzz_targets/fuzz_normal.rs b/fuzz/fuzz_targets/fuzz_normal.rs index 7af9852..4e114d2 100644 --- a/fuzz/fuzz_targets/fuzz_normal.rs +++ b/fuzz/fuzz_targets/fuzz_normal.rs @@ -1,7 +1,7 @@ #![no_main] #[macro_use] extern crate libfuzzer_sys; -use diffutils::{unified_diff, normal_diff}; +use diffutils::{normal_diff, unified_diff}; use std::fs::{self, File}; use std::io::Write; diff --git a/fuzz/fuzz_targets/fuzz_patch.rs b/fuzz/fuzz_targets/fuzz_patch.rs index f964a8b..c190d76 100644 --- a/fuzz/fuzz_targets/fuzz_patch.rs +++ b/fuzz/fuzz_targets/fuzz_patch.rs @@ -1,7 +1,7 @@ #![no_main] #[macro_use] extern crate libfuzzer_sys; -use diffutils::{unified_diff, normal_diff}; +use diffutils::{normal_diff, unified_diff}; use std::fs::{self, File}; use std::io::Write; use std::process::Command; diff --git a/src/lib.rs b/src/lib.rs index 64811e3..a78b64d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,11 +1,10 @@ - -pub mod unified_diff; -pub mod normal_diff; -pub mod ed_diff; pub mod context_diff; +pub mod ed_diff; +pub mod normal_diff; +pub mod unified_diff; // Re-export the public functions/types you need -pub use unified_diff::diff as unified_diff; -pub use normal_diff::diff as normal_diff; +pub use context_diff::diff as context_diff; pub use ed_diff::diff as ed_diff; -pub use context_diff::diff as context_diff; \ No newline at end of file +pub use normal_diff::diff as normal_diff; +pub use unified_diff::diff as unified_diff; diff --git a/src/main.rs b/src/main.rs index 15abb8e..ad0555e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,12 +9,11 @@ use std::env; use std::fs; use std::io::{self, Write}; - -mod params; -mod normal_diff; -mod unified_diff; mod context_diff; mod ed_diff; +mod normal_diff; +mod params; +mod unified_diff; fn main() -> Result<(), String> { let opts = env::args_os(); From 416a4be06cf359aaf78fa1d2ef1048502fee456a Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 22 Jan 2024 22:32:49 +0100 Subject: [PATCH 014/194] remove old files --- lib/context-diff/Cargo.toml | 11 ----------- lib/ed-diff/Cargo.toml | 11 ----------- lib/ed-diff/fuzz/.gitignore | 4 ---- lib/ed-diff/fuzz/Cargo.toml | 26 -------------------------- lib/normal-diff/Cargo.toml | 11 ----------- lib/normal-diff/fuzz/.gitignore | 4 ---- lib/normal-diff/fuzz/Cargo.toml | 16 ---------------- lib/unified-diff/Cargo.toml | 11 ----------- 8 files changed, 94 deletions(-) delete mode 100644 lib/context-diff/Cargo.toml delete mode 100644 lib/ed-diff/Cargo.toml delete mode 100644 lib/ed-diff/fuzz/.gitignore delete mode 100644 lib/ed-diff/fuzz/Cargo.toml delete mode 100644 lib/normal-diff/Cargo.toml delete mode 100644 lib/normal-diff/fuzz/.gitignore delete mode 100644 lib/normal-diff/fuzz/Cargo.toml delete mode 100644 lib/unified-diff/Cargo.toml diff --git a/lib/context-diff/Cargo.toml b/lib/context-diff/Cargo.toml deleted file mode 100644 index 4c8f714..0000000 --- a/lib/context-diff/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "context-diff" -version = "0.3.0" -edition = "2018" -description = "An implementation of the GNU unified diff format" -license = "MIT OR Apache-2.0" -repository = "https://github.com/notriddle/diffutils" -exclude = [ "fuzz" ] - -[dependencies] -diff = "0.1.10" diff --git a/lib/ed-diff/Cargo.toml b/lib/ed-diff/Cargo.toml deleted file mode 100644 index 0b2e90d..0000000 --- a/lib/ed-diff/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "ed-diff" -version = "0.3.0" -edition = "2018" -description = "An implementation of the GNU unified diff format" -license = "MIT OR Apache-2.0" -repository = "https://github.com/notriddle/diffutils" -exclude = [ "fuzz" ] - -[dependencies] -diff = "0.1.10" diff --git a/lib/ed-diff/fuzz/.gitignore b/lib/ed-diff/fuzz/.gitignore deleted file mode 100644 index 572e03b..0000000 --- a/lib/ed-diff/fuzz/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ - -target -corpus -artifacts diff --git a/lib/ed-diff/fuzz/Cargo.toml b/lib/ed-diff/fuzz/Cargo.toml deleted file mode 100644 index 2f3460d..0000000 --- a/lib/ed-diff/fuzz/Cargo.toml +++ /dev/null @@ -1,26 +0,0 @@ - -[package] -name = "ed-diff-fuzz" -version = "0.0.0" -publish = false -edition = "2018" - -[package.metadata] -cargo-fuzz = true - -[dependencies] -libfuzzer-sys = "0.3" - -[dependencies.ed-diff] -path = ".." - -# Prevent this from interfering with workspaces -[workspace] -members = ["."] - -[[bin]] -name = "fuzz_ed" -path = "fuzz_targets/fuzz_ed.rs" -test = false -doc = false - diff --git a/lib/normal-diff/Cargo.toml b/lib/normal-diff/Cargo.toml deleted file mode 100644 index f5add71..0000000 --- a/lib/normal-diff/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "normal-diff" -version = "0.3.0" -edition = "2018" -description = "An implementation of the GNU unified diff format" -license = "MIT OR Apache-2.0" -repository = "https://github.com/notriddle/diffutils" -exclude = [ "fuzz" ] - -[dependencies] -diff = "0.1.10" diff --git a/lib/normal-diff/fuzz/.gitignore b/lib/normal-diff/fuzz/.gitignore deleted file mode 100644 index 572e03b..0000000 --- a/lib/normal-diff/fuzz/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ - -target -corpus -artifacts diff --git a/lib/normal-diff/fuzz/Cargo.toml b/lib/normal-diff/fuzz/Cargo.toml deleted file mode 100644 index 4274bbe..0000000 --- a/lib/normal-diff/fuzz/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ - -[package] -name = "normal-diff-fuzz" -version = "0.0.0" -publish = false -edition = "2021" - -[package.metadata] -cargo-fuzz = true - -[dependencies] -libfuzzer-sys = "0.3" - -# Prevent this from interfering with workspaces -[workspace] -members = ["."] diff --git a/lib/unified-diff/Cargo.toml b/lib/unified-diff/Cargo.toml deleted file mode 100644 index fa4f693..0000000 --- a/lib/unified-diff/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "unified-diff" -version = "0.3.0" -edition = "2018" -description = "An implementation of the GNU unified diff format" -license = "MIT OR Apache-2.0" -repository = "https://github.com/notriddle/diffutils" -exclude = [ "fuzz" ] - -[dependencies] -diff = "0.1.10" From 7b3001f1ffe34aa32ca9e4ea90de3382a5f392bc Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 22 Jan 2024 22:33:29 +0100 Subject: [PATCH 015/194] adjust the rule --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 7f33327..d1c57d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "0.3.0" edition = "2018" description = "A CLI app for generating diff files" license = "MIT OR Apache-2.0" -repository = "https://github.com/notriddle/diffutils" +repository = "https://github.com/uutils/diffutils" [lib] name = "diffutils" From 7939749338877beb8b42093f56a27efb43d747a1 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 22 Jan 2024 22:33:47 +0100 Subject: [PATCH 016/194] remove old config --- bin/diffutils/Cargo.toml | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 bin/diffutils/Cargo.toml diff --git a/bin/diffutils/Cargo.toml b/bin/diffutils/Cargo.toml deleted file mode 100644 index 63f2f09..0000000 --- a/bin/diffutils/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "diffutils" -version = "0.3.0" -edition = "2018" -description = "A CLI app for generating diff files" -license = "MIT OR Apache-2.0" -repository = "https://github.com/notriddle/diffutils" - -[[bin]] -name = "diffutils" -path = "main.rs" From a10ef621c8b889d3ed134369079a4ec302018c5b Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Tue, 23 Jan 2024 09:52:49 +0100 Subject: [PATCH 017/194] Ajust the test paths --- src/context_diff.rs | 63 ++++++++++++++++++++---------------- src/ed_diff.rs | 31 ++++++++++-------- src/normal_diff.rs | 60 ++++++++++++++++++---------------- src/unified_diff.rs | 79 +++++++++++++++++++++++++-------------------- 4 files changed, 128 insertions(+), 105 deletions(-) diff --git a/src/context_diff.rs b/src/context_diff.rs index b6348d4..1526d4d 100644 --- a/src/context_diff.rs +++ b/src/context_diff.rs @@ -356,7 +356,8 @@ pub fn diff( #[test] fn test_permutations() { // test all possible six-line files. - let _ = std::fs::create_dir("target"); + let target = "target/context-diff/"; + let _ = std::fs::create_dir(target); for &a in &[0, 1, 2] { for &b in &[0, 1, 2] { for &c in &[0, 1, 2] { @@ -400,21 +401,21 @@ fn test_permutations() { } // This test diff is intentionally reversed. // We want it to turn the alef into bet. - let diff = diff(&alef, "a/alef", &bet, "target/alef", 2); - File::create("target/ab.diff") + let diff = diff(&alef, "a/alef", &bet, &format!("{}/alef", target), 2); + File::create(&format!("{}/ab.diff", target)) .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create("target/alef").unwrap(); + let mut fa = File::create(&format!("{}/alef", target)).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create("target/bet").unwrap(); + let mut fb = File::create(&format!("{}/bet", target)).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") .arg("--context") - .stdin(File::open("target/ab.diff").unwrap()) + .stdin(File::open(&format!("{}/ab.diff", target)).unwrap()) .output() .unwrap(); if !output.status.success() { @@ -422,7 +423,7 @@ fn test_permutations() { } //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read("target/alef").unwrap(); + let alef = fs::read(&format!("{}/alef", target)).unwrap(); assert_eq!(alef, bet); } } @@ -434,8 +435,9 @@ fn test_permutations() { #[test] fn test_permutations_empty_lines() { + let target = "target/context-diff/"; // test all possible six-line files with missing newlines. - let _ = std::fs::create_dir("target"); + let _ = std::fs::create_dir(target); for &a in &[0, 1, 2] { for &b in &[0, 1, 2] { for &c in &[0, 1, 2] { @@ -473,21 +475,22 @@ fn test_permutations_empty_lines() { } // This test diff is intentionally reversed. // We want it to turn the alef into bet. - let diff = diff(&alef, "a/alef_", &bet, "target/alef_", 2); - File::create("target/ab_.diff") + let diff = + diff(&alef, "a/alef_", &bet, &format!("{}/alef_", target), 2); + File::create(&format!("{}/ab_.diff", target)) .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create("target/alef_").unwrap(); + let mut fa = File::create(&format!("{}/alef_", target)).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create("target/bet_").unwrap(); + let mut fb = File::create(&format!("{}/bet_", target)).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") .arg("--context") - .stdin(File::open("target/ab_.diff").unwrap()) + .stdin(File::open(&format!("{}/ab_.diff", target)).unwrap()) .output() .unwrap(); if !output.status.success() { @@ -495,7 +498,7 @@ fn test_permutations_empty_lines() { } //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read("target/alef_").unwrap(); + let alef = fs::read(&format!("{}/alef_", target)).unwrap(); assert_eq!(alef, bet); } } @@ -507,8 +510,9 @@ fn test_permutations_empty_lines() { #[test] fn test_permutations_missing_lines() { + let target = "target/context-diff/"; // test all possible six-line files. - let _ = std::fs::create_dir("target"); + let _ = std::fs::create_dir(target); for &a in &[0, 1, 2] { for &b in &[0, 1, 2] { for &c in &[0, 1, 2] { @@ -549,21 +553,22 @@ fn test_permutations_missing_lines() { }; // This test diff is intentionally reversed. // We want it to turn the alef into bet. - let diff = diff(&alef, "a/alefx", &bet, "target/alefx", 2); - File::create("target/abx.diff") + let diff = + diff(&alef, "a/alefx", &bet, &format!("{}/alefx", target), 2); + File::create(&format!("{}/abx.diff", target)) .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create("target/alefx").unwrap(); + let mut fa = File::create(&format!("{}/alefx", target)).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create("target/betx").unwrap(); + let mut fb = File::create(&format!("{}/betx", target)).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") .arg("--context") - .stdin(File::open("target/abx.diff").unwrap()) + .stdin(File::open(&format!("{}/abx.diff", target)).unwrap()) .output() .unwrap(); if !output.status.success() { @@ -571,7 +576,7 @@ fn test_permutations_missing_lines() { } //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read("target/alefx").unwrap(); + let alef = fs::read(&format!("{}/alefx", target)).unwrap(); assert_eq!(alef, bet); } } @@ -583,8 +588,9 @@ fn test_permutations_missing_lines() { #[test] fn test_permutations_reverse() { + let target = "target/context-diff/"; // test all possible six-line files. - let _ = std::fs::create_dir("target"); + let _ = std::fs::create_dir(target); for &a in &[0, 1, 2] { for &b in &[0, 1, 2] { for &c in &[0, 1, 2] { @@ -628,21 +634,22 @@ fn test_permutations_reverse() { } // This test diff is intentionally reversed. // We want it to turn the alef into bet. - let diff = diff(&alef, "a/alefr", &bet, "target/alefr", 2); - File::create("target/abr.diff") + let diff = + diff(&alef, "a/alefr", &bet, &format!("{}/alefr", target), 2); + File::create(&format!("{}/abr.diff", target)) .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create("target/alefr").unwrap(); + let mut fa = File::create(&format!("{}/alefr", target)).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create("target/betr").unwrap(); + let mut fb = File::create(&format!("{}/betr", target)).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") .arg("--context") - .stdin(File::open("target/abr.diff").unwrap()) + .stdin(File::open(&format!("{}/abr.diff", target)).unwrap()) .output() .unwrap(); if !output.status.success() { @@ -650,7 +657,7 @@ fn test_permutations_reverse() { } //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read("target/alefr").unwrap(); + let alef = fs::read(&format!("{}/alefr", target)).unwrap(); assert_eq!(alef, bet); } } diff --git a/src/ed_diff.rs b/src/ed_diff.rs index 5b686bf..96cd44d 100644 --- a/src/ed_diff.rs +++ b/src/ed_diff.rs @@ -154,8 +154,9 @@ pub fn diff_w(expected: &[u8], actual: &[u8], filename: &str) -> Result, #[test] fn test_permutations() { + let target = "target/ed-diff/"; // test all possible six-line files. - let _ = std::fs::create_dir("target"); + let _ = std::fs::create_dir(target); for &a in &[0, 1, 2] { for &b in &[0, 1, 2] { for &c in &[0, 1, 2] { @@ -199,19 +200,19 @@ fn test_permutations() { } // This test diff is intentionally reversed. // We want it to turn the alef into bet. - let diff = diff_w(&alef, &bet, "target/alef").unwrap(); + let diff = diff_w(&alef, &bet, &format!("{}/alef", target)).unwrap(); File::create("target/ab.ed") .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create("target/alef").unwrap(); + let mut fa = File::create(&format!("{}/alef", target)).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create("target/bet").unwrap(); + let mut fb = File::create(&format!("{}/bet", target)).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("ed") - .arg("target/alef") + .arg(&format!("{}/alef", target)) .stdin(File::open("target/ab.ed").unwrap()) .output() .unwrap(); @@ -220,7 +221,7 @@ fn test_permutations() { } //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read("target/alef").unwrap(); + let alef = fs::read(&format!("{}/alef", target)).unwrap(); assert_eq!(alef, bet); } } @@ -232,8 +233,9 @@ fn test_permutations() { #[test] fn test_permutations_empty_lines() { + let target = "target/ed-diff/"; // test all possible six-line files with missing newlines. - let _ = std::fs::create_dir("target"); + let _ = std::fs::create_dir(target); for &a in &[0, 1, 2] { for &b in &[0, 1, 2] { for &c in &[0, 1, 2] { @@ -278,7 +280,7 @@ fn test_permutations_empty_lines() { .unwrap(); let mut fa = File::create("target/alef_").unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create("target/bet_").unwrap(); + let mut fb = File::create(&format!("{}/bet_", target)).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; @@ -304,8 +306,9 @@ fn test_permutations_empty_lines() { #[test] fn test_permutations_reverse() { + let target = "target/ed-diff/"; // test all possible six-line files. - let _ = std::fs::create_dir("target"); + let _ = std::fs::create_dir(target); for &a in &[0, 1, 2] { for &b in &[0, 1, 2] { for &c in &[0, 1, 2] { @@ -349,19 +352,19 @@ fn test_permutations_reverse() { } // This test diff is intentionally reversed. // We want it to turn the alef into bet. - let diff = diff_w(&alef, &bet, "target/alefr").unwrap(); + let diff = diff_w(&alef, &bet, &format!("{}/alefr", target)).unwrap(); File::create("target/abr.ed") .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create("target/alefr").unwrap(); + let mut fa = File::create(&format!("{}/alefr", target)).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create("target/betr").unwrap(); + let mut fb = File::create(&format!("{}/betr", target)).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("ed") - .arg("target/alefr") + .arg(&format!("{}/alefr", target)) .stdin(File::open("target/abr.ed").unwrap()) .output() .unwrap(); @@ -370,7 +373,7 @@ fn test_permutations_reverse() { } //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read("target/alefr").unwrap(); + let alef = fs::read(&format!("{}/alefr", target)).unwrap(); assert_eq!(alef, bet); } } diff --git a/src/normal_diff.rs b/src/normal_diff.rs index 01d1a72..02334f2 100644 --- a/src/normal_diff.rs +++ b/src/normal_diff.rs @@ -170,8 +170,9 @@ pub fn diff(expected: &[u8], actual: &[u8]) -> Vec { #[test] fn test_permutations() { + let target = "target/normal-diff/"; // test all possible six-line files. - let _ = std::fs::create_dir("target"); + let _ = std::fs::create_dir(target); for &a in &[0, 1, 2] { for &b in &[0, 1, 2] { for &c in &[0, 1, 2] { @@ -216,20 +217,20 @@ fn test_permutations() { // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff(&alef, &bet); - File::create("target/ab.diff") + File::create(&format!("{}/ab.diff", target)) .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create("target/alef").unwrap(); + let mut fa = File::create(&format!("{}/alef", target)).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create("target/bet").unwrap(); + let mut fb = File::create(&format!("{}/bet", target)).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") - .arg("target/alef") - .stdin(File::open("target/ab.diff").unwrap()) + .arg(&format!("{}/alef", target)) + .stdin(File::open(&format!("{}/ab.diff", target)).unwrap()) .output() .unwrap(); if !output.status.success() { @@ -237,7 +238,7 @@ fn test_permutations() { } //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read("target/alef").unwrap(); + let alef = fs::read(&format!("{}/alef", target)).unwrap(); assert_eq!(alef, bet); } } @@ -249,8 +250,9 @@ fn test_permutations() { #[test] fn test_permutations_missing_line_ending() { + let target = "target/normal-diff/"; // test all possible six-line files with missing newlines. - let _ = std::fs::create_dir("target"); + let _ = std::fs::create_dir(target); for &a in &[0, 1, 2] { for &b in &[0, 1, 2] { for &c in &[0, 1, 2] { @@ -309,21 +311,21 @@ fn test_permutations_missing_line_ending() { // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff(&alef, &bet); - File::create("target/abn.diff") + File::create(&format!("{}/abn.diff", target)) .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create("target/alefn").unwrap(); + let mut fa = File::create(&format!("{}/alefn", target)).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create("target/betn").unwrap(); + let mut fb = File::create(&format!("{}/betn", target)).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") .arg("--normal") - .arg("target/alefn") - .stdin(File::open("target/abn.diff").unwrap()) + .arg(&format!("{}/alefn", target)) + .stdin(File::open(&format!("{}/abn.diff", target)).unwrap()) .output() .unwrap(); if !output.status.success() { @@ -331,7 +333,7 @@ fn test_permutations_missing_line_ending() { } //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read("target/alefn").unwrap(); + let alef = fs::read(&format!("{}/alefn", target)).unwrap(); assert_eq!(alef, bet); } } @@ -344,8 +346,9 @@ fn test_permutations_missing_line_ending() { #[test] fn test_permutations_empty_lines() { + let target = "target/normal-diff/"; // test all possible six-line files with missing newlines. - let _ = std::fs::create_dir("target"); + let _ = std::fs::create_dir(target); for &a in &[0, 1, 2] { for &b in &[0, 1, 2] { for &c in &[0, 1, 2] { @@ -384,20 +387,20 @@ fn test_permutations_empty_lines() { // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff(&alef, &bet); - File::create("target/ab_.diff") + File::create(&format!("{}/ab_.diff", target)) .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create("target/alef_").unwrap(); + let mut fa = File::create(&format!("{}/alef_", target)).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create("target/bet_").unwrap(); + let mut fb = File::create(&format!("{}/bet_", target)).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") - .arg("target/alef_") - .stdin(File::open("target/ab_.diff").unwrap()) + .arg(&format!("{}/alef_", target)) + .stdin(File::open(&format!("{}/ab_.diff", target)).unwrap()) .output() .unwrap(); if !output.status.success() { @@ -405,7 +408,7 @@ fn test_permutations_empty_lines() { } //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read("target/alef_").unwrap(); + let alef = fs::read(&format!("{}/alef_", target)).unwrap(); assert_eq!(alef, bet); } } @@ -417,8 +420,9 @@ fn test_permutations_empty_lines() { #[test] fn test_permutations_reverse() { + let target = "target/normal-diff/"; // test all possible six-line files. - let _ = std::fs::create_dir("target"); + let _ = std::fs::create_dir(target); for &a in &[0, 1, 2] { for &b in &[0, 1, 2] { for &c in &[0, 1, 2] { @@ -463,20 +467,20 @@ fn test_permutations_reverse() { // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff(&alef, &bet); - File::create("target/abr.diff") + File::create(&format!("{}/abr.diff", target)) .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create("target/alefr").unwrap(); + let mut fa = File::create(&format!("{}/alefr", target)).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create("target/betr").unwrap(); + let mut fb = File::create(&format!("{}/betr", target)).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") - .arg("target/alefr") - .stdin(File::open("target/abr.diff").unwrap()) + .arg(&format!("{}/alefr", target)) + .stdin(File::open(&format!("{}/abr.diff", target)).unwrap()) .output() .unwrap(); if !output.status.success() { @@ -484,7 +488,7 @@ fn test_permutations_reverse() { } //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read("target/alefr").unwrap(); + let alef = fs::read(&format!("{}/alefr", target)).unwrap(); assert_eq!(alef, bet); } } diff --git a/src/unified_diff.rs b/src/unified_diff.rs index 9939134..b332824 100644 --- a/src/unified_diff.rs +++ b/src/unified_diff.rs @@ -384,8 +384,9 @@ pub fn diff( #[test] fn test_permutations() { + let target = "target/unified-diff/"; // test all possible six-line files. - let _ = std::fs::create_dir("target"); + let _ = std::fs::create_dir(target); for &a in &[0, 1, 2] { for &b in &[0, 1, 2] { for &c in &[0, 1, 2] { @@ -429,20 +430,20 @@ fn test_permutations() { } // This test diff is intentionally reversed. // We want it to turn the alef into bet. - let diff = diff(&alef, "a/alef", &bet, "target/alef", 2); - File::create("target/ab.diff") + let diff = diff(&alef, "a/alef", &bet, &format!("{}/alef", target), 2); + File::create(&format!("{}/ab.diff", target)) .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create("target/alef").unwrap(); + let mut fa = File::create(&format!("{}/alef", target)).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create("target/bet").unwrap(); + let mut fb = File::create(&format!("{}/bet", target)).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") - .stdin(File::open("target/ab.diff").unwrap()) + .stdin(File::open(&format!("{}/ab.diff", target)).unwrap()) .output() .unwrap(); if !output.status.success() { @@ -450,7 +451,7 @@ fn test_permutations() { } //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read("target/alef").unwrap(); + let alef = fs::read(&format!("{}/alef", target)).unwrap(); assert_eq!(alef, bet); } } @@ -462,8 +463,9 @@ fn test_permutations() { #[test] fn test_permutations_missing_line_ending() { + let target = "target/unified-diff/"; // test all possible six-line files with missing newlines. - let _ = std::fs::create_dir("target"); + let _ = std::fs::create_dir(target); for &a in &[0, 1, 2] { for &b in &[0, 1, 2] { for &c in &[0, 1, 2] { @@ -521,20 +523,21 @@ fn test_permutations_missing_line_ending() { } // This test diff is intentionally reversed. // We want it to turn the alef into bet. - let diff = diff(&alef, "a/alefn", &bet, "target/alefn", 2); - File::create("target/abn.diff") + let diff = + diff(&alef, "a/alefn", &bet, &format!("{}/alefn", target), 2); + File::create(&format!("{}/abn.diff", target)) .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create("target/alefn").unwrap(); + let mut fa = File::create(&format!("{}/alefn", target)).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create("target/betn").unwrap(); + let mut fb = File::create(&format!("{}/betn", target)).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") - .stdin(File::open("target/abn.diff").unwrap()) + .stdin(File::open(&format!("{}/abn.diff", target)).unwrap()) .output() .unwrap(); if !output.status.success() { @@ -542,7 +545,7 @@ fn test_permutations_missing_line_ending() { } //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read("target/alefn").unwrap(); + let alef = fs::read(&format!("{}/alefn", target)).unwrap(); assert_eq!(alef, bet); } } @@ -555,8 +558,9 @@ fn test_permutations_missing_line_ending() { #[test] fn test_permutations_empty_lines() { + let target = "target/unified-diff/"; // test all possible six-line files with missing newlines. - let _ = std::fs::create_dir("target"); + let _ = std::fs::create_dir(target); for &a in &[0, 1, 2] { for &b in &[0, 1, 2] { for &c in &[0, 1, 2] { @@ -609,20 +613,21 @@ fn test_permutations_empty_lines() { } // This test diff is intentionally reversed. // We want it to turn the alef into bet. - let diff = diff(&alef, "a/alef_", &bet, "target/alef_", 2); - File::create("target/ab_.diff") + let diff = + diff(&alef, "a/alef_", &bet, &format!("{}/alef_", target), 2); + File::create(&format!("{}/ab_.diff", target)) .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create("target/alef_").unwrap(); + let mut fa = File::create(&format!("{}/alef_", target)).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create("target/bet_").unwrap(); + let mut fb = File::create(&format!("{}/bet_", target)).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") - .stdin(File::open("target/ab_.diff").unwrap()) + .stdin(File::open(&format!("{}/ab_.diff", target)).unwrap()) .output() .unwrap(); if !output.status.success() { @@ -630,7 +635,7 @@ fn test_permutations_empty_lines() { } //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read("target/alef_").unwrap(); + let alef = fs::read(&format!("{}/alef_", target)).unwrap(); assert_eq!(alef, bet); } } @@ -643,8 +648,9 @@ fn test_permutations_empty_lines() { #[test] fn test_permutations_missing_lines() { + let target = "target/unified-diff/"; // test all possible six-line files. - let _ = std::fs::create_dir("target"); + let _ = std::fs::create_dir(target); for &a in &[0, 1, 2] { for &b in &[0, 1, 2] { for &c in &[0, 1, 2] { @@ -682,20 +688,21 @@ fn test_permutations_missing_lines() { } // This test diff is intentionally reversed. // We want it to turn the alef into bet. - let diff = diff(&alef, "a/alefx", &bet, "target/alefx", 2); - File::create("target/abx.diff") + let diff = + diff(&alef, "a/alefx", &bet, &format!("{}/alefx", target), 2); + File::create(&format!("{}/abx.diff", target)) .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create("target/alefx").unwrap(); + let mut fa = File::create(&format!("{}/alefx", target)).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create("target/betx").unwrap(); + let mut fb = File::create(&format!("{}/betx", target)).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") - .stdin(File::open("target/abx.diff").unwrap()) + .stdin(File::open(&format!("{}/abx.diff", target)).unwrap()) .output() .unwrap(); if !output.status.success() { @@ -703,7 +710,7 @@ fn test_permutations_missing_lines() { } //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read("target/alefx").unwrap(); + let alef = fs::read(&format!("{}/alefx", target)).unwrap(); assert_eq!(alef, bet); } } @@ -715,8 +722,9 @@ fn test_permutations_missing_lines() { #[test] fn test_permutations_reverse() { + let target = "target/unified-diff/"; // test all possible six-line files. - let _ = std::fs::create_dir("target"); + let _ = std::fs::create_dir(target); for &a in &[0, 1, 2] { for &b in &[0, 1, 2] { for &c in &[0, 1, 2] { @@ -760,20 +768,21 @@ fn test_permutations_reverse() { } // This test diff is intentionally reversed. // We want it to turn the alef into bet. - let diff = diff(&alef, "a/alefr", &bet, "target/alefr", 2); - File::create("target/abr.diff") + let diff = + diff(&alef, "a/alefr", &bet, &format!("{}/alefr", target), 2); + File::create(&format!("{}/abr.diff", target)) .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create("target/alefr").unwrap(); + let mut fa = File::create(&format!("{}/alefr", target)).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create("target/betr").unwrap(); + let mut fb = File::create(&format!("{}/betr", target)).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") - .stdin(File::open("target/abr.diff").unwrap()) + .stdin(File::open(&format!("{}/abr.diff", target)).unwrap()) .output() .unwrap(); if !output.status.success() { @@ -781,7 +790,7 @@ fn test_permutations_reverse() { } //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read("target/alefr").unwrap(); + let alef = fs::read(&format!("{}/alefr", target)).unwrap(); assert_eq!(alef, bet); } } From 6648963df105f173066aa4e6a2ccf318d5f0b939 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Tue, 23 Jan 2024 10:08:08 +0100 Subject: [PATCH 018/194] remove the unused function diff_w --- src/ed_diff.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/ed_diff.rs b/src/ed_diff.rs index 96cd44d..dd62950 100644 --- a/src/ed_diff.rs +++ b/src/ed_diff.rs @@ -146,12 +146,6 @@ pub fn diff(expected: &[u8], actual: &[u8]) -> Result, DiffError> { Ok(output) } -pub fn diff_w(expected: &[u8], actual: &[u8], filename: &str) -> Result, DiffError> { - let mut output = diff(expected, actual)?; - writeln!(&mut output, "w {}", filename).unwrap(); - Ok(output) -} - #[test] fn test_permutations() { let target = "target/ed-diff/"; From fdc35f6b8e175f4d31f8e2ce8257ddaeea21d116 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Tue, 23 Jan 2024 10:12:08 +0100 Subject: [PATCH 019/194] use mod tests + bring back the function used only for tests --- src/context_diff.rs | 568 +++++++++++++++++++++-------------------- src/ed_diff.rs | 425 ++++++++++++++++--------------- src/normal_diff.rs | 502 ++++++++++++++++++------------------ src/unified_diff.rs | 606 +++++++++++++++++++++++--------------------- 4 files changed, 1072 insertions(+), 1029 deletions(-) diff --git a/src/context_diff.rs b/src/context_diff.rs index 1526d4d..dbc54db 100644 --- a/src/context_diff.rs +++ b/src/context_diff.rs @@ -353,312 +353,318 @@ pub fn diff( output } -#[test] -fn test_permutations() { - // test all possible six-line files. - let target = "target/context-diff/"; - let _ = std::fs::create_dir(target); - for &a in &[0, 1, 2] { - for &b in &[0, 1, 2] { - for &c in &[0, 1, 2] { - for &d in &[0, 1, 2] { - for &e in &[0, 1, 2] { - for &f in &[0, 1, 2] { - use std::fs::{self, File}; - use std::io::Write; - use std::process::Command; - let mut alef = Vec::new(); - let mut bet = Vec::new(); - alef.write_all(if a == 0 { b"a\n" } else { b"b\n" }) - .unwrap(); - if a != 2 { - bet.write_all(b"b\n").unwrap(); - } - alef.write_all(if b == 0 { b"c\n" } else { b"d\n" }) - .unwrap(); - if b != 2 { - bet.write_all(b"d\n").unwrap(); - } - alef.write_all(if c == 0 { b"e\n" } else { b"f\n" }) - .unwrap(); - if c != 2 { - bet.write_all(b"f\n").unwrap(); - } - alef.write_all(if d == 0 { b"g\n" } else { b"h\n" }) - .unwrap(); - if d != 2 { - bet.write_all(b"h\n").unwrap(); - } - alef.write_all(if e == 0 { b"i\n" } else { b"j\n" }) - .unwrap(); - if e != 2 { - bet.write_all(b"j\n").unwrap(); - } - alef.write_all(if f == 0 { b"k\n" } else { b"l\n" }) - .unwrap(); - if f != 2 { - bet.write_all(b"l\n").unwrap(); - } - // This test diff is intentionally reversed. - // We want it to turn the alef into bet. - let diff = diff(&alef, "a/alef", &bet, &format!("{}/alef", target), 2); - File::create(&format!("{}/ab.diff", target)) - .unwrap() - .write_all(&diff) - .unwrap(); - let mut fa = File::create(&format!("{}/alef", target)).unwrap(); - fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{}/bet", target)).unwrap(); - fb.write_all(&bet[..]).unwrap(); - let _ = fa; - let _ = fb; - let output = Command::new("patch") - .arg("-p0") - .arg("--context") - .stdin(File::open(&format!("{}/ab.diff", target)).unwrap()) - .output() - .unwrap(); - if !output.status.success() { - panic!("{:?}", output); +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_permutations() { + // test all possible six-line files. + let target = "target/context-diff/"; + let _ = std::fs::create_dir(target); + for &a in &[0, 1, 2] { + for &b in &[0, 1, 2] { + for &c in &[0, 1, 2] { + for &d in &[0, 1, 2] { + for &e in &[0, 1, 2] { + for &f in &[0, 1, 2] { + use std::fs::{self, File}; + use std::io::Write; + use std::process::Command; + let mut alef = Vec::new(); + let mut bet = Vec::new(); + alef.write_all(if a == 0 { b"a\n" } else { b"b\n" }) + .unwrap(); + if a != 2 { + bet.write_all(b"b\n").unwrap(); + } + alef.write_all(if b == 0 { b"c\n" } else { b"d\n" }) + .unwrap(); + if b != 2 { + bet.write_all(b"d\n").unwrap(); + } + alef.write_all(if c == 0 { b"e\n" } else { b"f\n" }) + .unwrap(); + if c != 2 { + bet.write_all(b"f\n").unwrap(); + } + alef.write_all(if d == 0 { b"g\n" } else { b"h\n" }) + .unwrap(); + if d != 2 { + bet.write_all(b"h\n").unwrap(); + } + alef.write_all(if e == 0 { b"i\n" } else { b"j\n" }) + .unwrap(); + if e != 2 { + bet.write_all(b"j\n").unwrap(); + } + alef.write_all(if f == 0 { b"k\n" } else { b"l\n" }) + .unwrap(); + if f != 2 { + bet.write_all(b"l\n").unwrap(); + } + // This test diff is intentionally reversed. + // We want it to turn the alef into bet. + let diff = + diff(&alef, "a/alef", &bet, &format!("{}/alef", target), 2); + File::create(&format!("{}/ab.diff", target)) + .unwrap() + .write_all(&diff) + .unwrap(); + let mut fa = File::create(&format!("{}/alef", target)).unwrap(); + fa.write_all(&alef[..]).unwrap(); + let mut fb = File::create(&format!("{}/bet", target)).unwrap(); + fb.write_all(&bet[..]).unwrap(); + let _ = fa; + let _ = fb; + let output = Command::new("patch") + .arg("-p0") + .arg("--context") + .stdin(File::open(&format!("{}/ab.diff", target)).unwrap()) + .output() + .unwrap(); + if !output.status.success() { + panic!("{:?}", output); + } + //println!("{}", String::from_utf8_lossy(&output.stdout)); + //println!("{}", String::from_utf8_lossy(&output.stderr)); + let alef = fs::read(&format!("{}/alef", target)).unwrap(); + assert_eq!(alef, bet); } - //println!("{}", String::from_utf8_lossy(&output.stdout)); - //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read(&format!("{}/alef", target)).unwrap(); - assert_eq!(alef, bet); } } } } } } -} -#[test] -fn test_permutations_empty_lines() { - let target = "target/context-diff/"; - // test all possible six-line files with missing newlines. - let _ = std::fs::create_dir(target); - for &a in &[0, 1, 2] { - for &b in &[0, 1, 2] { - for &c in &[0, 1, 2] { - for &d in &[0, 1, 2] { - for &e in &[0, 1, 2] { - for &f in &[0, 1, 2] { - use std::fs::{self, File}; - use std::io::Write; - use std::process::Command; - let mut alef = Vec::new(); - let mut bet = Vec::new(); - alef.write_all(if a == 0 { b"\n" } else { b"b\n" }).unwrap(); - if a != 2 { - bet.write_all(b"b\n").unwrap(); - } - alef.write_all(if b == 0 { b"\n" } else { b"d\n" }).unwrap(); - if b != 2 { - bet.write_all(b"d\n").unwrap(); - } - alef.write_all(if c == 0 { b"\n" } else { b"f\n" }).unwrap(); - if c != 2 { - bet.write_all(b"f\n").unwrap(); - } - alef.write_all(if d == 0 { b"\n" } else { b"h\n" }).unwrap(); - if d != 2 { - bet.write_all(b"h\n").unwrap(); - } - alef.write_all(if e == 0 { b"\n" } else { b"j\n" }).unwrap(); - if e != 2 { - bet.write_all(b"j\n").unwrap(); - } - alef.write_all(if f == 0 { b"\n" } else { b"l\n" }).unwrap(); - if f != 2 { - bet.write_all(b"l\n").unwrap(); - } - // This test diff is intentionally reversed. - // We want it to turn the alef into bet. - let diff = - diff(&alef, "a/alef_", &bet, &format!("{}/alef_", target), 2); - File::create(&format!("{}/ab_.diff", target)) - .unwrap() - .write_all(&diff) - .unwrap(); - let mut fa = File::create(&format!("{}/alef_", target)).unwrap(); - fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{}/bet_", target)).unwrap(); - fb.write_all(&bet[..]).unwrap(); - let _ = fa; - let _ = fb; - let output = Command::new("patch") - .arg("-p0") - .arg("--context") - .stdin(File::open(&format!("{}/ab_.diff", target)).unwrap()) - .output() - .unwrap(); - if !output.status.success() { - panic!("{:?}", output); + #[test] + fn test_permutations_empty_lines() { + let target = "target/context-diff/"; + // test all possible six-line files with missing newlines. + let _ = std::fs::create_dir(target); + for &a in &[0, 1, 2] { + for &b in &[0, 1, 2] { + for &c in &[0, 1, 2] { + for &d in &[0, 1, 2] { + for &e in &[0, 1, 2] { + for &f in &[0, 1, 2] { + use std::fs::{self, File}; + use std::io::Write; + use std::process::Command; + let mut alef = Vec::new(); + let mut bet = Vec::new(); + alef.write_all(if a == 0 { b"\n" } else { b"b\n" }).unwrap(); + if a != 2 { + bet.write_all(b"b\n").unwrap(); + } + alef.write_all(if b == 0 { b"\n" } else { b"d\n" }).unwrap(); + if b != 2 { + bet.write_all(b"d\n").unwrap(); + } + alef.write_all(if c == 0 { b"\n" } else { b"f\n" }).unwrap(); + if c != 2 { + bet.write_all(b"f\n").unwrap(); + } + alef.write_all(if d == 0 { b"\n" } else { b"h\n" }).unwrap(); + if d != 2 { + bet.write_all(b"h\n").unwrap(); + } + alef.write_all(if e == 0 { b"\n" } else { b"j\n" }).unwrap(); + if e != 2 { + bet.write_all(b"j\n").unwrap(); + } + alef.write_all(if f == 0 { b"\n" } else { b"l\n" }).unwrap(); + if f != 2 { + bet.write_all(b"l\n").unwrap(); + } + // This test diff is intentionally reversed. + // We want it to turn the alef into bet. + let diff = + diff(&alef, "a/alef_", &bet, &format!("{}/alef_", target), 2); + File::create(&format!("{}/ab_.diff", target)) + .unwrap() + .write_all(&diff) + .unwrap(); + let mut fa = File::create(&format!("{}/alef_", target)).unwrap(); + fa.write_all(&alef[..]).unwrap(); + let mut fb = File::create(&format!("{}/bet_", target)).unwrap(); + fb.write_all(&bet[..]).unwrap(); + let _ = fa; + let _ = fb; + let output = Command::new("patch") + .arg("-p0") + .arg("--context") + .stdin(File::open(&format!("{}/ab_.diff", target)).unwrap()) + .output() + .unwrap(); + if !output.status.success() { + panic!("{:?}", output); + } + //println!("{}", String::from_utf8_lossy(&output.stdout)); + //println!("{}", String::from_utf8_lossy(&output.stderr)); + let alef = fs::read(&format!("{}/alef_", target)).unwrap(); + assert_eq!(alef, bet); } - //println!("{}", String::from_utf8_lossy(&output.stdout)); - //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read(&format!("{}/alef_", target)).unwrap(); - assert_eq!(alef, bet); } } } } } } -} -#[test] -fn test_permutations_missing_lines() { - let target = "target/context-diff/"; - // test all possible six-line files. - let _ = std::fs::create_dir(target); - for &a in &[0, 1, 2] { - for &b in &[0, 1, 2] { - for &c in &[0, 1, 2] { - for &d in &[0, 1, 2] { - for &e in &[0, 1, 2] { - for &f in &[0, 1, 2] { - use std::fs::{self, File}; - use std::io::Write; - use std::process::Command; - let mut alef = Vec::new(); - let mut bet = Vec::new(); - alef.write_all(if a == 0 { b"a\n" } else { b"" }).unwrap(); - if a != 2 { - bet.write_all(b"b\n").unwrap(); - } - alef.write_all(if b == 0 { b"c\n" } else { b"" }).unwrap(); - if b != 2 { - bet.write_all(b"d\n").unwrap(); - } - alef.write_all(if c == 0 { b"e\n" } else { b"" }).unwrap(); - if c != 2 { - bet.write_all(b"f\n").unwrap(); - } - alef.write_all(if d == 0 { b"g\n" } else { b"" }).unwrap(); - if d != 2 { - bet.write_all(b"h\n").unwrap(); - } - alef.write_all(if e == 0 { b"i\n" } else { b"" }).unwrap(); - if e != 2 { - bet.write_all(b"j\n").unwrap(); - } - alef.write_all(if f == 0 { b"k\n" } else { b"" }).unwrap(); - if f != 2 { - bet.write_all(b"l\n").unwrap(); - } - if alef.is_empty() && bet.is_empty() { - continue; - }; - // This test diff is intentionally reversed. - // We want it to turn the alef into bet. - let diff = - diff(&alef, "a/alefx", &bet, &format!("{}/alefx", target), 2); - File::create(&format!("{}/abx.diff", target)) - .unwrap() - .write_all(&diff) - .unwrap(); - let mut fa = File::create(&format!("{}/alefx", target)).unwrap(); - fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{}/betx", target)).unwrap(); - fb.write_all(&bet[..]).unwrap(); - let _ = fa; - let _ = fb; - let output = Command::new("patch") - .arg("-p0") - .arg("--context") - .stdin(File::open(&format!("{}/abx.diff", target)).unwrap()) - .output() - .unwrap(); - if !output.status.success() { - panic!("{:?}", output); + #[test] + fn test_permutations_missing_lines() { + let target = "target/context-diff/"; + // test all possible six-line files. + let _ = std::fs::create_dir(target); + for &a in &[0, 1, 2] { + for &b in &[0, 1, 2] { + for &c in &[0, 1, 2] { + for &d in &[0, 1, 2] { + for &e in &[0, 1, 2] { + for &f in &[0, 1, 2] { + use std::fs::{self, File}; + use std::io::Write; + use std::process::Command; + let mut alef = Vec::new(); + let mut bet = Vec::new(); + alef.write_all(if a == 0 { b"a\n" } else { b"" }).unwrap(); + if a != 2 { + bet.write_all(b"b\n").unwrap(); + } + alef.write_all(if b == 0 { b"c\n" } else { b"" }).unwrap(); + if b != 2 { + bet.write_all(b"d\n").unwrap(); + } + alef.write_all(if c == 0 { b"e\n" } else { b"" }).unwrap(); + if c != 2 { + bet.write_all(b"f\n").unwrap(); + } + alef.write_all(if d == 0 { b"g\n" } else { b"" }).unwrap(); + if d != 2 { + bet.write_all(b"h\n").unwrap(); + } + alef.write_all(if e == 0 { b"i\n" } else { b"" }).unwrap(); + if e != 2 { + bet.write_all(b"j\n").unwrap(); + } + alef.write_all(if f == 0 { b"k\n" } else { b"" }).unwrap(); + if f != 2 { + bet.write_all(b"l\n").unwrap(); + } + if alef.is_empty() && bet.is_empty() { + continue; + }; + // This test diff is intentionally reversed. + // We want it to turn the alef into bet. + let diff = + diff(&alef, "a/alefx", &bet, &format!("{}/alefx", target), 2); + File::create(&format!("{}/abx.diff", target)) + .unwrap() + .write_all(&diff) + .unwrap(); + let mut fa = File::create(&format!("{}/alefx", target)).unwrap(); + fa.write_all(&alef[..]).unwrap(); + let mut fb = File::create(&format!("{}/betx", target)).unwrap(); + fb.write_all(&bet[..]).unwrap(); + let _ = fa; + let _ = fb; + let output = Command::new("patch") + .arg("-p0") + .arg("--context") + .stdin(File::open(&format!("{}/abx.diff", target)).unwrap()) + .output() + .unwrap(); + if !output.status.success() { + panic!("{:?}", output); + } + //println!("{}", String::from_utf8_lossy(&output.stdout)); + //println!("{}", String::from_utf8_lossy(&output.stderr)); + let alef = fs::read(&format!("{}/alefx", target)).unwrap(); + assert_eq!(alef, bet); } - //println!("{}", String::from_utf8_lossy(&output.stdout)); - //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read(&format!("{}/alefx", target)).unwrap(); - assert_eq!(alef, bet); } } } } } } -} -#[test] -fn test_permutations_reverse() { - let target = "target/context-diff/"; - // test all possible six-line files. - let _ = std::fs::create_dir(target); - for &a in &[0, 1, 2] { - for &b in &[0, 1, 2] { - for &c in &[0, 1, 2] { - for &d in &[0, 1, 2] { - for &e in &[0, 1, 2] { - for &f in &[0, 1, 2] { - use std::fs::{self, File}; - use std::io::Write; - use std::process::Command; - let mut alef = Vec::new(); - let mut bet = Vec::new(); - alef.write_all(if a == 0 { b"a\n" } else { b"f\n" }) - .unwrap(); - if a != 2 { - bet.write_all(b"a\n").unwrap(); - } - alef.write_all(if b == 0 { b"b\n" } else { b"e\n" }) - .unwrap(); - if b != 2 { - bet.write_all(b"b\n").unwrap(); - } - alef.write_all(if c == 0 { b"c\n" } else { b"d\n" }) - .unwrap(); - if c != 2 { - bet.write_all(b"c\n").unwrap(); - } - alef.write_all(if d == 0 { b"d\n" } else { b"c\n" }) - .unwrap(); - if d != 2 { - bet.write_all(b"d\n").unwrap(); - } - alef.write_all(if e == 0 { b"e\n" } else { b"b\n" }) - .unwrap(); - if e != 2 { - bet.write_all(b"e\n").unwrap(); - } - alef.write_all(if f == 0 { b"f\n" } else { b"a\n" }) - .unwrap(); - if f != 2 { - bet.write_all(b"f\n").unwrap(); - } - // This test diff is intentionally reversed. - // We want it to turn the alef into bet. - let diff = - diff(&alef, "a/alefr", &bet, &format!("{}/alefr", target), 2); - File::create(&format!("{}/abr.diff", target)) - .unwrap() - .write_all(&diff) - .unwrap(); - let mut fa = File::create(&format!("{}/alefr", target)).unwrap(); - fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{}/betr", target)).unwrap(); - fb.write_all(&bet[..]).unwrap(); - let _ = fa; - let _ = fb; - let output = Command::new("patch") - .arg("-p0") - .arg("--context") - .stdin(File::open(&format!("{}/abr.diff", target)).unwrap()) - .output() - .unwrap(); - if !output.status.success() { - panic!("{:?}", output); + #[test] + fn test_permutations_reverse() { + let target = "target/context-diff/"; + // test all possible six-line files. + let _ = std::fs::create_dir(target); + for &a in &[0, 1, 2] { + for &b in &[0, 1, 2] { + for &c in &[0, 1, 2] { + for &d in &[0, 1, 2] { + for &e in &[0, 1, 2] { + for &f in &[0, 1, 2] { + use std::fs::{self, File}; + use std::io::Write; + use std::process::Command; + let mut alef = Vec::new(); + let mut bet = Vec::new(); + alef.write_all(if a == 0 { b"a\n" } else { b"f\n" }) + .unwrap(); + if a != 2 { + bet.write_all(b"a\n").unwrap(); + } + alef.write_all(if b == 0 { b"b\n" } else { b"e\n" }) + .unwrap(); + if b != 2 { + bet.write_all(b"b\n").unwrap(); + } + alef.write_all(if c == 0 { b"c\n" } else { b"d\n" }) + .unwrap(); + if c != 2 { + bet.write_all(b"c\n").unwrap(); + } + alef.write_all(if d == 0 { b"d\n" } else { b"c\n" }) + .unwrap(); + if d != 2 { + bet.write_all(b"d\n").unwrap(); + } + alef.write_all(if e == 0 { b"e\n" } else { b"b\n" }) + .unwrap(); + if e != 2 { + bet.write_all(b"e\n").unwrap(); + } + alef.write_all(if f == 0 { b"f\n" } else { b"a\n" }) + .unwrap(); + if f != 2 { + bet.write_all(b"f\n").unwrap(); + } + // This test diff is intentionally reversed. + // We want it to turn the alef into bet. + let diff = + diff(&alef, "a/alefr", &bet, &format!("{}/alefr", target), 2); + File::create(&format!("{}/abr.diff", target)) + .unwrap() + .write_all(&diff) + .unwrap(); + let mut fa = File::create(&format!("{}/alefr", target)).unwrap(); + fa.write_all(&alef[..]).unwrap(); + let mut fb = File::create(&format!("{}/betr", target)).unwrap(); + fb.write_all(&bet[..]).unwrap(); + let _ = fa; + let _ = fb; + let output = Command::new("patch") + .arg("-p0") + .arg("--context") + .stdin(File::open(&format!("{}/abr.diff", target)).unwrap()) + .output() + .unwrap(); + if !output.status.success() { + panic!("{:?}", output); + } + //println!("{}", String::from_utf8_lossy(&output.stdout)); + //println!("{}", String::from_utf8_lossy(&output.stderr)); + let alef = fs::read(&format!("{}/alefr", target)).unwrap(); + assert_eq!(alef, bet); } - //println!("{}", String::from_utf8_lossy(&output.stdout)); - //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read(&format!("{}/alefr", target)).unwrap(); - assert_eq!(alef, bet); } } } diff --git a/src/ed_diff.rs b/src/ed_diff.rs index dd62950..5bafef4 100644 --- a/src/ed_diff.rs +++ b/src/ed_diff.rs @@ -146,229 +146,242 @@ pub fn diff(expected: &[u8], actual: &[u8]) -> Result, DiffError> { Ok(output) } -#[test] -fn test_permutations() { - let target = "target/ed-diff/"; - // test all possible six-line files. - let _ = std::fs::create_dir(target); - for &a in &[0, 1, 2] { - for &b in &[0, 1, 2] { - for &c in &[0, 1, 2] { - for &d in &[0, 1, 2] { - for &e in &[0, 1, 2] { - for &f in &[0, 1, 2] { - use std::fs::{self, File}; - use std::io::Write; - use std::process::Command; - let mut alef = Vec::new(); - let mut bet = Vec::new(); - alef.write_all(if a == 0 { b"a\n" } else { b"b\n" }) - .unwrap(); - if a != 2 { - bet.write_all(b"b\n").unwrap(); - } - alef.write_all(if b == 0 { b"c\n" } else { b"d\n" }) - .unwrap(); - if b != 2 { - bet.write_all(b"d\n").unwrap(); - } - alef.write_all(if c == 0 { b"e\n" } else { b"f\n" }) - .unwrap(); - if c != 2 { - bet.write_all(b"f\n").unwrap(); - } - alef.write_all(if d == 0 { b"g\n" } else { b"h\n" }) - .unwrap(); - if d != 2 { - bet.write_all(b"h\n").unwrap(); - } - alef.write_all(if e == 0 { b"i\n" } else { b"j\n" }) - .unwrap(); - if e != 2 { - bet.write_all(b"j\n").unwrap(); - } - alef.write_all(if f == 0 { b"k\n" } else { b"l\n" }) - .unwrap(); - if f != 2 { - bet.write_all(b"l\n").unwrap(); - } - // This test diff is intentionally reversed. - // We want it to turn the alef into bet. - let diff = diff_w(&alef, &bet, &format!("{}/alef", target)).unwrap(); - File::create("target/ab.ed") - .unwrap() - .write_all(&diff) - .unwrap(); - let mut fa = File::create(&format!("{}/alef", target)).unwrap(); - fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{}/bet", target)).unwrap(); - fb.write_all(&bet[..]).unwrap(); - let _ = fa; - let _ = fb; - let output = Command::new("ed") - .arg(&format!("{}/alef", target)) - .stdin(File::open("target/ab.ed").unwrap()) - .output() - .unwrap(); - if !output.status.success() { - panic!("{:?}", output); +#[cfg(test)] +mod tests { + + use super::*; + pub fn diff_w(expected: &[u8], actual: &[u8], filename: &str) -> Result, DiffError> { + let mut output = diff(expected, actual)?; + writeln!(&mut output, "w {}", filename).unwrap(); + Ok(output) + } + + #[test] + fn test_permutations() { + let target = "target/ed-diff/"; + // test all possible six-line files. + let _ = std::fs::create_dir(target); + for &a in &[0, 1, 2] { + for &b in &[0, 1, 2] { + for &c in &[0, 1, 2] { + for &d in &[0, 1, 2] { + for &e in &[0, 1, 2] { + for &f in &[0, 1, 2] { + use std::fs::{self, File}; + use std::io::Write; + use std::process::Command; + let mut alef = Vec::new(); + let mut bet = Vec::new(); + alef.write_all(if a == 0 { b"a\n" } else { b"b\n" }) + .unwrap(); + if a != 2 { + bet.write_all(b"b\n").unwrap(); + } + alef.write_all(if b == 0 { b"c\n" } else { b"d\n" }) + .unwrap(); + if b != 2 { + bet.write_all(b"d\n").unwrap(); + } + alef.write_all(if c == 0 { b"e\n" } else { b"f\n" }) + .unwrap(); + if c != 2 { + bet.write_all(b"f\n").unwrap(); + } + alef.write_all(if d == 0 { b"g\n" } else { b"h\n" }) + .unwrap(); + if d != 2 { + bet.write_all(b"h\n").unwrap(); + } + alef.write_all(if e == 0 { b"i\n" } else { b"j\n" }) + .unwrap(); + if e != 2 { + bet.write_all(b"j\n").unwrap(); + } + alef.write_all(if f == 0 { b"k\n" } else { b"l\n" }) + .unwrap(); + if f != 2 { + bet.write_all(b"l\n").unwrap(); + } + // This test diff is intentionally reversed. + // We want it to turn the alef into bet. + let diff = + diff_w(&alef, &bet, &format!("{}/alef", target)).unwrap(); + File::create("target/ab.ed") + .unwrap() + .write_all(&diff) + .unwrap(); + let mut fa = File::create(&format!("{}/alef", target)).unwrap(); + fa.write_all(&alef[..]).unwrap(); + let mut fb = File::create(&format!("{}/bet", target)).unwrap(); + fb.write_all(&bet[..]).unwrap(); + let _ = fa; + let _ = fb; + let output = Command::new("ed") + .arg(&format!("{}/alef", target)) + .stdin(File::open("target/ab.ed").unwrap()) + .output() + .unwrap(); + if !output.status.success() { + panic!("{:?}", output); + } + //println!("{}", String::from_utf8_lossy(&output.stdout)); + //println!("{}", String::from_utf8_lossy(&output.stderr)); + let alef = fs::read(&format!("{}/alef", target)).unwrap(); + assert_eq!(alef, bet); } - //println!("{}", String::from_utf8_lossy(&output.stdout)); - //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read(&format!("{}/alef", target)).unwrap(); - assert_eq!(alef, bet); } } } } } } -} -#[test] -fn test_permutations_empty_lines() { - let target = "target/ed-diff/"; - // test all possible six-line files with missing newlines. - let _ = std::fs::create_dir(target); - for &a in &[0, 1, 2] { - for &b in &[0, 1, 2] { - for &c in &[0, 1, 2] { - for &d in &[0, 1, 2] { - for &e in &[0, 1, 2] { - for &f in &[0, 1, 2] { - use std::fs::{self, File}; - use std::io::Write; - use std::process::Command; - let mut alef = Vec::new(); - let mut bet = Vec::new(); - alef.write_all(if a == 0 { b"\n" } else { b"b\n" }).unwrap(); - if a != 2 { - bet.write_all(b"b\n").unwrap(); - } - alef.write_all(if b == 0 { b"\n" } else { b"d\n" }).unwrap(); - if b != 2 { - bet.write_all(b"d\n").unwrap(); - } - alef.write_all(if c == 0 { b"\n" } else { b"f\n" }).unwrap(); - if c != 2 { - bet.write_all(b"f\n").unwrap(); + #[test] + fn test_permutations_empty_lines() { + let target = "target/ed-diff/"; + // test all possible six-line files with missing newlines. + let _ = std::fs::create_dir(target); + for &a in &[0, 1, 2] { + for &b in &[0, 1, 2] { + for &c in &[0, 1, 2] { + for &d in &[0, 1, 2] { + for &e in &[0, 1, 2] { + for &f in &[0, 1, 2] { + use std::fs::{self, File}; + use std::io::Write; + use std::process::Command; + let mut alef = Vec::new(); + let mut bet = Vec::new(); + alef.write_all(if a == 0 { b"\n" } else { b"b\n" }).unwrap(); + if a != 2 { + bet.write_all(b"b\n").unwrap(); + } + alef.write_all(if b == 0 { b"\n" } else { b"d\n" }).unwrap(); + if b != 2 { + bet.write_all(b"d\n").unwrap(); + } + alef.write_all(if c == 0 { b"\n" } else { b"f\n" }).unwrap(); + if c != 2 { + bet.write_all(b"f\n").unwrap(); + } + alef.write_all(if d == 0 { b"\n" } else { b"h\n" }).unwrap(); + if d != 2 { + bet.write_all(b"h\n").unwrap(); + } + alef.write_all(if e == 0 { b"\n" } else { b"j\n" }).unwrap(); + if e != 2 { + bet.write_all(b"j\n").unwrap(); + } + alef.write_all(if f == 0 { b"\n" } else { b"l\n" }).unwrap(); + if f != 2 { + bet.write_all(b"l\n").unwrap(); + } + // This test diff is intentionally reversed. + // We want it to turn the alef into bet. + let diff = diff_w(&alef, &bet, "target/alef_").unwrap(); + File::create("target/ab_.ed") + .unwrap() + .write_all(&diff) + .unwrap(); + let mut fa = File::create("target/alef_").unwrap(); + fa.write_all(&alef[..]).unwrap(); + let mut fb = File::create(&format!("{}/bet_", target)).unwrap(); + fb.write_all(&bet[..]).unwrap(); + let _ = fa; + let _ = fb; + let output = Command::new("ed") + .arg("target/alef_") + .stdin(File::open("target/ab_.ed").unwrap()) + .output() + .unwrap(); + if !output.status.success() { + panic!("{:?}", output); + } + //println!("{}", String::from_utf8_lossy(&output.stdout)); + //println!("{}", String::from_utf8_lossy(&output.stderr)); + let alef = fs::read("target/alef_").unwrap(); + assert_eq!(alef, bet); } - alef.write_all(if d == 0 { b"\n" } else { b"h\n" }).unwrap(); - if d != 2 { - bet.write_all(b"h\n").unwrap(); - } - alef.write_all(if e == 0 { b"\n" } else { b"j\n" }).unwrap(); - if e != 2 { - bet.write_all(b"j\n").unwrap(); - } - alef.write_all(if f == 0 { b"\n" } else { b"l\n" }).unwrap(); - if f != 2 { - bet.write_all(b"l\n").unwrap(); - } - // This test diff is intentionally reversed. - // We want it to turn the alef into bet. - let diff = diff_w(&alef, &bet, "target/alef_").unwrap(); - File::create("target/ab_.ed") - .unwrap() - .write_all(&diff) - .unwrap(); - let mut fa = File::create("target/alef_").unwrap(); - fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{}/bet_", target)).unwrap(); - fb.write_all(&bet[..]).unwrap(); - let _ = fa; - let _ = fb; - let output = Command::new("ed") - .arg("target/alef_") - .stdin(File::open("target/ab_.ed").unwrap()) - .output() - .unwrap(); - if !output.status.success() { - panic!("{:?}", output); - } - //println!("{}", String::from_utf8_lossy(&output.stdout)); - //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read("target/alef_").unwrap(); - assert_eq!(alef, bet); } } } } } } -} -#[test] -fn test_permutations_reverse() { - let target = "target/ed-diff/"; - // test all possible six-line files. - let _ = std::fs::create_dir(target); - for &a in &[0, 1, 2] { - for &b in &[0, 1, 2] { - for &c in &[0, 1, 2] { - for &d in &[0, 1, 2] { - for &e in &[0, 1, 2] { - for &f in &[0, 1, 2] { - use std::fs::{self, File}; - use std::io::Write; - use std::process::Command; - let mut alef = Vec::new(); - let mut bet = Vec::new(); - alef.write_all(if a == 0 { b"a\n" } else { b"f\n" }) - .unwrap(); - if a != 2 { - bet.write_all(b"a\n").unwrap(); - } - alef.write_all(if b == 0 { b"b\n" } else { b"e\n" }) - .unwrap(); - if b != 2 { - bet.write_all(b"b\n").unwrap(); - } - alef.write_all(if c == 0 { b"c\n" } else { b"d\n" }) - .unwrap(); - if c != 2 { - bet.write_all(b"c\n").unwrap(); - } - alef.write_all(if d == 0 { b"d\n" } else { b"c\n" }) - .unwrap(); - if d != 2 { - bet.write_all(b"d\n").unwrap(); - } - alef.write_all(if e == 0 { b"e\n" } else { b"b\n" }) - .unwrap(); - if e != 2 { - bet.write_all(b"e\n").unwrap(); - } - alef.write_all(if f == 0 { b"f\n" } else { b"a\n" }) - .unwrap(); - if f != 2 { - bet.write_all(b"f\n").unwrap(); - } - // This test diff is intentionally reversed. - // We want it to turn the alef into bet. - let diff = diff_w(&alef, &bet, &format!("{}/alefr", target)).unwrap(); - File::create("target/abr.ed") - .unwrap() - .write_all(&diff) - .unwrap(); - let mut fa = File::create(&format!("{}/alefr", target)).unwrap(); - fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{}/betr", target)).unwrap(); - fb.write_all(&bet[..]).unwrap(); - let _ = fa; - let _ = fb; - let output = Command::new("ed") - .arg(&format!("{}/alefr", target)) - .stdin(File::open("target/abr.ed").unwrap()) - .output() - .unwrap(); - if !output.status.success() { - panic!("{:?}", output); + #[test] + fn test_permutations_reverse() { + let target = "target/ed-diff/"; + // test all possible six-line files. + let _ = std::fs::create_dir(target); + for &a in &[0, 1, 2] { + for &b in &[0, 1, 2] { + for &c in &[0, 1, 2] { + for &d in &[0, 1, 2] { + for &e in &[0, 1, 2] { + for &f in &[0, 1, 2] { + use std::fs::{self, File}; + use std::io::Write; + use std::process::Command; + let mut alef = Vec::new(); + let mut bet = Vec::new(); + alef.write_all(if a == 0 { b"a\n" } else { b"f\n" }) + .unwrap(); + if a != 2 { + bet.write_all(b"a\n").unwrap(); + } + alef.write_all(if b == 0 { b"b\n" } else { b"e\n" }) + .unwrap(); + if b != 2 { + bet.write_all(b"b\n").unwrap(); + } + alef.write_all(if c == 0 { b"c\n" } else { b"d\n" }) + .unwrap(); + if c != 2 { + bet.write_all(b"c\n").unwrap(); + } + alef.write_all(if d == 0 { b"d\n" } else { b"c\n" }) + .unwrap(); + if d != 2 { + bet.write_all(b"d\n").unwrap(); + } + alef.write_all(if e == 0 { b"e\n" } else { b"b\n" }) + .unwrap(); + if e != 2 { + bet.write_all(b"e\n").unwrap(); + } + alef.write_all(if f == 0 { b"f\n" } else { b"a\n" }) + .unwrap(); + if f != 2 { + bet.write_all(b"f\n").unwrap(); + } + // This test diff is intentionally reversed. + // We want it to turn the alef into bet. + let diff = + diff_w(&alef, &bet, &format!("{}/alefr", target)).unwrap(); + File::create("target/abr.ed") + .unwrap() + .write_all(&diff) + .unwrap(); + let mut fa = File::create(&format!("{}/alefr", target)).unwrap(); + fa.write_all(&alef[..]).unwrap(); + let mut fb = File::create(&format!("{}/betr", target)).unwrap(); + fb.write_all(&bet[..]).unwrap(); + let _ = fa; + let _ = fb; + let output = Command::new("ed") + .arg(&format!("{}/alefr", target)) + .stdin(File::open("target/abr.ed").unwrap()) + .output() + .unwrap(); + if !output.status.success() { + panic!("{:?}", output); + } + //println!("{}", String::from_utf8_lossy(&output.stdout)); + //println!("{}", String::from_utf8_lossy(&output.stderr)); + let alef = fs::read(&format!("{}/alefr", target)).unwrap(); + assert_eq!(alef, bet); } - //println!("{}", String::from_utf8_lossy(&output.stdout)); - //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read(&format!("{}/alefr", target)).unwrap(); - assert_eq!(alef, bet); } } } diff --git a/src/normal_diff.rs b/src/normal_diff.rs index 02334f2..0955dce 100644 --- a/src/normal_diff.rs +++ b/src/normal_diff.rs @@ -168,98 +168,21 @@ pub fn diff(expected: &[u8], actual: &[u8]) -> Vec { output } -#[test] -fn test_permutations() { - let target = "target/normal-diff/"; - // test all possible six-line files. - let _ = std::fs::create_dir(target); - for &a in &[0, 1, 2] { - for &b in &[0, 1, 2] { - for &c in &[0, 1, 2] { - for &d in &[0, 1, 2] { - for &e in &[0, 1, 2] { - for &f in &[0, 1, 2] { - use std::fs::{self, File}; - use std::io::Write; - use std::process::Command; - let mut alef = Vec::new(); - let mut bet = Vec::new(); - alef.write_all(if a == 0 { b"a\n" } else { b"b\n" }) - .unwrap(); - if a != 2 { - bet.write_all(b"b\n").unwrap(); - } - alef.write_all(if b == 0 { b"c\n" } else { b"d\n" }) - .unwrap(); - if b != 2 { - bet.write_all(b"d\n").unwrap(); - } - alef.write_all(if c == 0 { b"e\n" } else { b"f\n" }) - .unwrap(); - if c != 2 { - bet.write_all(b"f\n").unwrap(); - } - alef.write_all(if d == 0 { b"g\n" } else { b"h\n" }) - .unwrap(); - if d != 2 { - bet.write_all(b"h\n").unwrap(); - } - alef.write_all(if e == 0 { b"i\n" } else { b"j\n" }) - .unwrap(); - if e != 2 { - bet.write_all(b"j\n").unwrap(); - } - alef.write_all(if f == 0 { b"k\n" } else { b"l\n" }) - .unwrap(); - if f != 2 { - bet.write_all(b"l\n").unwrap(); - } - // This test diff is intentionally reversed. - // We want it to turn the alef into bet. - let diff = diff(&alef, &bet); - File::create(&format!("{}/ab.diff", target)) - .unwrap() - .write_all(&diff) - .unwrap(); - let mut fa = File::create(&format!("{}/alef", target)).unwrap(); - fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{}/bet", target)).unwrap(); - fb.write_all(&bet[..]).unwrap(); - let _ = fa; - let _ = fb; - let output = Command::new("patch") - .arg("-p0") - .arg(&format!("{}/alef", target)) - .stdin(File::open(&format!("{}/ab.diff", target)).unwrap()) - .output() - .unwrap(); - if !output.status.success() { - panic!("{:?}", output); - } - //println!("{}", String::from_utf8_lossy(&output.stdout)); - //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read(&format!("{}/alef", target)).unwrap(); - assert_eq!(alef, bet); - } - } - } - } - } - } -} +#[cfg(test)] +mod tests { + use super::*; -#[test] -fn test_permutations_missing_line_ending() { - let target = "target/normal-diff/"; - // test all possible six-line files with missing newlines. - let _ = std::fs::create_dir(target); - for &a in &[0, 1, 2] { - for &b in &[0, 1, 2] { - for &c in &[0, 1, 2] { - for &d in &[0, 1, 2] { - for &e in &[0, 1, 2] { - for &f in &[0, 1, 2] { - for &g in &[0, 1, 2] { + #[test] + fn test_permutations() { + let target = "target/normal-diff/"; + // test all possible six-line files. + let _ = std::fs::create_dir(target); + for &a in &[0, 1, 2] { + for &b in &[0, 1, 2] { + for &c in &[0, 1, 2] { + for &d in &[0, 1, 2] { + for &e in &[0, 1, 2] { + for &f in &[0, 1, 2] { use std::fs::{self, File}; use std::io::Write; use std::process::Command; @@ -295,37 +218,23 @@ fn test_permutations_missing_line_ending() { if f != 2 { bet.write_all(b"l\n").unwrap(); } - match g { - 0 => { - alef.pop(); - } - 1 => { - bet.pop(); - } - 2 => { - alef.pop(); - bet.pop(); - } - _ => unreachable!(), - } // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff(&alef, &bet); - File::create(&format!("{}/abn.diff", target)) + File::create(&format!("{}/ab.diff", target)) .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create(&format!("{}/alefn", target)).unwrap(); + let mut fa = File::create(&format!("{}/alef", target)).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{}/betn", target)).unwrap(); + let mut fb = File::create(&format!("{}/bet", target)).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") - .arg("--normal") - .arg(&format!("{}/alefn", target)) - .stdin(File::open(&format!("{}/abn.diff", target)).unwrap()) + .arg(&format!("{}/alef", target)) + .stdin(File::open(&format!("{}/ab.diff", target)).unwrap()) .output() .unwrap(); if !output.status.success() { @@ -333,7 +242,7 @@ fn test_permutations_missing_line_ending() { } //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read(&format!("{}/alefn", target)).unwrap(); + let alef = fs::read(&format!("{}/alef", target)).unwrap(); assert_eq!(alef, bet); } } @@ -342,154 +251,251 @@ fn test_permutations_missing_line_ending() { } } } -} -#[test] -fn test_permutations_empty_lines() { - let target = "target/normal-diff/"; - // test all possible six-line files with missing newlines. - let _ = std::fs::create_dir(target); - for &a in &[0, 1, 2] { - for &b in &[0, 1, 2] { - for &c in &[0, 1, 2] { - for &d in &[0, 1, 2] { - for &e in &[0, 1, 2] { - for &f in &[0, 1, 2] { - use std::fs::{self, File}; - use std::io::Write; - use std::process::Command; - let mut alef = Vec::new(); - let mut bet = Vec::new(); - alef.write_all(if a == 0 { b"\n" } else { b"b\n" }).unwrap(); - if a != 2 { - bet.write_all(b"b\n").unwrap(); - } - alef.write_all(if b == 0 { b"\n" } else { b"d\n" }).unwrap(); - if b != 2 { - bet.write_all(b"d\n").unwrap(); - } - alef.write_all(if c == 0 { b"\n" } else { b"f\n" }).unwrap(); - if c != 2 { - bet.write_all(b"f\n").unwrap(); - } - alef.write_all(if d == 0 { b"\n" } else { b"h\n" }).unwrap(); - if d != 2 { - bet.write_all(b"h\n").unwrap(); - } - alef.write_all(if e == 0 { b"\n" } else { b"j\n" }).unwrap(); - if e != 2 { - bet.write_all(b"j\n").unwrap(); - } - alef.write_all(if f == 0 { b"\n" } else { b"l\n" }).unwrap(); - if f != 2 { - bet.write_all(b"l\n").unwrap(); - } - // This test diff is intentionally reversed. - // We want it to turn the alef into bet. - let diff = diff(&alef, &bet); - File::create(&format!("{}/ab_.diff", target)) - .unwrap() - .write_all(&diff) - .unwrap(); - let mut fa = File::create(&format!("{}/alef_", target)).unwrap(); - fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{}/bet_", target)).unwrap(); - fb.write_all(&bet[..]).unwrap(); - let _ = fa; - let _ = fb; - let output = Command::new("patch") - .arg("-p0") - .arg(&format!("{}/alef_", target)) - .stdin(File::open(&format!("{}/ab_.diff", target)).unwrap()) - .output() - .unwrap(); - if !output.status.success() { - panic!("{:?}", output); + #[test] + fn test_permutations_missing_line_ending() { + let target = "target/normal-diff/"; + // test all possible six-line files with missing newlines. + let _ = std::fs::create_dir(target); + for &a in &[0, 1, 2] { + for &b in &[0, 1, 2] { + for &c in &[0, 1, 2] { + for &d in &[0, 1, 2] { + for &e in &[0, 1, 2] { + for &f in &[0, 1, 2] { + for &g in &[0, 1, 2] { + use std::fs::{self, File}; + use std::io::Write; + use std::process::Command; + let mut alef = Vec::new(); + let mut bet = Vec::new(); + alef.write_all(if a == 0 { b"a\n" } else { b"b\n" }) + .unwrap(); + if a != 2 { + bet.write_all(b"b\n").unwrap(); + } + alef.write_all(if b == 0 { b"c\n" } else { b"d\n" }) + .unwrap(); + if b != 2 { + bet.write_all(b"d\n").unwrap(); + } + alef.write_all(if c == 0 { b"e\n" } else { b"f\n" }) + .unwrap(); + if c != 2 { + bet.write_all(b"f\n").unwrap(); + } + alef.write_all(if d == 0 { b"g\n" } else { b"h\n" }) + .unwrap(); + if d != 2 { + bet.write_all(b"h\n").unwrap(); + } + alef.write_all(if e == 0 { b"i\n" } else { b"j\n" }) + .unwrap(); + if e != 2 { + bet.write_all(b"j\n").unwrap(); + } + alef.write_all(if f == 0 { b"k\n" } else { b"l\n" }) + .unwrap(); + if f != 2 { + bet.write_all(b"l\n").unwrap(); + } + match g { + 0 => { + alef.pop(); + } + 1 => { + bet.pop(); + } + 2 => { + alef.pop(); + bet.pop(); + } + _ => unreachable!(), + } + // This test diff is intentionally reversed. + // We want it to turn the alef into bet. + let diff = diff(&alef, &bet); + File::create(&format!("{}/abn.diff", target)) + .unwrap() + .write_all(&diff) + .unwrap(); + let mut fa = + File::create(&format!("{}/alefn", target)).unwrap(); + fa.write_all(&alef[..]).unwrap(); + let mut fb = File::create(&format!("{}/betn", target)).unwrap(); + fb.write_all(&bet[..]).unwrap(); + let _ = fa; + let _ = fb; + let output = Command::new("patch") + .arg("-p0") + .arg("--normal") + .arg(&format!("{}/alefn", target)) + .stdin(File::open(&format!("{}/abn.diff", target)).unwrap()) + .output() + .unwrap(); + if !output.status.success() { + panic!("{:?}", output); + } + //println!("{}", String::from_utf8_lossy(&output.stdout)); + //println!("{}", String::from_utf8_lossy(&output.stderr)); + let alef = fs::read(&format!("{}/alefn", target)).unwrap(); + assert_eq!(alef, bet); + } } - //println!("{}", String::from_utf8_lossy(&output.stdout)); - //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read(&format!("{}/alef_", target)).unwrap(); - assert_eq!(alef, bet); } } } } } } -} -#[test] -fn test_permutations_reverse() { - let target = "target/normal-diff/"; - // test all possible six-line files. - let _ = std::fs::create_dir(target); - for &a in &[0, 1, 2] { - for &b in &[0, 1, 2] { - for &c in &[0, 1, 2] { - for &d in &[0, 1, 2] { - for &e in &[0, 1, 2] { - for &f in &[0, 1, 2] { - use std::fs::{self, File}; - use std::io::Write; - use std::process::Command; - let mut alef = Vec::new(); - let mut bet = Vec::new(); - alef.write_all(if a == 0 { b"a\n" } else { b"f\n" }) - .unwrap(); - if a != 2 { - bet.write_all(b"a\n").unwrap(); - } - alef.write_all(if b == 0 { b"b\n" } else { b"e\n" }) - .unwrap(); - if b != 2 { - bet.write_all(b"b\n").unwrap(); - } - alef.write_all(if c == 0 { b"c\n" } else { b"d\n" }) - .unwrap(); - if c != 2 { - bet.write_all(b"c\n").unwrap(); - } - alef.write_all(if d == 0 { b"d\n" } else { b"c\n" }) - .unwrap(); - if d != 2 { - bet.write_all(b"d\n").unwrap(); - } - alef.write_all(if e == 0 { b"e\n" } else { b"b\n" }) - .unwrap(); - if e != 2 { - bet.write_all(b"e\n").unwrap(); - } - alef.write_all(if f == 0 { b"f\n" } else { b"a\n" }) - .unwrap(); - if f != 2 { - bet.write_all(b"f\n").unwrap(); + #[test] + fn test_permutations_empty_lines() { + let target = "target/normal-diff/"; + // test all possible six-line files with missing newlines. + let _ = std::fs::create_dir(target); + for &a in &[0, 1, 2] { + for &b in &[0, 1, 2] { + for &c in &[0, 1, 2] { + for &d in &[0, 1, 2] { + for &e in &[0, 1, 2] { + for &f in &[0, 1, 2] { + use std::fs::{self, File}; + use std::io::Write; + use std::process::Command; + let mut alef = Vec::new(); + let mut bet = Vec::new(); + alef.write_all(if a == 0 { b"\n" } else { b"b\n" }).unwrap(); + if a != 2 { + bet.write_all(b"b\n").unwrap(); + } + alef.write_all(if b == 0 { b"\n" } else { b"d\n" }).unwrap(); + if b != 2 { + bet.write_all(b"d\n").unwrap(); + } + alef.write_all(if c == 0 { b"\n" } else { b"f\n" }).unwrap(); + if c != 2 { + bet.write_all(b"f\n").unwrap(); + } + alef.write_all(if d == 0 { b"\n" } else { b"h\n" }).unwrap(); + if d != 2 { + bet.write_all(b"h\n").unwrap(); + } + alef.write_all(if e == 0 { b"\n" } else { b"j\n" }).unwrap(); + if e != 2 { + bet.write_all(b"j\n").unwrap(); + } + alef.write_all(if f == 0 { b"\n" } else { b"l\n" }).unwrap(); + if f != 2 { + bet.write_all(b"l\n").unwrap(); + } + // This test diff is intentionally reversed. + // We want it to turn the alef into bet. + let diff = diff(&alef, &bet); + File::create(&format!("{}/ab_.diff", target)) + .unwrap() + .write_all(&diff) + .unwrap(); + let mut fa = File::create(&format!("{}/alef_", target)).unwrap(); + fa.write_all(&alef[..]).unwrap(); + let mut fb = File::create(&format!("{}/bet_", target)).unwrap(); + fb.write_all(&bet[..]).unwrap(); + let _ = fa; + let _ = fb; + let output = Command::new("patch") + .arg("-p0") + .arg(&format!("{}/alef_", target)) + .stdin(File::open(&format!("{}/ab_.diff", target)).unwrap()) + .output() + .unwrap(); + if !output.status.success() { + panic!("{:?}", output); + } + //println!("{}", String::from_utf8_lossy(&output.stdout)); + //println!("{}", String::from_utf8_lossy(&output.stderr)); + let alef = fs::read(&format!("{}/alef_", target)).unwrap(); + assert_eq!(alef, bet); } - // This test diff is intentionally reversed. - // We want it to turn the alef into bet. - let diff = diff(&alef, &bet); - File::create(&format!("{}/abr.diff", target)) - .unwrap() - .write_all(&diff) - .unwrap(); - let mut fa = File::create(&format!("{}/alefr", target)).unwrap(); - fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{}/betr", target)).unwrap(); - fb.write_all(&bet[..]).unwrap(); - let _ = fa; - let _ = fb; - let output = Command::new("patch") - .arg("-p0") - .arg(&format!("{}/alefr", target)) - .stdin(File::open(&format!("{}/abr.diff", target)).unwrap()) - .output() - .unwrap(); - if !output.status.success() { - panic!("{:?}", output); + } + } + } + } + } + } + + #[test] + fn test_permutations_reverse() { + let target = "target/normal-diff/"; + // test all possible six-line files. + let _ = std::fs::create_dir(target); + for &a in &[0, 1, 2] { + for &b in &[0, 1, 2] { + for &c in &[0, 1, 2] { + for &d in &[0, 1, 2] { + for &e in &[0, 1, 2] { + for &f in &[0, 1, 2] { + use std::fs::{self, File}; + use std::io::Write; + use std::process::Command; + let mut alef = Vec::new(); + let mut bet = Vec::new(); + alef.write_all(if a == 0 { b"a\n" } else { b"f\n" }) + .unwrap(); + if a != 2 { + bet.write_all(b"a\n").unwrap(); + } + alef.write_all(if b == 0 { b"b\n" } else { b"e\n" }) + .unwrap(); + if b != 2 { + bet.write_all(b"b\n").unwrap(); + } + alef.write_all(if c == 0 { b"c\n" } else { b"d\n" }) + .unwrap(); + if c != 2 { + bet.write_all(b"c\n").unwrap(); + } + alef.write_all(if d == 0 { b"d\n" } else { b"c\n" }) + .unwrap(); + if d != 2 { + bet.write_all(b"d\n").unwrap(); + } + alef.write_all(if e == 0 { b"e\n" } else { b"b\n" }) + .unwrap(); + if e != 2 { + bet.write_all(b"e\n").unwrap(); + } + alef.write_all(if f == 0 { b"f\n" } else { b"a\n" }) + .unwrap(); + if f != 2 { + bet.write_all(b"f\n").unwrap(); + } + // This test diff is intentionally reversed. + // We want it to turn the alef into bet. + let diff = diff(&alef, &bet); + File::create(&format!("{}/abr.diff", target)) + .unwrap() + .write_all(&diff) + .unwrap(); + let mut fa = File::create(&format!("{}/alefr", target)).unwrap(); + fa.write_all(&alef[..]).unwrap(); + let mut fb = File::create(&format!("{}/betr", target)).unwrap(); + fb.write_all(&bet[..]).unwrap(); + let _ = fa; + let _ = fb; + let output = Command::new("patch") + .arg("-p0") + .arg(&format!("{}/alefr", target)) + .stdin(File::open(&format!("{}/abr.diff", target)).unwrap()) + .output() + .unwrap(); + if !output.status.success() { + panic!("{:?}", output); + } + //println!("{}", String::from_utf8_lossy(&output.stdout)); + //println!("{}", String::from_utf8_lossy(&output.stderr)); + let alef = fs::read(&format!("{}/alefr", target)).unwrap(); + assert_eq!(alef, bet); } - //println!("{}", String::from_utf8_lossy(&output.stdout)); - //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read(&format!("{}/alefr", target)).unwrap(); - assert_eq!(alef, bet); } } } diff --git a/src/unified_diff.rs b/src/unified_diff.rs index b332824..05074e5 100644 --- a/src/unified_diff.rs +++ b/src/unified_diff.rs @@ -382,97 +382,21 @@ pub fn diff( output } -#[test] -fn test_permutations() { - let target = "target/unified-diff/"; - // test all possible six-line files. - let _ = std::fs::create_dir(target); - for &a in &[0, 1, 2] { - for &b in &[0, 1, 2] { - for &c in &[0, 1, 2] { - for &d in &[0, 1, 2] { - for &e in &[0, 1, 2] { - for &f in &[0, 1, 2] { - use std::fs::{self, File}; - use std::io::Write; - use std::process::Command; - let mut alef = Vec::new(); - let mut bet = Vec::new(); - alef.write_all(if a == 0 { b"a\n" } else { b"b\n" }) - .unwrap(); - if a != 2 { - bet.write_all(b"b\n").unwrap(); - } - alef.write_all(if b == 0 { b"c\n" } else { b"d\n" }) - .unwrap(); - if b != 2 { - bet.write_all(b"d\n").unwrap(); - } - alef.write_all(if c == 0 { b"e\n" } else { b"f\n" }) - .unwrap(); - if c != 2 { - bet.write_all(b"f\n").unwrap(); - } - alef.write_all(if d == 0 { b"g\n" } else { b"h\n" }) - .unwrap(); - if d != 2 { - bet.write_all(b"h\n").unwrap(); - } - alef.write_all(if e == 0 { b"i\n" } else { b"j\n" }) - .unwrap(); - if e != 2 { - bet.write_all(b"j\n").unwrap(); - } - alef.write_all(if f == 0 { b"k\n" } else { b"l\n" }) - .unwrap(); - if f != 2 { - bet.write_all(b"l\n").unwrap(); - } - // This test diff is intentionally reversed. - // We want it to turn the alef into bet. - let diff = diff(&alef, "a/alef", &bet, &format!("{}/alef", target), 2); - File::create(&format!("{}/ab.diff", target)) - .unwrap() - .write_all(&diff) - .unwrap(); - let mut fa = File::create(&format!("{}/alef", target)).unwrap(); - fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{}/bet", target)).unwrap(); - fb.write_all(&bet[..]).unwrap(); - let _ = fa; - let _ = fb; - let output = Command::new("patch") - .arg("-p0") - .stdin(File::open(&format!("{}/ab.diff", target)).unwrap()) - .output() - .unwrap(); - if !output.status.success() { - panic!("{:?}", output); - } - //println!("{}", String::from_utf8_lossy(&output.stdout)); - //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read(&format!("{}/alef", target)).unwrap(); - assert_eq!(alef, bet); - } - } - } - } - } - } -} +#[cfg(test)] +mod tests { + use super::*; -#[test] -fn test_permutations_missing_line_ending() { - let target = "target/unified-diff/"; - // test all possible six-line files with missing newlines. - let _ = std::fs::create_dir(target); - for &a in &[0, 1, 2] { - for &b in &[0, 1, 2] { - for &c in &[0, 1, 2] { - for &d in &[0, 1, 2] { - for &e in &[0, 1, 2] { - for &f in &[0, 1, 2] { - for &g in &[0, 1, 2] { + #[test] + fn test_permutations() { + let target = "target/unified-diff/"; + // test all possible six-line files. + let _ = std::fs::create_dir(target); + for &a in &[0, 1, 2] { + for &b in &[0, 1, 2] { + for &c in &[0, 1, 2] { + for &d in &[0, 1, 2] { + for &e in &[0, 1, 2] { + for &f in &[0, 1, 2] { use std::fs::{self, File}; use std::io::Write; use std::process::Command; @@ -508,36 +432,23 @@ fn test_permutations_missing_line_ending() { if f != 2 { bet.write_all(b"l\n").unwrap(); } - match g { - 0 => { - alef.pop(); - } - 1 => { - bet.pop(); - } - 2 => { - alef.pop(); - bet.pop(); - } - _ => unreachable!(), - } // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = - diff(&alef, "a/alefn", &bet, &format!("{}/alefn", target), 2); - File::create(&format!("{}/abn.diff", target)) + diff(&alef, "a/alef", &bet, &format!("{}/alef", target), 2); + File::create(&format!("{}/ab.diff", target)) .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create(&format!("{}/alefn", target)).unwrap(); + let mut fa = File::create(&format!("{}/alef", target)).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{}/betn", target)).unwrap(); + let mut fb = File::create(&format!("{}/bet", target)).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") - .stdin(File::open(&format!("{}/abn.diff", target)).unwrap()) + .stdin(File::open(&format!("{}/ab.diff", target)).unwrap()) .output() .unwrap(); if !output.status.success() { @@ -545,7 +456,7 @@ fn test_permutations_missing_line_ending() { } //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read(&format!("{}/alefn", target)).unwrap(); + let alef = fs::read(&format!("{}/alef", target)).unwrap(); assert_eq!(alef, bet); } } @@ -554,80 +465,261 @@ fn test_permutations_missing_line_ending() { } } } -} -#[test] -fn test_permutations_empty_lines() { - let target = "target/unified-diff/"; - // test all possible six-line files with missing newlines. - let _ = std::fs::create_dir(target); - for &a in &[0, 1, 2] { - for &b in &[0, 1, 2] { - for &c in &[0, 1, 2] { - for &d in &[0, 1, 2] { - for &e in &[0, 1, 2] { - for &f in &[0, 1, 2] { - for &g in &[0, 1, 2, 3] { + #[test] + fn test_permutations_missing_line_ending() { + let target = "target/unified-diff/"; + // test all possible six-line files with missing newlines. + let _ = std::fs::create_dir(target); + for &a in &[0, 1, 2] { + for &b in &[0, 1, 2] { + for &c in &[0, 1, 2] { + for &d in &[0, 1, 2] { + for &e in &[0, 1, 2] { + for &f in &[0, 1, 2] { + for &g in &[0, 1, 2] { + use std::fs::{self, File}; + use std::io::Write; + use std::process::Command; + let mut alef = Vec::new(); + let mut bet = Vec::new(); + alef.write_all(if a == 0 { b"a\n" } else { b"b\n" }) + .unwrap(); + if a != 2 { + bet.write_all(b"b\n").unwrap(); + } + alef.write_all(if b == 0 { b"c\n" } else { b"d\n" }) + .unwrap(); + if b != 2 { + bet.write_all(b"d\n").unwrap(); + } + alef.write_all(if c == 0 { b"e\n" } else { b"f\n" }) + .unwrap(); + if c != 2 { + bet.write_all(b"f\n").unwrap(); + } + alef.write_all(if d == 0 { b"g\n" } else { b"h\n" }) + .unwrap(); + if d != 2 { + bet.write_all(b"h\n").unwrap(); + } + alef.write_all(if e == 0 { b"i\n" } else { b"j\n" }) + .unwrap(); + if e != 2 { + bet.write_all(b"j\n").unwrap(); + } + alef.write_all(if f == 0 { b"k\n" } else { b"l\n" }) + .unwrap(); + if f != 2 { + bet.write_all(b"l\n").unwrap(); + } + match g { + 0 => { + alef.pop(); + } + 1 => { + bet.pop(); + } + 2 => { + alef.pop(); + bet.pop(); + } + _ => unreachable!(), + } + // This test diff is intentionally reversed. + // We want it to turn the alef into bet. + let diff = diff( + &alef, + "a/alefn", + &bet, + &format!("{}/alefn", target), + 2, + ); + File::create(&format!("{}/abn.diff", target)) + .unwrap() + .write_all(&diff) + .unwrap(); + let mut fa = + File::create(&format!("{}/alefn", target)).unwrap(); + fa.write_all(&alef[..]).unwrap(); + let mut fb = File::create(&format!("{}/betn", target)).unwrap(); + fb.write_all(&bet[..]).unwrap(); + let _ = fa; + let _ = fb; + let output = Command::new("patch") + .arg("-p0") + .stdin(File::open(&format!("{}/abn.diff", target)).unwrap()) + .output() + .unwrap(); + if !output.status.success() { + panic!("{:?}", output); + } + //println!("{}", String::from_utf8_lossy(&output.stdout)); + //println!("{}", String::from_utf8_lossy(&output.stderr)); + let alef = fs::read(&format!("{}/alefn", target)).unwrap(); + assert_eq!(alef, bet); + } + } + } + } + } + } + } + } + + #[test] + fn test_permutations_empty_lines() { + let target = "target/unified-diff/"; + // test all possible six-line files with missing newlines. + let _ = std::fs::create_dir(target); + for &a in &[0, 1, 2] { + for &b in &[0, 1, 2] { + for &c in &[0, 1, 2] { + for &d in &[0, 1, 2] { + for &e in &[0, 1, 2] { + for &f in &[0, 1, 2] { + for &g in &[0, 1, 2, 3] { + use std::fs::{self, File}; + use std::io::Write; + use std::process::Command; + let mut alef = Vec::new(); + let mut bet = Vec::new(); + alef.write_all(if a == 0 { b"\n" } else { b"b\n" }).unwrap(); + if a != 2 { + bet.write_all(b"b\n").unwrap(); + } + alef.write_all(if b == 0 { b"\n" } else { b"d\n" }).unwrap(); + if b != 2 { + bet.write_all(b"d\n").unwrap(); + } + alef.write_all(if c == 0 { b"\n" } else { b"f\n" }).unwrap(); + if c != 2 { + bet.write_all(b"f\n").unwrap(); + } + alef.write_all(if d == 0 { b"\n" } else { b"h\n" }).unwrap(); + if d != 2 { + bet.write_all(b"h\n").unwrap(); + } + alef.write_all(if e == 0 { b"\n" } else { b"j\n" }).unwrap(); + if e != 2 { + bet.write_all(b"j\n").unwrap(); + } + alef.write_all(if f == 0 { b"\n" } else { b"l\n" }).unwrap(); + if f != 2 { + bet.write_all(b"l\n").unwrap(); + } + match g { + 0 => { + alef.pop(); + } + 1 => { + bet.pop(); + } + 2 => { + alef.pop(); + bet.pop(); + } + 3 => {} + _ => unreachable!(), + } + // This test diff is intentionally reversed. + // We want it to turn the alef into bet. + let diff = diff( + &alef, + "a/alef_", + &bet, + &format!("{}/alef_", target), + 2, + ); + File::create(&format!("{}/ab_.diff", target)) + .unwrap() + .write_all(&diff) + .unwrap(); + let mut fa = + File::create(&format!("{}/alef_", target)).unwrap(); + fa.write_all(&alef[..]).unwrap(); + let mut fb = File::create(&format!("{}/bet_", target)).unwrap(); + fb.write_all(&bet[..]).unwrap(); + let _ = fa; + let _ = fb; + let output = Command::new("patch") + .arg("-p0") + .stdin(File::open(&format!("{}/ab_.diff", target)).unwrap()) + .output() + .unwrap(); + if !output.status.success() { + panic!("{:?}", output); + } + //println!("{}", String::from_utf8_lossy(&output.stdout)); + //println!("{}", String::from_utf8_lossy(&output.stderr)); + let alef = fs::read(&format!("{}/alef_", target)).unwrap(); + assert_eq!(alef, bet); + } + } + } + } + } + } + } + } + + #[test] + fn test_permutations_missing_lines() { + let target = "target/unified-diff/"; + // test all possible six-line files. + let _ = std::fs::create_dir(target); + for &a in &[0, 1, 2] { + for &b in &[0, 1, 2] { + for &c in &[0, 1, 2] { + for &d in &[0, 1, 2] { + for &e in &[0, 1, 2] { + for &f in &[0, 1, 2] { use std::fs::{self, File}; use std::io::Write; use std::process::Command; let mut alef = Vec::new(); let mut bet = Vec::new(); - alef.write_all(if a == 0 { b"\n" } else { b"b\n" }).unwrap(); + alef.write_all(if a == 0 { b"a\n" } else { b"" }).unwrap(); if a != 2 { bet.write_all(b"b\n").unwrap(); } - alef.write_all(if b == 0 { b"\n" } else { b"d\n" }).unwrap(); + alef.write_all(if b == 0 { b"c\n" } else { b"" }).unwrap(); if b != 2 { bet.write_all(b"d\n").unwrap(); } - alef.write_all(if c == 0 { b"\n" } else { b"f\n" }).unwrap(); + alef.write_all(if c == 0 { b"e\n" } else { b"" }).unwrap(); if c != 2 { bet.write_all(b"f\n").unwrap(); } - alef.write_all(if d == 0 { b"\n" } else { b"h\n" }).unwrap(); + alef.write_all(if d == 0 { b"g\n" } else { b"" }).unwrap(); if d != 2 { bet.write_all(b"h\n").unwrap(); } - alef.write_all(if e == 0 { b"\n" } else { b"j\n" }).unwrap(); + alef.write_all(if e == 0 { b"i\n" } else { b"" }).unwrap(); if e != 2 { bet.write_all(b"j\n").unwrap(); } - alef.write_all(if f == 0 { b"\n" } else { b"l\n" }).unwrap(); + alef.write_all(if f == 0 { b"k\n" } else { b"" }).unwrap(); if f != 2 { bet.write_all(b"l\n").unwrap(); } - match g { - 0 => { - alef.pop(); - } - 1 => { - bet.pop(); - } - 2 => { - alef.pop(); - bet.pop(); - } - 3 => {} - _ => unreachable!(), - } // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = - diff(&alef, "a/alef_", &bet, &format!("{}/alef_", target), 2); - File::create(&format!("{}/ab_.diff", target)) + diff(&alef, "a/alefx", &bet, &format!("{}/alefx", target), 2); + File::create(&format!("{}/abx.diff", target)) .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create(&format!("{}/alef_", target)).unwrap(); + let mut fa = File::create(&format!("{}/alefx", target)).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{}/bet_", target)).unwrap(); + let mut fb = File::create(&format!("{}/betx", target)).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") - .stdin(File::open(&format!("{}/ab_.diff", target)).unwrap()) + .stdin(File::open(&format!("{}/abx.diff", target)).unwrap()) .output() .unwrap(); if !output.status.success() { @@ -635,7 +727,7 @@ fn test_permutations_empty_lines() { } //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read(&format!("{}/alef_", target)).unwrap(); + let alef = fs::read(&format!("{}/alefx", target)).unwrap(); assert_eq!(alef, bet); } } @@ -644,154 +736,80 @@ fn test_permutations_empty_lines() { } } } -} -#[test] -fn test_permutations_missing_lines() { - let target = "target/unified-diff/"; - // test all possible six-line files. - let _ = std::fs::create_dir(target); - for &a in &[0, 1, 2] { - for &b in &[0, 1, 2] { - for &c in &[0, 1, 2] { - for &d in &[0, 1, 2] { - for &e in &[0, 1, 2] { - for &f in &[0, 1, 2] { - use std::fs::{self, File}; - use std::io::Write; - use std::process::Command; - let mut alef = Vec::new(); - let mut bet = Vec::new(); - alef.write_all(if a == 0 { b"a\n" } else { b"" }).unwrap(); - if a != 2 { - bet.write_all(b"b\n").unwrap(); - } - alef.write_all(if b == 0 { b"c\n" } else { b"" }).unwrap(); - if b != 2 { - bet.write_all(b"d\n").unwrap(); - } - alef.write_all(if c == 0 { b"e\n" } else { b"" }).unwrap(); - if c != 2 { - bet.write_all(b"f\n").unwrap(); - } - alef.write_all(if d == 0 { b"g\n" } else { b"" }).unwrap(); - if d != 2 { - bet.write_all(b"h\n").unwrap(); - } - alef.write_all(if e == 0 { b"i\n" } else { b"" }).unwrap(); - if e != 2 { - bet.write_all(b"j\n").unwrap(); - } - alef.write_all(if f == 0 { b"k\n" } else { b"" }).unwrap(); - if f != 2 { - bet.write_all(b"l\n").unwrap(); - } - // This test diff is intentionally reversed. - // We want it to turn the alef into bet. - let diff = - diff(&alef, "a/alefx", &bet, &format!("{}/alefx", target), 2); - File::create(&format!("{}/abx.diff", target)) - .unwrap() - .write_all(&diff) - .unwrap(); - let mut fa = File::create(&format!("{}/alefx", target)).unwrap(); - fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{}/betx", target)).unwrap(); - fb.write_all(&bet[..]).unwrap(); - let _ = fa; - let _ = fb; - let output = Command::new("patch") - .arg("-p0") - .stdin(File::open(&format!("{}/abx.diff", target)).unwrap()) - .output() - .unwrap(); - if !output.status.success() { - panic!("{:?}", output); - } - //println!("{}", String::from_utf8_lossy(&output.stdout)); - //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read(&format!("{}/alefx", target)).unwrap(); - assert_eq!(alef, bet); - } - } - } - } - } - } -} - -#[test] -fn test_permutations_reverse() { - let target = "target/unified-diff/"; - // test all possible six-line files. - let _ = std::fs::create_dir(target); - for &a in &[0, 1, 2] { - for &b in &[0, 1, 2] { - for &c in &[0, 1, 2] { - for &d in &[0, 1, 2] { - for &e in &[0, 1, 2] { - for &f in &[0, 1, 2] { - use std::fs::{self, File}; - use std::io::Write; - use std::process::Command; - let mut alef = Vec::new(); - let mut bet = Vec::new(); - alef.write_all(if a == 0 { b"a\n" } else { b"f\n" }) - .unwrap(); - if a != 2 { - bet.write_all(b"a\n").unwrap(); - } - alef.write_all(if b == 0 { b"b\n" } else { b"e\n" }) - .unwrap(); - if b != 2 { - bet.write_all(b"b\n").unwrap(); - } - alef.write_all(if c == 0 { b"c\n" } else { b"d\n" }) - .unwrap(); - if c != 2 { - bet.write_all(b"c\n").unwrap(); - } - alef.write_all(if d == 0 { b"d\n" } else { b"c\n" }) - .unwrap(); - if d != 2 { - bet.write_all(b"d\n").unwrap(); - } - alef.write_all(if e == 0 { b"e\n" } else { b"b\n" }) - .unwrap(); - if e != 2 { - bet.write_all(b"e\n").unwrap(); - } - alef.write_all(if f == 0 { b"f\n" } else { b"a\n" }) - .unwrap(); - if f != 2 { - bet.write_all(b"f\n").unwrap(); - } - // This test diff is intentionally reversed. - // We want it to turn the alef into bet. - let diff = - diff(&alef, "a/alefr", &bet, &format!("{}/alefr", target), 2); - File::create(&format!("{}/abr.diff", target)) - .unwrap() - .write_all(&diff) - .unwrap(); - let mut fa = File::create(&format!("{}/alefr", target)).unwrap(); - fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{}/betr", target)).unwrap(); - fb.write_all(&bet[..]).unwrap(); - let _ = fa; - let _ = fb; - let output = Command::new("patch") - .arg("-p0") - .stdin(File::open(&format!("{}/abr.diff", target)).unwrap()) - .output() - .unwrap(); - if !output.status.success() { - panic!("{:?}", output); + #[test] + fn test_permutations_reverse() { + let target = "target/unified-diff/"; + // test all possible six-line files. + let _ = std::fs::create_dir(target); + for &a in &[0, 1, 2] { + for &b in &[0, 1, 2] { + for &c in &[0, 1, 2] { + for &d in &[0, 1, 2] { + for &e in &[0, 1, 2] { + for &f in &[0, 1, 2] { + use std::fs::{self, File}; + use std::io::Write; + use std::process::Command; + let mut alef = Vec::new(); + let mut bet = Vec::new(); + alef.write_all(if a == 0 { b"a\n" } else { b"f\n" }) + .unwrap(); + if a != 2 { + bet.write_all(b"a\n").unwrap(); + } + alef.write_all(if b == 0 { b"b\n" } else { b"e\n" }) + .unwrap(); + if b != 2 { + bet.write_all(b"b\n").unwrap(); + } + alef.write_all(if c == 0 { b"c\n" } else { b"d\n" }) + .unwrap(); + if c != 2 { + bet.write_all(b"c\n").unwrap(); + } + alef.write_all(if d == 0 { b"d\n" } else { b"c\n" }) + .unwrap(); + if d != 2 { + bet.write_all(b"d\n").unwrap(); + } + alef.write_all(if e == 0 { b"e\n" } else { b"b\n" }) + .unwrap(); + if e != 2 { + bet.write_all(b"e\n").unwrap(); + } + alef.write_all(if f == 0 { b"f\n" } else { b"a\n" }) + .unwrap(); + if f != 2 { + bet.write_all(b"f\n").unwrap(); + } + // This test diff is intentionally reversed. + // We want it to turn the alef into bet. + let diff = + diff(&alef, "a/alefr", &bet, &format!("{}/alefr", target), 2); + File::create(&format!("{}/abr.diff", target)) + .unwrap() + .write_all(&diff) + .unwrap(); + let mut fa = File::create(&format!("{}/alefr", target)).unwrap(); + fa.write_all(&alef[..]).unwrap(); + let mut fb = File::create(&format!("{}/betr", target)).unwrap(); + fb.write_all(&bet[..]).unwrap(); + let _ = fa; + let _ = fb; + let output = Command::new("patch") + .arg("-p0") + .stdin(File::open(&format!("{}/abr.diff", target)).unwrap()) + .output() + .unwrap(); + if !output.status.success() { + panic!("{:?}", output); + } + //println!("{}", String::from_utf8_lossy(&output.stdout)); + //println!("{}", String::from_utf8_lossy(&output.stderr)); + let alef = fs::read(&format!("{}/alefr", target)).unwrap(); + assert_eq!(alef, bet); } - //println!("{}", String::from_utf8_lossy(&output.stdout)); - //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read(&format!("{}/alefr", target)).unwrap(); - assert_eq!(alef, bet); } } } From d891e1034d86fcfd22254891d1ec41887fb90aa4 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Tue, 23 Jan 2024 11:42:12 +0100 Subject: [PATCH 020/194] run clippy pedantic $ cargo +nightly clippy --allow-dirty --fix -- -W clippy::pedantic --- src/context_diff.rs | 77 +++++++++++++---------------- src/ed_diff.rs | 38 ++++++-------- src/main.rs | 6 +-- src/normal_diff.rs | 66 +++++++++++-------------- src/params.rs | 13 ++--- src/unified_diff.rs | 117 ++++++++++++++++++-------------------------- 6 files changed, 133 insertions(+), 184 deletions(-) diff --git a/src/context_diff.rs b/src/context_diff.rs index dbc54db..b650985 100644 --- a/src/context_diff.rs +++ b/src/context_diff.rs @@ -253,6 +253,7 @@ fn make_diff(expected: &[u8], actual: &[u8], context_size: usize) -> Vec Vec { - let mut output = - format!("*** {}\t\n--- {}\t\n", expected_filename, actual_filename).into_bytes(); + let mut output = format!("*** {expected_filename}\t\n--- {actual_filename}\t\n").into_bytes(); let diff_results = make_diff(expected, actual, context_size); if diff_results.is_empty() { return Vec::new(); @@ -284,17 +284,16 @@ pub fn diff( let exp_start = if end_line_number_expected == line_number_expected { String::new() } else { - format!("{},", line_number_expected) + format!("{line_number_expected},") }; let act_start = if end_line_number_actual == line_number_actual { String::new() } else { - format!("{},", line_number_actual) + format!("{line_number_actual},") }; writeln!( output, - "***************\n*** {}{} ****", - exp_start, end_line_number_expected + "***************\n*** {exp_start}{end_line_number_expected} ****" ) .expect("write to Vec is infallible"); if !result.expected_all_context { @@ -322,7 +321,7 @@ pub fn diff( .expect("write to Vec is infallible"); } } - writeln!(output, "--- {}{} ----", act_start, end_line_number_actual) + writeln!(output, "--- {act_start}{end_line_number_actual} ----") .expect("write to Vec is infallible"); if !result.actual_all_context { for line in result.actual { @@ -406,29 +405,27 @@ mod tests { // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = - diff(&alef, "a/alef", &bet, &format!("{}/alef", target), 2); - File::create(&format!("{}/ab.diff", target)) + diff(&alef, "a/alef", &bet, &format!("{target}/alef"), 2); + File::create(&format!("{target}/ab.diff")) .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create(&format!("{}/alef", target)).unwrap(); + let mut fa = File::create(&format!("{target}/alef")).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{}/bet", target)).unwrap(); + let mut fb = File::create(&format!("{target}/bet")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") .arg("--context") - .stdin(File::open(&format!("{}/ab.diff", target)).unwrap()) + .stdin(File::open(&format!("{target}/ab.diff")).unwrap()) .output() .unwrap(); - if !output.status.success() { - panic!("{:?}", output); - } + assert!(output.status.success(), "{:?}", output); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read(&format!("{}/alef", target)).unwrap(); + let alef = fs::read(&format!("{target}/alef")).unwrap(); assert_eq!(alef, bet); } } @@ -481,29 +478,27 @@ mod tests { // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = - diff(&alef, "a/alef_", &bet, &format!("{}/alef_", target), 2); - File::create(&format!("{}/ab_.diff", target)) + diff(&alef, "a/alef_", &bet, &format!("{target}/alef_"), 2); + File::create(&format!("{target}/ab_.diff")) .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create(&format!("{}/alef_", target)).unwrap(); + let mut fa = File::create(&format!("{target}/alef_")).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{}/bet_", target)).unwrap(); + let mut fb = File::create(&format!("{target}/bet_")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") .arg("--context") - .stdin(File::open(&format!("{}/ab_.diff", target)).unwrap()) + .stdin(File::open(&format!("{target}/ab_.diff")).unwrap()) .output() .unwrap(); - if !output.status.success() { - panic!("{:?}", output); - } + assert!(output.status.success(), "{:?}", output); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read(&format!("{}/alef_", target)).unwrap(); + let alef = fs::read(&format!("{target}/alef_")).unwrap(); assert_eq!(alef, bet); } } @@ -559,29 +554,27 @@ mod tests { // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = - diff(&alef, "a/alefx", &bet, &format!("{}/alefx", target), 2); - File::create(&format!("{}/abx.diff", target)) + diff(&alef, "a/alefx", &bet, &format!("{target}/alefx"), 2); + File::create(&format!("{target}/abx.diff")) .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create(&format!("{}/alefx", target)).unwrap(); + let mut fa = File::create(&format!("{target}/alefx")).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{}/betx", target)).unwrap(); + let mut fb = File::create(&format!("{target}/betx")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") .arg("--context") - .stdin(File::open(&format!("{}/abx.diff", target)).unwrap()) + .stdin(File::open(&format!("{target}/abx.diff")).unwrap()) .output() .unwrap(); - if !output.status.success() { - panic!("{:?}", output); - } + assert!(output.status.success(), "{:?}", output); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read(&format!("{}/alefx", target)).unwrap(); + let alef = fs::read(&format!("{target}/alefx")).unwrap(); assert_eq!(alef, bet); } } @@ -640,29 +633,27 @@ mod tests { // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = - diff(&alef, "a/alefr", &bet, &format!("{}/alefr", target), 2); - File::create(&format!("{}/abr.diff", target)) + diff(&alef, "a/alefr", &bet, &format!("{target}/alefr"), 2); + File::create(&format!("{target}/abr.diff")) .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create(&format!("{}/alefr", target)).unwrap(); + let mut fa = File::create(&format!("{target}/alefr")).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{}/betr", target)).unwrap(); + let mut fb = File::create(&format!("{target}/betr")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") .arg("--context") - .stdin(File::open(&format!("{}/abr.diff", target)).unwrap()) + .stdin(File::open(&format!("{target}/abr.diff")).unwrap()) .output() .unwrap(); - if !output.status.success() { - panic!("{:?}", output); - } + assert!(output.status.success(), "{:?}", output); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read(&format!("{}/alefr", target)).unwrap(); + let alef = fs::read(&format!("{target}/alefr")).unwrap(); assert_eq!(alef, bet); } } diff --git a/src/ed_diff.rs b/src/ed_diff.rs index 5bafef4..3e674d7 100644 --- a/src/ed_diff.rs +++ b/src/ed_diff.rs @@ -152,7 +152,7 @@ mod tests { use super::*; pub fn diff_w(expected: &[u8], actual: &[u8], filename: &str) -> Result, DiffError> { let mut output = diff(expected, actual)?; - writeln!(&mut output, "w {}", filename).unwrap(); + writeln!(&mut output, "w {filename}").unwrap(); Ok(output) } @@ -204,29 +204,26 @@ mod tests { } // This test diff is intentionally reversed. // We want it to turn the alef into bet. - let diff = - diff_w(&alef, &bet, &format!("{}/alef", target)).unwrap(); + let diff = diff_w(&alef, &bet, &format!("{target}/alef")).unwrap(); File::create("target/ab.ed") .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create(&format!("{}/alef", target)).unwrap(); + let mut fa = File::create(&format!("{target}/alef")).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{}/bet", target)).unwrap(); + let mut fb = File::create(&format!("{target}/bet")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("ed") - .arg(&format!("{}/alef", target)) + .arg(&format!("{target}/alef")) .stdin(File::open("target/ab.ed").unwrap()) .output() .unwrap(); - if !output.status.success() { - panic!("{:?}", output); - } + assert!(output.status.success(), "{:?}", output); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read(&format!("{}/alef", target)).unwrap(); + let alef = fs::read(&format!("{target}/alef")).unwrap(); assert_eq!(alef, bet); } } @@ -285,7 +282,7 @@ mod tests { .unwrap(); let mut fa = File::create("target/alef_").unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{}/bet_", target)).unwrap(); + let mut fb = File::create(&format!("{target}/bet_")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; @@ -294,9 +291,7 @@ mod tests { .stdin(File::open("target/ab_.ed").unwrap()) .output() .unwrap(); - if !output.status.success() { - panic!("{:?}", output); - } + assert!(output.status.success(), "{:?}", output); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); let alef = fs::read("target/alef_").unwrap(); @@ -357,29 +352,26 @@ mod tests { } // This test diff is intentionally reversed. // We want it to turn the alef into bet. - let diff = - diff_w(&alef, &bet, &format!("{}/alefr", target)).unwrap(); + let diff = diff_w(&alef, &bet, &format!("{target}/alefr")).unwrap(); File::create("target/abr.ed") .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create(&format!("{}/alefr", target)).unwrap(); + let mut fa = File::create(&format!("{target}/alefr")).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{}/betr", target)).unwrap(); + let mut fb = File::create(&format!("{target}/betr")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("ed") - .arg(&format!("{}/alefr", target)) + .arg(&format!("{target}/alefr")) .stdin(File::open("target/abr.ed").unwrap()) .output() .unwrap(); - if !output.status.success() { - panic!("{:?}", output); - } + assert!(output.status.success(), "{:?}", output); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read(&format!("{}/alefr", target)).unwrap(); + let alef = fs::read(&format!("{target}/alefr")).unwrap(); assert_eq!(alef, bet); } } diff --git a/src/main.rs b/src/main.rs index ad0555e..eaa7d55 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ // For the full copyright and license information, please view the LICENSE-* // files that was distributed with this source code. -use crate::params::*; +use crate::params::{parse_params, Format, Params}; use std::env; use std::fs; @@ -27,13 +27,13 @@ fn main() -> Result<(), String> { let from_content = match fs::read(&from) { Ok(from_content) => from_content, Err(e) => { - return Err(format!("Failed to read from-file: {}", e)); + return Err(format!("Failed to read from-file: {e}")); } }; let to_content = match fs::read(&to) { Ok(to_content) => to_content, Err(e) => { - return Err(format!("Failed to read from-file: {}", e)); + return Err(format!("Failed to read from-file: {e}")); } }; // run diff diff --git a/src/normal_diff.rs b/src/normal_diff.rs index 0955dce..c3297a1 100644 --- a/src/normal_diff.rs +++ b/src/normal_diff.rs @@ -109,6 +109,7 @@ fn make_diff(expected: &[u8], actual: &[u8]) -> Vec { results } +#[must_use] pub fn diff(expected: &[u8], actual: &[u8]) -> Vec { let mut output = Vec::new(); let diff_results = make_diff(expected, actual); @@ -221,28 +222,26 @@ mod tests { // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff(&alef, &bet); - File::create(&format!("{}/ab.diff", target)) + File::create(&format!("{target}/ab.diff")) .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create(&format!("{}/alef", target)).unwrap(); + let mut fa = File::create(&format!("{target}/alef")).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{}/bet", target)).unwrap(); + let mut fb = File::create(&format!("{target}/bet")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") - .arg(&format!("{}/alef", target)) - .stdin(File::open(&format!("{}/ab.diff", target)).unwrap()) + .arg(&format!("{target}/alef")) + .stdin(File::open(&format!("{target}/ab.diff")).unwrap()) .output() .unwrap(); - if !output.status.success() { - panic!("{:?}", output); - } + assert!(output.status.success(), "{:?}", output); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read(&format!("{}/alef", target)).unwrap(); + let alef = fs::read(&format!("{target}/alef")).unwrap(); assert_eq!(alef, bet); } } @@ -315,30 +314,27 @@ mod tests { // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff(&alef, &bet); - File::create(&format!("{}/abn.diff", target)) + File::create(&format!("{target}/abn.diff")) .unwrap() .write_all(&diff) .unwrap(); - let mut fa = - File::create(&format!("{}/alefn", target)).unwrap(); + let mut fa = File::create(&format!("{target}/alefn")).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{}/betn", target)).unwrap(); + let mut fb = File::create(&format!("{target}/betn")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") .arg("--normal") - .arg(&format!("{}/alefn", target)) - .stdin(File::open(&format!("{}/abn.diff", target)).unwrap()) + .arg(&format!("{target}/alefn")) + .stdin(File::open(&format!("{target}/abn.diff")).unwrap()) .output() .unwrap(); - if !output.status.success() { - panic!("{:?}", output); - } + assert!(output.status.success(), "{:?}", output); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read(&format!("{}/alefn", target)).unwrap(); + let alef = fs::read(&format!("{target}/alefn")).unwrap(); assert_eq!(alef, bet); } } @@ -392,28 +388,26 @@ mod tests { // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff(&alef, &bet); - File::create(&format!("{}/ab_.diff", target)) + File::create(&format!("{target}/ab_.diff")) .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create(&format!("{}/alef_", target)).unwrap(); + let mut fa = File::create(&format!("{target}/alef_")).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{}/bet_", target)).unwrap(); + let mut fb = File::create(&format!("{target}/bet_")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") - .arg(&format!("{}/alef_", target)) - .stdin(File::open(&format!("{}/ab_.diff", target)).unwrap()) + .arg(&format!("{target}/alef_")) + .stdin(File::open(&format!("{target}/ab_.diff")).unwrap()) .output() .unwrap(); - if !output.status.success() { - panic!("{:?}", output); - } + assert!(output.status.success(), "{:?}", output); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read(&format!("{}/alef_", target)).unwrap(); + let alef = fs::read(&format!("{target}/alef_")).unwrap(); assert_eq!(alef, bet); } } @@ -472,28 +466,26 @@ mod tests { // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff(&alef, &bet); - File::create(&format!("{}/abr.diff", target)) + File::create(&format!("{target}/abr.diff")) .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create(&format!("{}/alefr", target)).unwrap(); + let mut fa = File::create(&format!("{target}/alefr")).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{}/betr", target)).unwrap(); + let mut fb = File::create(&format!("{target}/betr")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") - .arg(&format!("{}/alefr", target)) - .stdin(File::open(&format!("{}/abr.diff", target)).unwrap()) + .arg(&format!("{target}/alefr")) + .stdin(File::open(&format!("{target}/abr.diff")).unwrap()) .output() .unwrap(); - if !output.status.success() { - panic!("{:?}", output); - } + assert!(output.status.success(), "{:?}", output); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read(&format!("{}/alefr", target)).unwrap(); + let alef = fs::read(&format!("{target}/alefr")).unwrap(); assert_eq!(alef, bet); } } diff --git a/src/params.rs b/src/params.rs index 9c1517a..b118be7 100644 --- a/src/params.rs +++ b/src/params.rs @@ -30,11 +30,9 @@ pub struct Params { pub fn parse_params>(opts: I) -> Result { let mut opts = opts.into_iter(); // parse CLI - let exe = match opts.next() { - Some(from) => from, - None => { - return Err("Usage: ".to_string()); - } + + let Some(exe) = opts.next() else { + return Err("Usage: ".to_string()); }; let mut from = None; let mut to = None; @@ -101,9 +99,8 @@ pub fn parse_params>(opts: I) -> Result return Err(format!("Unknown option: {}", String::from_utf8_lossy(&[b]))), } @@ -111,7 +108,7 @@ pub fn parse_params>(opts: I) -> Result ", exe.to_string_lossy())); } diff --git a/src/unified_diff.rs b/src/unified_diff.rs index 05074e5..18d93bc 100644 --- a/src/unified_diff.rs +++ b/src/unified_diff.rs @@ -79,7 +79,7 @@ fn make_diff(expected: &[u8], actual: &[u8], context_size: usize) -> Vec expected_lines_count { - mismatch.lines.push(DiffLine::MissingNL) + mismatch.lines.push(DiffLine::MissingNL); } mismatch.lines.push(DiffLine::Actual(res)); mismatch.lines.push(DiffLine::MissingNL); @@ -89,7 +89,7 @@ fn make_diff(expected: &[u8], actual: &[u8], context_size: usize) -> Vec expected_lines_count { - mismatch.lines.push(DiffLine::MissingNL) + mismatch.lines.push(DiffLine::MissingNL); } } line_number_expected += 1; @@ -111,7 +111,7 @@ fn make_diff(expected: &[u8], actual: &[u8], context_size: usize) -> Vec actual_lines_count { - mismatch.lines.push(DiffLine::MissingNL) + mismatch.lines.push(DiffLine::MissingNL); } line_number_actual += 1; lines_since_mismatch = 0; @@ -224,6 +224,7 @@ fn make_diff(expected: &[u8], actual: &[u8], context_size: usize) -> Vec Vec { - let mut output = - format!("--- {}\t\n+++ {}\t\n", expected_filename, actual_filename).into_bytes(); + let mut output = format!("--- {expected_filename}\t\n+++ {actual_filename}\t\n").into_bytes(); let diff_results = make_diff(expected, actual, context_size); if diff_results.is_empty() { return Vec::new(); @@ -334,25 +334,24 @@ pub fn diff( // I made this comment because this stuff is not obvious from GNU's // documentation on the format at all. if expected_count == 0 { - line_number_expected -= 1 + line_number_expected -= 1; } if actual_count == 0 { - line_number_actual -= 1 + line_number_actual -= 1; } let exp_ct = if expected_count == 1 { String::new() } else { - format!(",{}", expected_count) + format!(",{expected_count}") }; let act_ct = if actual_count == 1 { String::new() } else { - format!(",{}", actual_count) + format!(",{actual_count}") }; writeln!( output, - "@@ -{}{} +{}{} @@", - line_number_expected, exp_ct, line_number_actual, act_ct + "@@ -{line_number_expected}{exp_ct} +{line_number_actual}{act_ct} @@" ) .expect("write to Vec is infallible"); for line in result.lines { @@ -435,28 +434,26 @@ mod tests { // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = - diff(&alef, "a/alef", &bet, &format!("{}/alef", target), 2); - File::create(&format!("{}/ab.diff", target)) + diff(&alef, "a/alef", &bet, &format!("{target}/alef"), 2); + File::create(&format!("{target}/ab.diff")) .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create(&format!("{}/alef", target)).unwrap(); + let mut fa = File::create(&format!("{target}/alef")).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{}/bet", target)).unwrap(); + let mut fb = File::create(&format!("{target}/bet")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") - .stdin(File::open(&format!("{}/ab.diff", target)).unwrap()) + .stdin(File::open(&format!("{target}/ab.diff")).unwrap()) .output() .unwrap(); - if !output.status.success() { - panic!("{:?}", output); - } + assert!(output.status.success(), "{:?}", output); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read(&format!("{}/alef", target)).unwrap(); + let alef = fs::read(&format!("{target}/alef")).unwrap(); assert_eq!(alef, bet); } } @@ -528,35 +525,27 @@ mod tests { } // This test diff is intentionally reversed. // We want it to turn the alef into bet. - let diff = diff( - &alef, - "a/alefn", - &bet, - &format!("{}/alefn", target), - 2, - ); - File::create(&format!("{}/abn.diff", target)) + let diff = + diff(&alef, "a/alefn", &bet, &format!("{target}/alefn"), 2); + File::create(&format!("{target}/abn.diff")) .unwrap() .write_all(&diff) .unwrap(); - let mut fa = - File::create(&format!("{}/alefn", target)).unwrap(); + let mut fa = File::create(&format!("{target}/alefn")).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{}/betn", target)).unwrap(); + let mut fb = File::create(&format!("{target}/betn")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") - .stdin(File::open(&format!("{}/abn.diff", target)).unwrap()) + .stdin(File::open(&format!("{target}/abn.diff")).unwrap()) .output() .unwrap(); - if !output.status.success() { - panic!("{:?}", output); - } + assert!(output.status.success(), "{:?}", output); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read(&format!("{}/alefn", target)).unwrap(); + let alef = fs::read(&format!("{target}/alefn")).unwrap(); assert_eq!(alef, bet); } } @@ -624,35 +613,27 @@ mod tests { } // This test diff is intentionally reversed. // We want it to turn the alef into bet. - let diff = diff( - &alef, - "a/alef_", - &bet, - &format!("{}/alef_", target), - 2, - ); - File::create(&format!("{}/ab_.diff", target)) + let diff = + diff(&alef, "a/alef_", &bet, &format!("{target}/alef_"), 2); + File::create(&format!("{target}/ab_.diff")) .unwrap() .write_all(&diff) .unwrap(); - let mut fa = - File::create(&format!("{}/alef_", target)).unwrap(); + let mut fa = File::create(&format!("{target}/alef_")).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{}/bet_", target)).unwrap(); + let mut fb = File::create(&format!("{target}/bet_")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") - .stdin(File::open(&format!("{}/ab_.diff", target)).unwrap()) + .stdin(File::open(&format!("{target}/ab_.diff")).unwrap()) .output() .unwrap(); - if !output.status.success() { - panic!("{:?}", output); - } + assert!(output.status.success(), "{:?}", output); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read(&format!("{}/alef_", target)).unwrap(); + let alef = fs::read(&format!("{target}/alef_")).unwrap(); assert_eq!(alef, bet); } } @@ -706,28 +687,26 @@ mod tests { // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = - diff(&alef, "a/alefx", &bet, &format!("{}/alefx", target), 2); - File::create(&format!("{}/abx.diff", target)) + diff(&alef, "a/alefx", &bet, &format!("{target}/alefx"), 2); + File::create(&format!("{target}/abx.diff")) .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create(&format!("{}/alefx", target)).unwrap(); + let mut fa = File::create(&format!("{target}/alefx")).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{}/betx", target)).unwrap(); + let mut fb = File::create(&format!("{target}/betx")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") - .stdin(File::open(&format!("{}/abx.diff", target)).unwrap()) + .stdin(File::open(&format!("{target}/abx.diff")).unwrap()) .output() .unwrap(); - if !output.status.success() { - panic!("{:?}", output); - } + assert!(output.status.success(), "{:?}", output); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read(&format!("{}/alefx", target)).unwrap(); + let alef = fs::read(&format!("{target}/alefx")).unwrap(); assert_eq!(alef, bet); } } @@ -786,28 +765,26 @@ mod tests { // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = - diff(&alef, "a/alefr", &bet, &format!("{}/alefr", target), 2); - File::create(&format!("{}/abr.diff", target)) + diff(&alef, "a/alefr", &bet, &format!("{target}/alefr"), 2); + File::create(&format!("{target}/abr.diff")) .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create(&format!("{}/alefr", target)).unwrap(); + let mut fa = File::create(&format!("{target}/alefr")).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{}/betr", target)).unwrap(); + let mut fb = File::create(&format!("{target}/betr")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") - .stdin(File::open(&format!("{}/abr.diff", target)).unwrap()) + .stdin(File::open(&format!("{target}/abr.diff")).unwrap()) .output() .unwrap(); - if !output.status.success() { - panic!("{:?}", output); - } + assert!(output.status.success(), "{:?}", output); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read(&format!("{}/alefr", target)).unwrap(); + let alef = fs::read(&format!("{target}/alefr")).unwrap(); assert_eq!(alef, bet); } } From 1372c5386c8f323f081f8a35dda4e2cd707c65e2 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Tue, 23 Jan 2024 12:36:57 +0100 Subject: [PATCH 021/194] use pretty assertions to help with Windows debug --- Cargo.toml | 3 +++ src/context_diff.rs | 2 +- src/ed_diff.rs | 2 +- src/normal_diff.rs | 2 +- src/unified_diff.rs | 1 + 5 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d1c57d5..22a2afc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,3 +16,6 @@ path = "src/main.rs" [dependencies] diff = "0.1.10" + +[dev-dependencies] +pretty_assertions = "1" diff --git a/src/context_diff.rs b/src/context_diff.rs index b650985..1c9d44f 100644 --- a/src/context_diff.rs +++ b/src/context_diff.rs @@ -355,7 +355,7 @@ pub fn diff( #[cfg(test)] mod tests { use super::*; - + use pretty_assertions::assert_eq; #[test] fn test_permutations() { // test all possible six-line files. diff --git a/src/ed_diff.rs b/src/ed_diff.rs index 3e674d7..2f8c0dd 100644 --- a/src/ed_diff.rs +++ b/src/ed_diff.rs @@ -148,8 +148,8 @@ pub fn diff(expected: &[u8], actual: &[u8]) -> Result, DiffError> { #[cfg(test)] mod tests { - use super::*; + use pretty_assertions::assert_eq; pub fn diff_w(expected: &[u8], actual: &[u8], filename: &str) -> Result, DiffError> { let mut output = diff(expected, actual)?; writeln!(&mut output, "w {filename}").unwrap(); diff --git a/src/normal_diff.rs b/src/normal_diff.rs index c3297a1..eda9d8c 100644 --- a/src/normal_diff.rs +++ b/src/normal_diff.rs @@ -172,7 +172,7 @@ pub fn diff(expected: &[u8], actual: &[u8]) -> Vec { #[cfg(test)] mod tests { use super::*; - + use pretty_assertions::assert_eq; #[test] fn test_permutations() { let target = "target/normal-diff/"; diff --git a/src/unified_diff.rs b/src/unified_diff.rs index 18d93bc..350069a 100644 --- a/src/unified_diff.rs +++ b/src/unified_diff.rs @@ -384,6 +384,7 @@ pub fn diff( #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_permutations() { From f5b65a5720706b13ec0a96e046a549a8eb275736 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Tue, 23 Jan 2024 12:45:02 +0100 Subject: [PATCH 022/194] add debug info --- src/unified_diff.rs | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/unified_diff.rs b/src/unified_diff.rs index 350069a..2be092b 100644 --- a/src/unified_diff.rs +++ b/src/unified_diff.rs @@ -446,14 +446,30 @@ mod tests { fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; + println!( + "diff: {:?}", + String::from_utf8(diff.clone()) + .unwrap_or_else(|_| String::from("[Invalid UTF-8]")) + ); + println!( + "alef: {:?}", + String::from_utf8(alef.clone()) + .unwrap_or_else(|_| String::from("[Invalid UTF-8]")) + ); + println!( + "bet: {:?}", + String::from_utf8(bet.clone()) + .unwrap_or_else(|_| String::from("[Invalid UTF-8]")) + ); + let output = Command::new("patch") .arg("-p0") .stdin(File::open(&format!("{target}/ab.diff")).unwrap()) .output() .unwrap(); + println!("{}", String::from_utf8_lossy(&output.stdout)); + println!("{}", String::from_utf8_lossy(&output.stderr)); assert!(output.status.success(), "{:?}", output); - //println!("{}", String::from_utf8_lossy(&output.stdout)); - //println!("{}", String::from_utf8_lossy(&output.stderr)); let alef = fs::read(&format!("{target}/alef")).unwrap(); assert_eq!(alef, bet); } From 865f97c78da540d8ae3d71b776355155e357d67c Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Wed, 24 Jan 2024 09:30:11 +0100 Subject: [PATCH 023/194] upgrade the rust edition --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 22a2afc..984e4d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,13 @@ [package] name = "diffutils" version = "0.3.0" -edition = "2018" +edition = "2021" description = "A CLI app for generating diff files" license = "MIT OR Apache-2.0" repository = "https://github.com/uutils/diffutils" [lib] -name = "diffutils" +name = "diffutilslib" path = "src/lib.rs" [[bin]] From 3c1176082ee0bdf1c4eac87b487fd240016a5de3 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Wed, 24 Jan 2024 09:30:47 +0100 Subject: [PATCH 024/194] Ship with Cargo.lock --- .gitignore | 1 - Cargo.lock | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 Cargo.lock diff --git a/.gitignore b/.gitignore index 3991d07..033ac24 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,4 @@ /lib/ed-diff/target /lib/context-diff/target /lib/unified-diff/target -Cargo.lock *.swp diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..2ae28d9 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,33 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "diffutils" +version = "0.3.0" +dependencies = [ + "diff", + "pretty_assertions", +] + +[[package]] +name = "pretty_assertions" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" From 0e14b37e3801518001843730273c1e2892ffe50c Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Wed, 24 Jan 2024 09:31:09 +0100 Subject: [PATCH 025/194] refresh the gitignore --- .gitignore | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.gitignore b/.gitignore index 033ac24..e9868bd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,2 @@ /target -/lib/normal-diff/target -/lib/ed-diff/target -/lib/context-diff/target -/lib/unified-diff/target *.swp From e5de3cd93ee9d731679d702307a861adb22fd780 Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Fri, 26 Jan 2024 16:23:16 +0100 Subject: [PATCH 026/194] ci: remove actions-rs/cargo & use cargo directly --- .github/workflows/ci.yml | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 33f1bc4..d93d582 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,9 +19,7 @@ jobs: profile: minimal toolchain: stable override: true - - uses: actions-rs/cargo@v1 - with: - command: check + - run: cargo check test: name: cargo test @@ -36,9 +34,7 @@ jobs: profile: minimal toolchain: stable override: true - - uses: actions-rs/cargo@v1 - with: - command: test + - run: cargo test fmt: name: cargo fmt --all -- --check @@ -51,10 +47,7 @@ jobs: toolchain: stable override: true - run: rustup component add rustfmt - - uses: actions-rs/cargo@v1 - with: - command: fmt - args: --all -- --check + - run: cargo fmt --all -- --check clippy: name: cargo clippy -- -D warnings @@ -70,10 +63,7 @@ jobs: toolchain: stable override: true - run: rustup component add clippy - - uses: actions-rs/cargo@v1 - with: - command: clippy - args: -- -D warnings + - run: cargo clippy -- -D warnings coverage: name: Code Coverage @@ -114,10 +104,7 @@ jobs: default: true profile: minimal # minimal component installation (ie, no documentation) - name: Test - uses: actions-rs/cargo@v1 - with: - command: test - args: ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} --no-fail-fast + run: cargo test ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} --no-fail-fast env: CARGO_INCREMENTAL: "0" RUSTC_WRAPPER: "" From 329a7e1f4ae7d78d8c9d7cf72bb0409a1fdd12d0 Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Fri, 26 Jan 2024 16:29:22 +0100 Subject: [PATCH 027/194] ci: use dtolnay/rust-toolchain instead of unmaintained actions-rs/toolchain --- .github/workflows/ci.yml | 30 +++++------------------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d93d582..03fc854 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,11 +14,7 @@ jobs: os: [ubuntu-latest, macOS-latest, windows-latest] steps: - uses: actions/checkout@v4 - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true + - uses: dtolnay/rust-toolchain@stable - run: cargo check test: @@ -29,11 +25,7 @@ jobs: os: [ubuntu-latest, macOS-latest, windows-latest] steps: - uses: actions/checkout@v4 - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true + - uses: dtolnay/rust-toolchain@stable - run: cargo test fmt: @@ -41,11 +33,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true + - uses: dtolnay/rust-toolchain@stable - run: rustup component add rustfmt - run: cargo fmt --all -- --check @@ -57,11 +45,7 @@ jobs: os: [ubuntu-latest, macOS-latest, windows-latest] steps: - uses: actions/checkout@v4 - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true + - uses: dtolnay/rust-toolchain@stable - run: rustup component add clippy - run: cargo clippy -- -D warnings @@ -98,11 +82,7 @@ jobs: outputs CODECOV_FLAGS - name: rust toolchain ~ install - uses: actions-rs/toolchain@v1 - with: - toolchain: ${{ steps.vars.outputs.TOOLCHAIN }} - default: true - profile: minimal # minimal component installation (ie, no documentation) + uses: dtolnay/rust-toolchain@nightly - name: Test run: cargo test ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} --no-fail-fast env: From 6a69a3985271092c0c15b09ed644da638bcf34c1 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 27 Jan 2024 09:50:58 +0100 Subject: [PATCH 028/194] add badges --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 009f66a..aef5bdd 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,10 @@ +[![Crates.io](https://img.shields.io/crates/v/diffutils.svg)](https://crates.io/crates/diffutils) +[![Discord](https://img.shields.io/badge/discord-join-7289DA.svg?logo=discord&longCache=true&style=flat)](https://discord.gg/wQVJbvJ) +[![License](http://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/uutils/diffutils/blob/main/LICENSE) +[![dependency status](https://deps.rs/repo/github/uutils/diffutils/status.svg)](https://deps.rs/repo/github/uutils/diffutils) + +[![CodeCov](https://codecov.io/gh/uutils/diffutils/branch/master/graph/badge.svg)](https://codecov.io/gh/uutils/diffutils) + The goal of this package is to be a dropped in replacement for the [diffutils commands](https://www.gnu.org/software/diffutils/) in Rust. From c28973c019afe575b98828277c4a06574b3c0a6a Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 9 Feb 2024 10:28:27 +0100 Subject: [PATCH 029/194] coverage: remove the fail fast to continue for linux even if windows fails --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 03fc854..3ffce21 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,7 +53,6 @@ jobs: name: Code Coverage runs-on: ${{ matrix.job.os }} strategy: - fail-fast: true matrix: job: - { os: ubuntu-latest , features: unix } From a94c6a60cf537c3007d1f0329d87ba889409161f Mon Sep 17 00:00:00 2001 From: Olivier Tilloy Date: Wed, 7 Feb 2024 20:50:55 +0100 Subject: [PATCH 030/194] Normal diff: compact ranges of lines where possible (fixes #10) --- src/normal_diff.rs | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/normal_diff.rs b/src/normal_diff.rs index eda9d8c..d4c2a86 100644 --- a/src/normal_diff.rs +++ b/src/normal_diff.rs @@ -136,6 +136,28 @@ pub fn diff(expected: &[u8], actual: &[u8]) -> Vec { line_number_actual - 1 ) .unwrap(), + (1, 1) => writeln!( + &mut output, + "{}c{}", + line_number_expected, line_number_actual + ) + .unwrap(), + (1, _) => writeln!( + &mut output, + "{}c{},{}", + line_number_expected, + line_number_actual, + actual_count + line_number_actual - 1 + ) + .unwrap(), + (_, 1) => writeln!( + &mut output, + "{},{}c{}", + line_number_expected, + expected_count + line_number_expected - 1, + line_number_actual + ) + .unwrap(), _ => writeln!( &mut output, "{},{}c{},{}", @@ -173,6 +195,18 @@ pub fn diff(expected: &[u8], actual: &[u8]) -> Vec { mod tests { use super::*; use pretty_assertions::assert_eq; + + #[test] + fn test_basic() { + let mut a = Vec::new(); + a.write_all(b"a\n").unwrap(); + let mut b = Vec::new(); + b.write_all(b"b\n").unwrap(); + let diff = diff(&a, &b); + let expected = b"1c1\n< a\n---\n> b\n".to_vec(); + assert_eq!(diff, expected); + } + #[test] fn test_permutations() { let target = "target/normal-diff/"; From 3f9556aa05f494ea27a4bbd6e90e584c5915d8f3 Mon Sep 17 00:00:00 2001 From: Olivier Tilloy Date: Thu, 8 Feb 2024 17:04:00 +0100 Subject: [PATCH 031/194] Add comments to ease readability and maintainability --- src/normal_diff.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/normal_diff.rs b/src/normal_diff.rs index d4c2a86..fa9c059 100644 --- a/src/normal_diff.rs +++ b/src/normal_diff.rs @@ -111,6 +111,8 @@ fn make_diff(expected: &[u8], actual: &[u8]) -> Vec { #[must_use] pub fn diff(expected: &[u8], actual: &[u8]) -> Vec { + // See https://www.gnu.org/software/diffutils/manual/html_node/Detailed-Normal.html + // for details on the syntax of the normal format. let mut output = Vec::new(); let diff_results = make_diff(expected, actual); for result in diff_results { @@ -121,6 +123,7 @@ pub fn diff(expected: &[u8], actual: &[u8]) -> Vec { match (expected_count, actual_count) { (0, 0) => unreachable!(), (0, _) => writeln!( + // 'a' stands for "Add lines" &mut output, "{}a{},{}", line_number_expected - 1, @@ -129,6 +132,7 @@ pub fn diff(expected: &[u8], actual: &[u8]) -> Vec { ) .unwrap(), (_, 0) => writeln!( + // 'd' stands for "Delete lines" &mut output, "{},{}d{}", line_number_expected, @@ -137,12 +141,16 @@ pub fn diff(expected: &[u8], actual: &[u8]) -> Vec { ) .unwrap(), (1, 1) => writeln!( + // 'c' stands for "Change lines" + // exactly one line replaced by one line &mut output, "{}c{}", - line_number_expected, line_number_actual + line_number_expected, + line_number_actual ) .unwrap(), (1, _) => writeln!( + // one line replaced by multiple lines &mut output, "{}c{},{}", line_number_expected, @@ -151,6 +159,7 @@ pub fn diff(expected: &[u8], actual: &[u8]) -> Vec { ) .unwrap(), (_, 1) => writeln!( + // multiple lines replaced by one line &mut output, "{},{}c{}", line_number_expected, @@ -159,6 +168,7 @@ pub fn diff(expected: &[u8], actual: &[u8]) -> Vec { ) .unwrap(), _ => writeln!( + // general case: multiple lines replaced by multiple lines &mut output, "{},{}c{},{}", line_number_expected, From 54a5407bec1e96f7526c0fb5e0cbd40e46496c1e Mon Sep 17 00:00:00 2001 From: Olivier Tilloy Date: Fri, 9 Feb 2024 17:51:50 +0100 Subject: [PATCH 032/194] Fix trivial typo in error message --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index eaa7d55..8a3e74e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -33,7 +33,7 @@ fn main() -> Result<(), String> { let to_content = match fs::read(&to) { Ok(to_content) => to_content, Err(e) => { - return Err(format!("Failed to read from-file: {e}")); + return Err(format!("Failed to read to-file: {e}")); } }; // run diff From 790ef1e633ecfda15e0f81b66777365576adf838 Mon Sep 17 00:00:00 2001 From: Olivier Tilloy Date: Sat, 17 Feb 2024 14:27:52 +0000 Subject: [PATCH 033/194] Run the GNU test suite in CI (fixes #8) (#13) --- .github/workflows/ci.yml | 17 ++++ tests/print-test-results.sh | 38 +++++++++ tests/run-upstream-testsuite.sh | 141 ++++++++++++++++++++++++++++++++ 3 files changed, 196 insertions(+) create mode 100755 tests/print-test-results.sh create mode 100755 tests/run-upstream-testsuite.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3ffce21..d416f74 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,6 +49,23 @@ jobs: - run: rustup component add clippy - run: cargo clippy -- -D warnings + gnu-testsuite: + name: GNU test suite + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - run: cargo build --release + # do not fail, the report is merely informative (at least until all tests pass reliably) + - run: ./tests/run-upstream-testsuite.sh release || true + env: + TERM: xterm + - uses: actions/upload-artifact@v4 + with: + name: test-results.json + path: tests/test-results.json + - run: ./tests/print-test-results.sh tests/test-results.json + coverage: name: Code Coverage runs-on: ${{ matrix.job.os }} diff --git a/tests/print-test-results.sh b/tests/print-test-results.sh new file mode 100755 index 0000000..23136d6 --- /dev/null +++ b/tests/print-test-results.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +# Print the test results written to a JSON file by run-upstream-testsuite.sh +# in a markdown format. The printout includes the name of the test, the result, +# the URL to the test script and the contents of stdout and stderr. +# It can be used verbatim as the description when filing an issue for a test +# with an unexpected result. + +json="test-results.json" +[[ -n $1 ]] && json="$1" + +codeblock () { echo -e "\`\`\`\n$1\n\`\`\`"; } + +jq -c '.tests[]' "$json" | while read -r test +do + name=$(echo "$test" | jq -r '.test') + echo "# test: $name" + result=$(echo "$test" | jq -r '.result') + echo "result: $result" + url=$(echo "$test" | jq -r '.url') + echo "url: $url" + if [[ "$result" != "SKIP" ]] + then + stdout=$(echo "$test" | jq -r '.stdout' | base64 -d) + if [[ -n "$stdout" ]] + then + echo "## stdout" + codeblock "$stdout" + fi + stderr=$(echo "$test" | jq -r '.stderr' | base64 -d) + if [[ -n "$stderr" ]] + then + echo "## stderr" + codeblock "$stderr" + fi + fi + echo "" +done diff --git a/tests/run-upstream-testsuite.sh b/tests/run-upstream-testsuite.sh new file mode 100755 index 0000000..d85dfd6 --- /dev/null +++ b/tests/run-upstream-testsuite.sh @@ -0,0 +1,141 @@ +#!/bin/bash + +# Run the GNU upstream test suite for diffutils against a local build of the +# Rust implementation, print out a summary of the test results, and writes a +# JSON file ('test-results.json') containing detailed information about the +# test run. + +# The JSON file contains metadata about the test run, and for each test the +# result as well as the contents of stdout, stderr, and of all the files +# written by the test script, if any (excluding subdirectories). + +# The script takes a shortcut to fetch only the test suite from the upstream +# repository and carefully avoids running the autotools machinery which is +# time-consuming and resource-intensive, and doesn't offer the option to not +# build the upstream binaries. As a consequence, the environment in which the +# tests are run might not match exactly that used when the upstream tests are +# run through the autotools. + +# By default it expects a release build of the diffutils binary, but a +# different build profile can be specified as an argument +# (e.g. 'dev' or 'test'). +# Unless overriden by the $TESTS environment variable, all tests in the test +# suite will be run. Tests targeting a command that is not yet implemented +# (e.g. cmp, diff3 or sdiff) are skipped. + +scriptpath=$(dirname "$(readlink -f "$0")") +rev=$(git rev-parse HEAD) + +# Allow passing a specific profile as parameter (default to "release") +profile="release" +[[ -n $1 ]] && profile="$1" + +# Verify that the diffutils binary was built for the requested profile +binary="$scriptpath/../target/$profile/diffutils" +if [[ ! -x "$binary" ]] +then + echo "Missing build for profile $profile" + exit 1 +fi + +# Work in a temporary directory +tempdir=$(mktemp -d) +cd "$tempdir" + +# Check out the upstream test suite +gitserver="https://git.savannah.gnu.org" +testsuite="$gitserver/git/diffutils.git" +echo "Fetching upstream test suite from $testsuite" +git clone -n --depth=1 --filter=tree:0 "$testsuite" &> /dev/null +cd diffutils +git sparse-checkout set --no-cone tests &> /dev/null +git checkout &> /dev/null +upstreamrev=$(git rev-parse HEAD) + +# Ensure that calling `diff` invokes the built `diffutils` binary instead of +# the upstream `diff` binary that is most likely installed on the system +mkdir src +cd src +ln -s "$binary" diff +cd ../tests + +if [[ -n "$TESTS" ]] +then + tests="$TESTS" +else + # Get a list of all upstream tests (default if $TESTS isn't set) + echo -e '\n\nprinttests:\n\t@echo "${TESTS}"' >> Makefile.am + tests=$(make -f Makefile.am printtests) +fi +total=$(echo "$tests" | wc -w) +echo "Running $total tests" +export LC_ALL=C +export KEEP=yes +exitcode=0 +timestamp=$(date -Iseconds) +urlroot="$gitserver/cgit/diffutils.git/tree/tests/" +passed=0 +failed=0 +skipped=0 +normal="$(tput sgr0)" +for test in $tests +do + result="FAIL" + url="$urlroot$test?id=$upstreamrev" + # Run only the tests that invoke `diff`, + # because other binaries aren't implemented yet + if ! grep -E -s -q "(cmp|diff3|sdiff)" "$test" + then + sh "$test" 1> stdout.txt 2> stderr.txt && result="PASS" || exitcode=1 + json+="{\"test\":\"$test\",\"result\":\"$result\"," + json+="\"url\":\"$url\"," + json+="\"stdout\":\"$(base64 -w0 < stdout.txt)\"," + json+="\"stderr\":\"$(base64 -w0 < stderr.txt)\"," + json+="\"files\":{" + cd gt-$test.* + # Note: this doesn't include the contents of subdirectories, + # but there isn't much value added in doing so + for file in * + do + [[ -f "$file" ]] && json+="\"$file\":\"$(base64 -w0 < "$file")\"," + done + json="${json%,}}}," + cd - > /dev/null + [[ "$result" = "PASS" ]] && (( passed++ )) + [[ "$result" = "FAIL" ]] && (( failed++ )) + else + result="SKIP" + (( skipped++ )) + json+="{\"test\":\"$test\",\"url\":\"$url\",\"result\":\"$result\"}," + fi + color=2 # green + [[ "$result" = "FAIL" ]] && color=1 # red + [[ "$result" = "SKIP" ]] && color=3 # yellow + printf " %-40s $(tput setaf $color)$result$(tput sgr0)\n" "$test" +done +echo "" +echo -n "Summary: TOTAL: $total / " +echo -n "$(tput setaf 2)PASS$normal: $passed / " +echo -n "$(tput setaf 1)FAIL$normal: $failed / " +echo "$(tput setaf 3)SKIP$normal: $skipped" +echo "" + +json="\"tests\":[${json%,}]" +metadata="\"timestamp\":\"$timestamp\"," +metadata+="\"revision\":\"$rev\"," +metadata+="\"upstream-revision\":\"$upstreamrev\"," +if [[ -n "$GITHUB_ACTIONS" ]] +then + metadata+="\"branch\":\"$GITHUB_REF\"," +fi +json="{$metadata $json}" + +# Clean up +cd "$scriptpath" +rm -rf "$tempdir" + +resultsfile="test-results.json" +echo "$json" | jq > "$resultsfile" +echo "Results written to $scriptpath/$resultsfile" + +exit $exitcode From 6c29f02527358a81737b1086dc490f288edfb799 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 17 Feb 2024 17:05:51 +0100 Subject: [PATCH 034/194] improve the readme and example --- README.md | 91 ++++++++++++++++++++++++------------------------------- 1 file changed, 40 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index aef5bdd..c99770f 100644 --- a/README.md +++ b/README.md @@ -7,59 +7,48 @@ The goal of this package is to be a dropped in replacement for the [diffutils commands](https://www.gnu.org/software/diffutils/) in Rust. - Based on the incomplete diff generator in https://github.com/rust-lang/rust/blob/master/src/tools/compiletest/src/runtest.rs, and made to be compatible with GNU's diff and patch tools. + +## Installation + +Ensure you have Rust installed on your system. You can install Rust through [rustup](https://rustup.rs/). + +Clone the repository and build the project using Cargo: + +```bash +git clone https://github.com/uutils/diffutils.git +cd diffutils +cargo build --release ``` -~/diffutils$ cargo run -- diff -u3 Cargo.lock Cargo.toml + +```bash + +cat <fruits_old.txt +Apple +Banana +Cherry +EOF + +cat <fruits_new.txt +Apple +Fig +Cherry +EOF + +$ cargo run -- -u fruits_old.txt fruits_new.txt Finished dev [unoptimized + debuginfo] target(s) in 0.00s - Running `target/debug/diff -u3 Cargo.lock Cargo.toml` ---- Cargo.lock -+++ Cargo.toml -@@ -1,39 +1,7 @@ --# This file is automatically @generated by Cargo. --# It is not intended for manual editing. --version = 3 -- --[[package]] --name = "context-diff" --version = "0.1.0" --dependencies = [ -- "diff 0.1.12", --] -- --[[package]] --name = "diff" --version = "0.1.0" --dependencies = [ -- "context-diff", -- "normal-diff", -- "unified-diff", --] -- --[[package]] --name = "diff" --version = "0.1.12" --source = "registry+https://github.com/rust-lang/crates.io-index" --checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499" -- --[[package]] --name = "normal-diff" --version = "0.1.0" --dependencies = [ -- "diff 0.1.12", --] -- --[[package]] --name = "unified-diff" --version = "0.3.0" --dependencies = [ -- "diff 0.1.12", -+[workspace] -+members = [ -+ "lib/unified-diff", -+ "lib/context-diff", -+ "lib/normal-diff", -+ "bin/diff", - ] + Running `target/debug/diffutils -u fruits_old.txt fruits_new.txt` +--- fruits_old.txt ++++ fruits_new.txt +@@ -1,3 +1,3 @@ + Apple +-Banana ++Fig + Cherry + ``` + +## License + +diffutils is licensed under the MIT and Apache Licenses - see the `LICENSE-MIT` or `LICENSE-APACHE` files for details From c90eee442ffe294b110e6fb452797cb55307524e Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 17 Feb 2024 17:07:28 +0100 Subject: [PATCH 035/194] Add the example section --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index c99770f..1308648 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,8 @@ cd diffutils cargo build --release ``` +## Example + ```bash cat <fruits_old.txt From 3bc8668f78ffdacad2f8ab62abb931c6aaa665db Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 17 Feb 2024 18:19:02 +0100 Subject: [PATCH 036/194] fix(deps): update rust crate libfuzzer-sys to 0.4 (#5) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- fuzz/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index c1bebc9..650e1d4 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -9,7 +9,7 @@ edition = "2018" cargo-fuzz = true [dependencies] -libfuzzer-sys = "0.3" +libfuzzer-sys = "0.4" diffutils = { path = "../" } # Prevent this from interfering with workspaces From 1241db48069906973e6ef1c200d3eb1199fc3fe9 Mon Sep 17 00:00:00 2001 From: Olivier Tilloy Date: Wed, 21 Feb 2024 22:43:17 +0100 Subject: [PATCH 037/194] Match GNU diff's implementation for exit codes (fixes #17) --- src/main.rs | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/main.rs b/src/main.rs index 8a3e74e..a684ff4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,7 @@ use std::env; use std::fs; use std::io::{self, Write}; +use std::process::{exit, ExitCode}; mod context_diff; mod ed_diff; @@ -15,25 +16,35 @@ mod normal_diff; mod params; mod unified_diff; -fn main() -> Result<(), String> { +// Exit codes are documented at +// https://www.gnu.org/software/diffutils/manual/html_node/Invoking-diff.html. +// An exit status of 0 means no differences were found, +// 1 means some differences were found, +// and 2 means trouble. +fn main() -> ExitCode { let opts = env::args_os(); let Params { from, to, context_count, format, - } = parse_params(opts)?; + } = parse_params(opts).unwrap_or_else(|error| { + eprintln!("{error}"); + exit(2); + }); // read files let from_content = match fs::read(&from) { Ok(from_content) => from_content, Err(e) => { - return Err(format!("Failed to read from-file: {e}")); + eprintln!("Failed to read from-file: {e}"); + return ExitCode::from(2); } }; let to_content = match fs::read(&to) { Ok(to_content) => to_content, Err(e) => { - return Err(format!("Failed to read to-file: {e}")); + eprintln!("Failed to read to-file: {e}"); + return ExitCode::from(2); } }; // run diff @@ -53,8 +64,15 @@ fn main() -> Result<(), String> { &to.to_string_lossy(), context_count, ), - Format::Ed => ed_diff::diff(&from_content, &to_content)?, + Format::Ed => ed_diff::diff(&from_content, &to_content).unwrap_or_else(|error| { + eprintln!("{error}"); + exit(2); + }), }; io::stdout().write_all(&result).unwrap(); - Ok(()) + if result.is_empty() { + ExitCode::SUCCESS + } else { + ExitCode::from(1) + } } From 0a67bf9fb827853596bc1ec54d4822763e119170 Mon Sep 17 00:00:00 2001 From: Olivier Tilloy Date: Thu, 22 Feb 2024 17:15:03 +0100 Subject: [PATCH 038/194] Add integration tests that check the exit codes and stdout/stderr --- Cargo.lock | 352 +++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 3 + tests/integration.rs | 98 ++++++++++++ 3 files changed, 453 insertions(+) create mode 100644 tests/integration.rs diff --git a/Cargo.lock b/Cargo.lock index 2ae28d9..cf8f04f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,18 +2,180 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstyle" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" + +[[package]] +name = "assert_cmd" +version = "2.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed72493ac66d5804837f480ab3766c72bdfab91a65e565fc54fa9e42db0073a8" +dependencies = [ + "anstyle", + "bstr", + "doc-comment", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" + +[[package]] +name = "bstr" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c48f0051a4b4c5e0b6d365cd04af53aeaa209e3cc15ec2cdb69e73cc87fbd0dc" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "diff" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + [[package]] name = "diffutils" version = "0.3.0" dependencies = [ + "assert_cmd", "diff", + "predicates", "pretty_assertions", + "tempfile", +] + +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + +[[package]] +name = "libc" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + +[[package]] +name = "num-traits" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "predicates" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b87bfd4605926cdfefc1c3b5f8fe560e3feca9d5552cf68c466d3d8236c7e8" +dependencies = [ + "anstyle", + "difflib", + "float-cmp", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" + +[[package]] +name = "predicates-tree" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" +dependencies = [ + "predicates-core", + "termtree", ] [[package]] @@ -26,6 +188,196 @@ dependencies = [ "yansi", ] +[[package]] +name = "proc-macro2" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "rustix" +version = "0.38.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "serde" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74f1bdc9872430ce9b75da68329d1c1746faf50ffac5f19e02b71e37ff881ffb" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67" +dependencies = [ + "cfg-if", + "fastrand", + "rustix", + "windows-sys", +] + +[[package]] +name = "termtree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98532992affa02e52709d5b4d145a3668ae10d9081eea4a7f26f719a8476f71" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7269c1442e75af9fa59290383f7665b828efc76c429cc0b7f2ecb33cf51ebae" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f70ab2cebf332b7ecbdd98900c2da5298a8c862472fb35c75fc297eabb9d89b8" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "679f235acf6b1639408c0f6db295697a19d103b0cdc88146aa1b992c580c647d" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3480ac194b55ae274a7e135c21645656825da4a7f5b6e9286291b2113c94a78b" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42c46bab241c121402d1cb47d028ea3680ee2f359dcc287482dcf7fdddc73363" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc885a4332ee1afb9a1bacf11514801011725570d35675abc229ce7e3afe4d20" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e440c60457f84b0bee09208e62acc7ade264b38c4453f6312b8c9ab1613e73c" + [[package]] name = "yansi" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index 984e4d9..f1c7b71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,3 +19,6 @@ diff = "0.1.10" [dev-dependencies] pretty_assertions = "1" +assert_cmd = "2.0.14" +predicates = "3.1.0" +tempfile = "3.10.0" diff --git a/tests/integration.rs b/tests/integration.rs new file mode 100644 index 0000000..f5768d0 --- /dev/null +++ b/tests/integration.rs @@ -0,0 +1,98 @@ +// This file is part of the uutils diffutils package. +// +// For the full copyright and license information, please view the LICENSE-* +// files that was distributed with this source code. + +use assert_cmd::prelude::*; +use predicates::prelude::*; +use std::io::Write; +use std::process::Command; +use tempfile::NamedTempFile; + +// Integration tests for the diffutils command + +#[test] +fn unknown_param() -> Result<(), Box> { + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("--foobar"); + cmd.assert() + .code(predicate::eq(2)) + .failure() + .stderr(predicate::str::starts_with("Usage: ")); + Ok(()) +} + +#[test] +fn cannot_read_from_file() -> Result<(), Box> { + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("foo.txt").arg("bar.txt"); + cmd.assert() + .code(predicate::eq(2)) + .failure() + .stderr(predicate::str::starts_with("Failed to read from-file")); + Ok(()) +} + +#[test] +fn cannot_read_to_file() -> Result<(), Box> { + let file = NamedTempFile::new()?; + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg(file.path()).arg("bar.txt"); + cmd.assert() + .code(predicate::eq(2)) + .failure() + .stderr(predicate::str::starts_with("Failed to read to-file")); + Ok(()) +} + +#[test] +fn no_differences() -> Result<(), Box> { + let file = NamedTempFile::new()?; + for option in ["", "-u", "-c", "-e"] { + let mut cmd = Command::cargo_bin("diffutils")?; + if !option.is_empty() { + cmd.arg(option); + } + cmd.arg(file.path()).arg(file.path()); + cmd.assert() + .code(predicate::eq(0)) + .success() + .stdout(predicate::str::is_empty()); + } + Ok(()) +} + +#[test] +fn differences() -> Result<(), Box> { + let mut file1 = NamedTempFile::new()?; + file1.write_all("foo\n".as_bytes())?; + let mut file2 = NamedTempFile::new()?; + file2.write_all("bar\n".as_bytes())?; + for option in ["", "-u", "-c", "-e"] { + let mut cmd = Command::cargo_bin("diffutils")?; + if !option.is_empty() { + cmd.arg(option); + } + cmd.arg(file1.path()).arg(file2.path()); + cmd.assert() + .code(predicate::eq(1)) + .failure() + .stdout(predicate::str::is_empty().not()); + } + Ok(()) +} + +#[test] +fn missing_newline() -> Result<(), Box> { + let mut file1 = NamedTempFile::new()?; + file1.write_all("foo".as_bytes())?; + let mut file2 = NamedTempFile::new()?; + file2.write_all("bar".as_bytes())?; + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("-e").arg(file1.path()).arg(file2.path()); + cmd.assert() + .code(predicate::eq(2)) + .failure() + .stderr(predicate::str::starts_with("No newline at end of file")); + Ok(()) +} From a89f30afa0cdd597806ee0cb1c7f461f96c08c43 Mon Sep 17 00:00:00 2001 From: Olivier Tilloy Date: Fri, 23 Feb 2024 22:52:54 +0100 Subject: [PATCH 039/194] Ed diff: compact ranges of lines where possible (fixes #21) --- src/ed_diff.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/ed_diff.rs b/src/ed_diff.rs index 2f8c0dd..41a968a 100644 --- a/src/ed_diff.rs +++ b/src/ed_diff.rs @@ -122,6 +122,7 @@ pub fn diff(expected: &[u8], actual: &[u8]) -> Result, DiffError> { expected_count + line_number_expected - 1 ) .unwrap(), + (1, _) => writeln!(&mut output, "{}c", line_number_expected).unwrap(), _ => writeln!( &mut output, "{},{}c", @@ -156,6 +157,15 @@ mod tests { Ok(output) } + #[test] + fn test_basic() { + let from = b"a\n"; + let to = b"b\n"; + let diff = diff(from, to).unwrap(); + let expected = vec!["1c", "b", ".", ""].join("\n"); + assert_eq!(diff, expected.as_bytes()); + } + #[test] fn test_permutations() { let target = "target/ed-diff/"; From c68d3861700b5059518f86aa58c75906a0159fee Mon Sep 17 00:00:00 2001 From: Olivier Tilloy Date: Fri, 23 Feb 2024 23:45:38 +0100 Subject: [PATCH 040/194] Implement -s/--report-identical-files option (fixes #23) --- Cargo.lock | 41 ++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/main.rs | 16 +++++++++++++ src/params.rs | 56 ++++++++++++++++++++++++++++++++++++++++++++ tests/integration.rs | 41 ++++++++++++++++++++++++++++++++ 5 files changed, 155 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index cf8f04f..92ad218 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -81,6 +81,7 @@ dependencies = [ "diff", "predicates", "pretty_assertions", + "same-file", "tempfile", ] @@ -248,6 +249,15 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "serde" version = "1.0.197" @@ -312,6 +322,37 @@ dependencies = [ "libc", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index f1c7b71..3804a81 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ path = "src/main.rs" [dependencies] diff = "0.1.10" +same-file = "1.0.6" [dev-dependencies] pretty_assertions = "1" diff --git a/src/main.rs b/src/main.rs index a684ff4..ef14096 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,10 +28,25 @@ fn main() -> ExitCode { to, context_count, format, + report_identical_files, } = parse_params(opts).unwrap_or_else(|error| { eprintln!("{error}"); exit(2); }); + // if from and to are the same file, no need to perform any comparison + let maybe_report_identical_files = || { + if report_identical_files { + println!( + "Files {} and {} are identical", + from.to_string_lossy(), + to.to_string_lossy(), + ) + } + }; + if same_file::is_same_file(&from, &to).unwrap_or(false) { + maybe_report_identical_files(); + return ExitCode::SUCCESS; + } // read files let from_content = match fs::read(&from) { Ok(from_content) => from_content, @@ -71,6 +86,7 @@ fn main() -> ExitCode { }; io::stdout().write_all(&result).unwrap(); if result.is_empty() { + maybe_report_identical_files(); ExitCode::SUCCESS } else { ExitCode::from(1) diff --git a/src/params.rs b/src/params.rs index b118be7..66d3d56 100644 --- a/src/params.rs +++ b/src/params.rs @@ -25,6 +25,7 @@ pub struct Params { pub to: OsString, pub format: Format, pub context_count: usize, + pub report_identical_files: bool, } pub fn parse_params>(opts: I) -> Result { @@ -38,6 +39,7 @@ pub fn parse_params>(opts: I) -> Result>(opts: I) -> Result>(opts: I) -> Result Result<(), Box> { Ok(()) } +#[test] +fn no_differences_report_identical_files() -> Result<(), Box> { + // same file + let mut file1 = NamedTempFile::new()?; + file1.write_all("foo\n".as_bytes())?; + for option in ["", "-u", "-c", "-e"] { + let mut cmd = Command::cargo_bin("diffutils")?; + if !option.is_empty() { + cmd.arg(option); + } + cmd.arg("-s").arg(file1.path()).arg(file1.path()); + cmd.assert() + .code(predicate::eq(0)) + .success() + .stdout(predicate::eq(format!( + "Files {} and {} are identical\n", + file1.path().to_string_lossy(), + file1.path().to_string_lossy(), + ))); + } + // two files with the same content + let mut file2 = NamedTempFile::new()?; + file2.write_all("foo\n".as_bytes())?; + for option in ["", "-u", "-c", "-e"] { + let mut cmd = Command::cargo_bin("diffutils")?; + if !option.is_empty() { + cmd.arg(option); + } + cmd.arg("-s").arg(file1.path()).arg(file2.path()); + cmd.assert() + .code(predicate::eq(0)) + .success() + .stdout(predicate::eq(format!( + "Files {} and {} are identical\n", + file1.path().to_string_lossy(), + file2.path().to_string_lossy(), + ))); + } + Ok(()) +} + #[test] fn differences() -> Result<(), Box> { let mut file1 = NamedTempFile::new()?; From 62e10c6d6ca7984e4f10ecca2ea1ed06cc7f0324 Mon Sep 17 00:00:00 2001 From: hanbings Date: Tue, 19 Mar 2024 04:50:33 +0800 Subject: [PATCH 041/194] ci: fuzzers in the Github Actions CI. (#29) * Add fuzzing CI. * fuzz dependency change: diffutils -> diffutilslib * fix fuzz build. --- .github/workflows/fuzzing.yml | 72 ++++++++++++++++++++++++++++++++ fuzz/fuzz_targets/fuzz_ed.rs | 11 ++++- fuzz/fuzz_targets/fuzz_normal.rs | 2 +- fuzz/fuzz_targets/fuzz_patch.rs | 2 +- 4 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/fuzzing.yml diff --git a/.github/workflows/fuzzing.yml b/.github/workflows/fuzzing.yml new file mode 100644 index 0000000..589b952 --- /dev/null +++ b/.github/workflows/fuzzing.yml @@ -0,0 +1,72 @@ +name: Fuzzing + +# spell-checker:ignore fuzzer + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + fuzz-build: + name: Build the fuzzers + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@nightly + - name: Install `cargo-fuzz` + run: cargo install cargo-fuzz + - uses: Swatinem/rust-cache@v2 + with: + shared-key: "cargo-fuzz-cache-key" + cache-directories: "fuzz/target" + - name: Run `cargo-fuzz build` + run: cargo +nightly fuzz build + + fuzz-run: + needs: fuzz-build + name: Run the fuzzers + runs-on: ubuntu-latest + timeout-minutes: 5 + env: + RUN_FOR: 60 + strategy: + matrix: + test-target: + - { name: fuzz_ed, should_pass: true } + - { name: fuzz_normal, should_pass: true } + - { name: fuzz_patch, should_pass: true } + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@nightly + - name: Install `cargo-fuzz` + run: cargo install cargo-fuzz + - uses: Swatinem/rust-cache@v2 + with: + shared-key: "cargo-fuzz-cache-key" + cache-directories: "fuzz/target" + - name: Restore Cached Corpus + uses: actions/cache/restore@v4 + with: + key: corpus-cache-${{ matrix.test-target.name }} + path: | + fuzz/corpus/${{ matrix.test-target.name }} + - name: Run ${{ matrix.test-target.name }} for XX seconds + shell: bash + continue-on-error: ${{ !matrix.test-target.name.should_pass }} + run: | + cargo +nightly fuzz run ${{ matrix.test-target.name }} -- -max_total_time=${{ env.RUN_FOR }} -detect_leaks=0 + - name: Save Corpus Cache + uses: actions/cache/save@v4 + with: + key: corpus-cache-${{ matrix.test-target.name }} + path: | + fuzz/corpus/${{ matrix.test-target.name }} diff --git a/fuzz/fuzz_targets/fuzz_ed.rs b/fuzz/fuzz_targets/fuzz_ed.rs index e46908d..5c5132e 100644 --- a/fuzz/fuzz_targets/fuzz_ed.rs +++ b/fuzz/fuzz_targets/fuzz_ed.rs @@ -1,11 +1,18 @@ #![no_main] #[macro_use] extern crate libfuzzer_sys; -use diffutils::{ed_diff, normal_diff, unified_diff}; +use diffutilslib::ed_diff; +use diffutilslib::ed_diff::DiffError; use std::fs::{self, File}; use std::io::Write; use std::process::Command; +fn diff_w(expected: &[u8], actual: &[u8], filename: &str) -> Result, DiffError> { + let mut output = ed_diff::diff(expected, actual)?; + writeln!(&mut output, "w {filename}").unwrap(); + Ok(output) +} + fuzz_target!(|x: (Vec, Vec)| { let (mut from, mut to) = x; from.push(b'\n'); @@ -30,7 +37,7 @@ fuzz_target!(|x: (Vec, Vec)| { } else { return; } - let diff = ed_diff::diff_w(&from, &to, "target/fuzz.file").unwrap(); + let diff = diff_w(&from, &to, "target/fuzz.file").unwrap(); File::create("target/fuzz.file.original") .unwrap() .write_all(&from) diff --git a/fuzz/fuzz_targets/fuzz_normal.rs b/fuzz/fuzz_targets/fuzz_normal.rs index 4e114d2..a44ece3 100644 --- a/fuzz/fuzz_targets/fuzz_normal.rs +++ b/fuzz/fuzz_targets/fuzz_normal.rs @@ -1,7 +1,7 @@ #![no_main] #[macro_use] extern crate libfuzzer_sys; -use diffutils::{normal_diff, unified_diff}; +use diffutilslib::normal_diff; use std::fs::{self, File}; use std::io::Write; diff --git a/fuzz/fuzz_targets/fuzz_patch.rs b/fuzz/fuzz_targets/fuzz_patch.rs index c190d76..d353523 100644 --- a/fuzz/fuzz_targets/fuzz_patch.rs +++ b/fuzz/fuzz_targets/fuzz_patch.rs @@ -1,7 +1,7 @@ #![no_main] #[macro_use] extern crate libfuzzer_sys; -use diffutils::{normal_diff, unified_diff}; +use diffutilslib::unified_diff; use std::fs::{self, File}; use std::io::Write; use std::process::Command; From 4ed7ea1553c927845e39831a111bf76c081f0589 Mon Sep 17 00:00:00 2001 From: Olivier Tilloy Date: Tue, 19 Mar 2024 11:45:06 +0100 Subject: [PATCH 042/194] Implement -q/--brief option (fixes #19) (#20) * Implement -q/--brief option * Optimization: stop analyzing the files as soon as there are any differences * Unit tests for the stop_early parameter * Simplify checks --- src/context_diff.rs | 124 +++++++++++++++++++++++++++++++++++++---- src/ed_diff.rs | 38 +++++++++++-- src/main.rs | 17 +++++- src/normal_diff.rs | 44 ++++++++++++--- src/params.rs | 57 +++++++++++++++++++ src/unified_diff.rs | 130 ++++++++++++++++++++++++++++++++++++++----- tests/integration.rs | 24 ++++++++ 7 files changed, 394 insertions(+), 40 deletions(-) diff --git a/src/context_diff.rs b/src/context_diff.rs index 1c9d44f..abc0e9e 100644 --- a/src/context_diff.rs +++ b/src/context_diff.rs @@ -41,7 +41,12 @@ impl Mismatch { } // Produces a diff between the expected output and actual output. -fn make_diff(expected: &[u8], actual: &[u8], context_size: usize) -> Vec { +fn make_diff( + expected: &[u8], + actual: &[u8], + context_size: usize, + stop_early: bool, +) -> Vec { let mut line_number_expected = 1; let mut line_number_actual = 1; let mut context_queue: VecDeque<&[u8]> = VecDeque::with_capacity(context_size); @@ -191,6 +196,10 @@ fn make_diff(expected: &[u8], actual: &[u8], context_size: usize) -> Vec Vec { let mut output = format!("*** {expected_filename}\t\n--- {actual_filename}\t\n").into_bytes(); - let diff_results = make_diff(expected, actual, context_size); + let diff_results = make_diff(expected, actual, context_size, stop_early); if diff_results.is_empty() { return Vec::new(); - }; + } + if stop_early { + return output; + } for result in diff_results { let mut line_number_expected = result.line_number_expected; let mut line_number_actual = result.line_number_actual; @@ -404,8 +417,14 @@ mod tests { } // This test diff is intentionally reversed. // We want it to turn the alef into bet. - let diff = - diff(&alef, "a/alef", &bet, &format!("{target}/alef"), 2); + let diff = diff( + &alef, + "a/alef", + &bet, + &format!("{target}/alef"), + 2, + false, + ); File::create(&format!("{target}/ab.diff")) .unwrap() .write_all(&diff) @@ -477,8 +496,14 @@ mod tests { } // This test diff is intentionally reversed. // We want it to turn the alef into bet. - let diff = - diff(&alef, "a/alef_", &bet, &format!("{target}/alef_"), 2); + let diff = diff( + &alef, + "a/alef_", + &bet, + &format!("{target}/alef_"), + 2, + false, + ); File::create(&format!("{target}/ab_.diff")) .unwrap() .write_all(&diff) @@ -553,8 +578,14 @@ mod tests { }; // This test diff is intentionally reversed. // We want it to turn the alef into bet. - let diff = - diff(&alef, "a/alefx", &bet, &format!("{target}/alefx"), 2); + let diff = diff( + &alef, + "a/alefx", + &bet, + &format!("{target}/alefx"), + 2, + false, + ); File::create(&format!("{target}/abx.diff")) .unwrap() .write_all(&diff) @@ -632,8 +663,14 @@ mod tests { } // This test diff is intentionally reversed. // We want it to turn the alef into bet. - let diff = - diff(&alef, "a/alefr", &bet, &format!("{target}/alefr"), 2); + let diff = diff( + &alef, + "a/alefr", + &bet, + &format!("{target}/alefr"), + 2, + false, + ); File::create(&format!("{target}/abr.diff")) .unwrap() .write_all(&diff) @@ -662,4 +699,69 @@ mod tests { } } } + + #[test] + fn test_stop_early() { + let from_filename = "foo"; + let from = vec!["a", "b", "c", ""].join("\n"); + let to_filename = "bar"; + let to = vec!["a", "d", "c", ""].join("\n"); + let context_size: usize = 3; + + let diff_full = diff( + from.as_bytes(), + from_filename, + to.as_bytes(), + to_filename, + context_size, + false, + ); + let expected_full = vec![ + "*** foo\t", + "--- bar\t", + "***************", + "*** 1,3 ****", + " a", + "! b", + " c", + "--- 1,3 ----", + " a", + "! d", + " c", + "", + ] + .join("\n"); + assert_eq!(diff_full, expected_full.as_bytes()); + + let diff_brief = diff( + from.as_bytes(), + from_filename, + to.as_bytes(), + to_filename, + context_size, + true, + ); + let expected_brief = vec!["*** foo\t", "--- bar\t", ""].join("\n"); + assert_eq!(diff_brief, expected_brief.as_bytes()); + + let nodiff_full = diff( + from.as_bytes(), + from_filename, + from.as_bytes(), + to_filename, + context_size, + false, + ); + assert!(nodiff_full.is_empty()); + + let nodiff_brief = diff( + from.as_bytes(), + from_filename, + from.as_bytes(), + to_filename, + context_size, + true, + ); + assert!(nodiff_brief.is_empty()); + } } diff --git a/src/ed_diff.rs b/src/ed_diff.rs index 41a968a..eec1fb4 100644 --- a/src/ed_diff.rs +++ b/src/ed_diff.rs @@ -42,7 +42,7 @@ impl Mismatch { } // Produces a diff between the expected output and actual output. -fn make_diff(expected: &[u8], actual: &[u8]) -> Result, DiffError> { +fn make_diff(expected: &[u8], actual: &[u8], stop_early: bool) -> Result, DiffError> { let mut line_number_expected = 1; let mut line_number_actual = 1; let mut results = Vec::new(); @@ -94,6 +94,10 @@ fn make_diff(expected: &[u8], actual: &[u8]) -> Result, DiffError> } } } + if stop_early && !results.is_empty() { + // Optimization: stop analyzing the files as soon as there are any differences + return Ok(results); + } } if !mismatch.actual.is_empty() || !mismatch.expected.is_empty() { @@ -103,9 +107,13 @@ fn make_diff(expected: &[u8], actual: &[u8]) -> Result, DiffError> Ok(results) } -pub fn diff(expected: &[u8], actual: &[u8]) -> Result, DiffError> { +pub fn diff(expected: &[u8], actual: &[u8], stop_early: bool) -> Result, DiffError> { let mut output = Vec::new(); - let diff_results = make_diff(expected, actual)?; + let diff_results = make_diff(expected, actual, stop_early)?; + if stop_early && !diff_results.is_empty() { + write!(&mut output, "\0").unwrap(); + return Ok(output); + } let mut lines_offset = 0; for result in diff_results { let line_number_expected: isize = result.line_number_expected as isize + lines_offset; @@ -152,7 +160,7 @@ mod tests { use super::*; use pretty_assertions::assert_eq; pub fn diff_w(expected: &[u8], actual: &[u8], filename: &str) -> Result, DiffError> { - let mut output = diff(expected, actual)?; + let mut output = diff(expected, actual, false)?; writeln!(&mut output, "w {filename}").unwrap(); Ok(output) } @@ -161,7 +169,7 @@ mod tests { fn test_basic() { let from = b"a\n"; let to = b"b\n"; - let diff = diff(from, to).unwrap(); + let diff = diff(from, to, false).unwrap(); let expected = vec!["1c", "b", ".", ""].join("\n"); assert_eq!(diff, expected.as_bytes()); } @@ -390,4 +398,24 @@ mod tests { } } } + + #[test] + fn test_stop_early() { + let from = vec!["a", "b", "c", ""].join("\n"); + let to = vec!["a", "d", "c", ""].join("\n"); + + let diff_full = diff(from.as_bytes(), to.as_bytes(), false).unwrap(); + let expected_full = vec!["2c", "d", ".", ""].join("\n"); + assert_eq!(diff_full, expected_full.as_bytes()); + + let diff_brief = diff(from.as_bytes(), to.as_bytes(), true).unwrap(); + let expected_brief = "\0".as_bytes(); + assert_eq!(diff_brief, expected_brief); + + let nodiff_full = diff(from.as_bytes(), from.as_bytes(), false).unwrap(); + assert!(nodiff_full.is_empty()); + + let nodiff_brief = diff(from.as_bytes(), from.as_bytes(), true).unwrap(); + assert!(nodiff_brief.is_empty()); + } } diff --git a/src/main.rs b/src/main.rs index ef14096..6ff2a0f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -29,6 +29,7 @@ fn main() -> ExitCode { context_count, format, report_identical_files, + brief, } = parse_params(opts).unwrap_or_else(|error| { eprintln!("{error}"); exit(2); @@ -64,13 +65,14 @@ fn main() -> ExitCode { }; // run diff let result: Vec = match format { - Format::Normal => normal_diff::diff(&from_content, &to_content), + Format::Normal => normal_diff::diff(&from_content, &to_content, brief), Format::Unified => unified_diff::diff( &from_content, &from.to_string_lossy(), &to_content, &to.to_string_lossy(), context_count, + brief, ), Format::Context => context_diff::diff( &from_content, @@ -78,13 +80,22 @@ fn main() -> ExitCode { &to_content, &to.to_string_lossy(), context_count, + brief, ), - Format::Ed => ed_diff::diff(&from_content, &to_content).unwrap_or_else(|error| { + Format::Ed => ed_diff::diff(&from_content, &to_content, brief).unwrap_or_else(|error| { eprintln!("{error}"); exit(2); }), }; - io::stdout().write_all(&result).unwrap(); + if brief && !result.is_empty() { + println!( + "Files {} and {} differ", + from.to_string_lossy(), + to.to_string_lossy() + ); + } else { + io::stdout().write_all(&result).unwrap(); + } if result.is_empty() { maybe_report_identical_files(); ExitCode::SUCCESS diff --git a/src/normal_diff.rs b/src/normal_diff.rs index fa9c059..e324ada 100644 --- a/src/normal_diff.rs +++ b/src/normal_diff.rs @@ -29,7 +29,7 @@ impl Mismatch { } // Produces a diff between the expected output and actual output. -fn make_diff(expected: &[u8], actual: &[u8]) -> Vec { +fn make_diff(expected: &[u8], actual: &[u8], stop_early: bool) -> Vec { let mut line_number_expected = 1; let mut line_number_actual = 1; let mut results = Vec::new(); @@ -100,6 +100,10 @@ fn make_diff(expected: &[u8], actual: &[u8]) -> Vec { } } } + if stop_early && !results.is_empty() { + // Optimization: stop analyzing the files as soon as there are any differences + return results; + } } if !mismatch.actual.is_empty() || !mismatch.expected.is_empty() { @@ -110,11 +114,15 @@ fn make_diff(expected: &[u8], actual: &[u8]) -> Vec { } #[must_use] -pub fn diff(expected: &[u8], actual: &[u8]) -> Vec { +pub fn diff(expected: &[u8], actual: &[u8], stop_early: bool) -> Vec { // See https://www.gnu.org/software/diffutils/manual/html_node/Detailed-Normal.html // for details on the syntax of the normal format. let mut output = Vec::new(); - let diff_results = make_diff(expected, actual); + let diff_results = make_diff(expected, actual, stop_early); + if stop_early && !diff_results.is_empty() { + write!(&mut output, "\0").unwrap(); + return output; + } for result in diff_results { let line_number_expected = result.line_number_expected; let line_number_actual = result.line_number_actual; @@ -212,7 +220,7 @@ mod tests { a.write_all(b"a\n").unwrap(); let mut b = Vec::new(); b.write_all(b"b\n").unwrap(); - let diff = diff(&a, &b); + let diff = diff(&a, &b, false); let expected = b"1c1\n< a\n---\n> b\n".to_vec(); assert_eq!(diff, expected); } @@ -265,7 +273,7 @@ mod tests { } // This test diff is intentionally reversed. // We want it to turn the alef into bet. - let diff = diff(&alef, &bet); + let diff = diff(&alef, &bet, false); File::create(&format!("{target}/ab.diff")) .unwrap() .write_all(&diff) @@ -357,7 +365,7 @@ mod tests { } // This test diff is intentionally reversed. // We want it to turn the alef into bet. - let diff = diff(&alef, &bet); + let diff = diff(&alef, &bet, false); File::create(&format!("{target}/abn.diff")) .unwrap() .write_all(&diff) @@ -431,7 +439,7 @@ mod tests { } // This test diff is intentionally reversed. // We want it to turn the alef into bet. - let diff = diff(&alef, &bet); + let diff = diff(&alef, &bet, false); File::create(&format!("{target}/ab_.diff")) .unwrap() .write_all(&diff) @@ -509,7 +517,7 @@ mod tests { } // This test diff is intentionally reversed. // We want it to turn the alef into bet. - let diff = diff(&alef, &bet); + let diff = diff(&alef, &bet, false); File::create(&format!("{target}/abr.diff")) .unwrap() .write_all(&diff) @@ -538,4 +546,24 @@ mod tests { } } } + + #[test] + fn test_stop_early() { + let from = vec!["a", "b", "c"].join("\n"); + let to = vec!["a", "d", "c"].join("\n"); + + let diff_full = diff(from.as_bytes(), to.as_bytes(), false); + let expected_full = vec!["2c2", "< b", "---", "> d", ""].join("\n"); + assert_eq!(diff_full, expected_full.as_bytes()); + + let diff_brief = diff(from.as_bytes(), to.as_bytes(), true); + let expected_brief = "\0".as_bytes(); + assert_eq!(diff_brief, expected_brief); + + let nodiff_full = diff(from.as_bytes(), from.as_bytes(), false); + assert!(nodiff_full.is_empty()); + + let nodiff_brief = diff(from.as_bytes(), from.as_bytes(), true); + assert!(nodiff_brief.is_empty()); + } } diff --git a/src/params.rs b/src/params.rs index 66d3d56..661cd37 100644 --- a/src/params.rs +++ b/src/params.rs @@ -26,6 +26,7 @@ pub struct Params { pub format: Format, pub context_count: usize, pub report_identical_files: bool, + pub brief: bool, } pub fn parse_params>(opts: I) -> Result { @@ -40,6 +41,7 @@ pub fn parse_params>(opts: I) -> Result>(opts: I) -> Result>(opts: I) -> Result Vec { +fn make_diff( + expected: &[u8], + actual: &[u8], + context_size: usize, + stop_early: bool, +) -> Vec { let mut line_number_expected = 1; let mut line_number_actual = 1; let mut context_queue: VecDeque<&[u8]> = VecDeque::with_capacity(context_size); @@ -180,6 +185,10 @@ fn make_diff(expected: &[u8], actual: &[u8], context_size: usize) -> Vec Vec { let mut output = format!("--- {expected_filename}\t\n+++ {actual_filename}\t\n").into_bytes(); - let diff_results = make_diff(expected, actual, context_size); + let diff_results = make_diff(expected, actual, context_size, stop_early); if diff_results.is_empty() { return Vec::new(); - }; + } + if stop_early { + return output; + } for result in diff_results { let mut line_number_expected = result.line_number_expected; let mut line_number_actual = result.line_number_actual; @@ -434,8 +447,14 @@ mod tests { } // This test diff is intentionally reversed. // We want it to turn the alef into bet. - let diff = - diff(&alef, "a/alef", &bet, &format!("{target}/alef"), 2); + let diff = diff( + &alef, + "a/alef", + &bet, + &format!("{target}/alef"), + 2, + false, + ); File::create(&format!("{target}/ab.diff")) .unwrap() .write_all(&diff) @@ -542,8 +561,14 @@ mod tests { } // This test diff is intentionally reversed. // We want it to turn the alef into bet. - let diff = - diff(&alef, "a/alefn", &bet, &format!("{target}/alefn"), 2); + let diff = diff( + &alef, + "a/alefn", + &bet, + &format!("{target}/alefn"), + 2, + false, + ); File::create(&format!("{target}/abn.diff")) .unwrap() .write_all(&diff) @@ -630,8 +655,14 @@ mod tests { } // This test diff is intentionally reversed. // We want it to turn the alef into bet. - let diff = - diff(&alef, "a/alef_", &bet, &format!("{target}/alef_"), 2); + let diff = diff( + &alef, + "a/alef_", + &bet, + &format!("{target}/alef_"), + 2, + false, + ); File::create(&format!("{target}/ab_.diff")) .unwrap() .write_all(&diff) @@ -703,8 +734,14 @@ mod tests { } // This test diff is intentionally reversed. // We want it to turn the alef into bet. - let diff = - diff(&alef, "a/alefx", &bet, &format!("{target}/alefx"), 2); + let diff = diff( + &alef, + "a/alefx", + &bet, + &format!("{target}/alefx"), + 2, + false, + ); File::create(&format!("{target}/abx.diff")) .unwrap() .write_all(&diff) @@ -781,8 +818,14 @@ mod tests { } // This test diff is intentionally reversed. // We want it to turn the alef into bet. - let diff = - diff(&alef, "a/alefr", &bet, &format!("{target}/alefr"), 2); + let diff = diff( + &alef, + "a/alefr", + &bet, + &format!("{target}/alefr"), + 2, + false, + ); File::create(&format!("{target}/abr.diff")) .unwrap() .write_all(&diff) @@ -810,4 +853,65 @@ mod tests { } } } + + #[test] + fn test_stop_early() { + let from_filename = "foo"; + let from = vec!["a", "b", "c", ""].join("\n"); + let to_filename = "bar"; + let to = vec!["a", "d", "c", ""].join("\n"); + let context_size: usize = 3; + + let diff_full = diff( + from.as_bytes(), + from_filename, + to.as_bytes(), + to_filename, + context_size, + false, + ); + let expected_full = vec![ + "--- foo\t", + "+++ bar\t", + "@@ -1,3 +1,3 @@", + " a", + "-b", + "+d", + " c", + "", + ] + .join("\n"); + assert_eq!(diff_full, expected_full.as_bytes()); + + let diff_brief = diff( + from.as_bytes(), + from_filename, + to.as_bytes(), + to_filename, + context_size, + true, + ); + let expected_brief = vec!["--- foo\t", "+++ bar\t", ""].join("\n"); + assert_eq!(diff_brief, expected_brief.as_bytes()); + + let nodiff_full = diff( + from.as_bytes(), + from_filename, + from.as_bytes(), + to_filename, + context_size, + false, + ); + assert!(nodiff_full.is_empty()); + + let nodiff_brief = diff( + from.as_bytes(), + from_filename, + from.as_bytes(), + to_filename, + context_size, + true, + ); + assert!(nodiff_brief.is_empty()); + } } diff --git a/tests/integration.rs b/tests/integration.rs index 50bf559..be1320e 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -123,6 +123,30 @@ fn differences() -> Result<(), Box> { Ok(()) } +#[test] +fn differences_brief() -> Result<(), Box> { + let mut file1 = NamedTempFile::new()?; + file1.write_all("foo\n".as_bytes())?; + let mut file2 = NamedTempFile::new()?; + file2.write_all("bar\n".as_bytes())?; + for option in ["", "-u", "-c", "-e"] { + let mut cmd = Command::cargo_bin("diffutils")?; + if !option.is_empty() { + cmd.arg(option); + } + cmd.arg("-q").arg(file1.path()).arg(file2.path()); + cmd.assert() + .code(predicate::eq(1)) + .failure() + .stdout(predicate::eq(format!( + "Files {} and {} differ\n", + file1.path().to_string_lossy(), + file2.path().to_string_lossy() + ))); + } + Ok(()) +} + #[test] fn missing_newline() -> Result<(), Box> { let mut file1 = NamedTempFile::new()?; From f916f1ce86bed29228e0bd02176516651a6003eb Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Sun, 24 Mar 2024 14:05:44 +0100 Subject: [PATCH 043/194] clippy: fix warnings from useless_vec lint --- src/context_diff.rs | 8 ++++---- src/ed_diff.rs | 8 ++++---- src/normal_diff.rs | 6 +++--- src/unified_diff.rs | 8 ++++---- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/context_diff.rs b/src/context_diff.rs index abc0e9e..af262a3 100644 --- a/src/context_diff.rs +++ b/src/context_diff.rs @@ -703,9 +703,9 @@ mod tests { #[test] fn test_stop_early() { let from_filename = "foo"; - let from = vec!["a", "b", "c", ""].join("\n"); + let from = ["a", "b", "c", ""].join("\n"); let to_filename = "bar"; - let to = vec!["a", "d", "c", ""].join("\n"); + let to = ["a", "d", "c", ""].join("\n"); let context_size: usize = 3; let diff_full = diff( @@ -716,7 +716,7 @@ mod tests { context_size, false, ); - let expected_full = vec![ + let expected_full = [ "*** foo\t", "--- bar\t", "***************", @@ -741,7 +741,7 @@ mod tests { context_size, true, ); - let expected_brief = vec!["*** foo\t", "--- bar\t", ""].join("\n"); + let expected_brief = ["*** foo\t", "--- bar\t", ""].join("\n"); assert_eq!(diff_brief, expected_brief.as_bytes()); let nodiff_full = diff( diff --git a/src/ed_diff.rs b/src/ed_diff.rs index eec1fb4..7613b22 100644 --- a/src/ed_diff.rs +++ b/src/ed_diff.rs @@ -170,7 +170,7 @@ mod tests { let from = b"a\n"; let to = b"b\n"; let diff = diff(from, to, false).unwrap(); - let expected = vec!["1c", "b", ".", ""].join("\n"); + let expected = ["1c", "b", ".", ""].join("\n"); assert_eq!(diff, expected.as_bytes()); } @@ -401,11 +401,11 @@ mod tests { #[test] fn test_stop_early() { - let from = vec!["a", "b", "c", ""].join("\n"); - let to = vec!["a", "d", "c", ""].join("\n"); + let from = ["a", "b", "c", ""].join("\n"); + let to = ["a", "d", "c", ""].join("\n"); let diff_full = diff(from.as_bytes(), to.as_bytes(), false).unwrap(); - let expected_full = vec!["2c", "d", ".", ""].join("\n"); + let expected_full = ["2c", "d", ".", ""].join("\n"); assert_eq!(diff_full, expected_full.as_bytes()); let diff_brief = diff(from.as_bytes(), to.as_bytes(), true).unwrap(); diff --git a/src/normal_diff.rs b/src/normal_diff.rs index e324ada..aeef145 100644 --- a/src/normal_diff.rs +++ b/src/normal_diff.rs @@ -549,11 +549,11 @@ mod tests { #[test] fn test_stop_early() { - let from = vec!["a", "b", "c"].join("\n"); - let to = vec!["a", "d", "c"].join("\n"); + let from = ["a", "b", "c"].join("\n"); + let to = ["a", "d", "c"].join("\n"); let diff_full = diff(from.as_bytes(), to.as_bytes(), false); - let expected_full = vec!["2c2", "< b", "---", "> d", ""].join("\n"); + let expected_full = ["2c2", "< b", "---", "> d", ""].join("\n"); assert_eq!(diff_full, expected_full.as_bytes()); let diff_brief = diff(from.as_bytes(), to.as_bytes(), true); diff --git a/src/unified_diff.rs b/src/unified_diff.rs index becf721..f9c256e 100644 --- a/src/unified_diff.rs +++ b/src/unified_diff.rs @@ -857,9 +857,9 @@ mod tests { #[test] fn test_stop_early() { let from_filename = "foo"; - let from = vec!["a", "b", "c", ""].join("\n"); + let from = ["a", "b", "c", ""].join("\n"); let to_filename = "bar"; - let to = vec!["a", "d", "c", ""].join("\n"); + let to = ["a", "d", "c", ""].join("\n"); let context_size: usize = 3; let diff_full = diff( @@ -870,7 +870,7 @@ mod tests { context_size, false, ); - let expected_full = vec![ + let expected_full = [ "--- foo\t", "+++ bar\t", "@@ -1,3 +1,3 @@", @@ -891,7 +891,7 @@ mod tests { context_size, true, ); - let expected_brief = vec!["--- foo\t", "+++ bar\t", ""].join("\n"); + let expected_brief = ["--- foo\t", "+++ bar\t", ""].join("\n"); assert_eq!(diff_brief, expected_brief.as_bytes()); let nodiff_full = diff( From 42eb15b87ad39f97bf016e23631eab9550d559c7 Mon Sep 17 00:00:00 2001 From: Tanmay Patil Date: Wed, 27 Mar 2024 22:46:23 +0530 Subject: [PATCH 044/194] Display modification times of input files in context diff Fixes #31 --- Cargo.lock | 155 ++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/context_diff.rs | 20 +++++- 3 files changed, 175 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 92ad218..6235103 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstyle" version = "1.0.6" @@ -55,12 +70,44 @@ dependencies = [ "serde", ] +[[package]] +name = "bumpalo" +version = "3.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" + +[[package]] +name = "cc" +version = "1.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + [[package]] name = "diff" version = "0.1.13" @@ -78,6 +125,7 @@ name = "diffutils" version = "0.3.0" dependencies = [ "assert_cmd", + "chrono", "diff", "predicates", "pretty_assertions", @@ -116,6 +164,38 @@ dependencies = [ "num-traits", ] +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "libc" version = "0.2.153" @@ -128,6 +208,12 @@ version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + [[package]] name = "memchr" version = "2.7.1" @@ -149,6 +235,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + [[package]] name = "predicates" version = "3.1.0" @@ -322,6 +414,60 @@ dependencies = [ "libc", ] +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + [[package]] name = "winapi" version = "0.3.9" @@ -353,6 +499,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index 3804a81..940e3d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ name = "diffutils" path = "src/main.rs" [dependencies] +chrono = "0.4.35" diff = "0.1.10" same-file = "1.0.6" diff --git a/src/context_diff.rs b/src/context_diff.rs index abc0e9e..fa09f7c 100644 --- a/src/context_diff.rs +++ b/src/context_diff.rs @@ -262,6 +262,22 @@ fn make_diff( results } +fn get_modification_time(file_path: &str) -> String { + use chrono::{DateTime, Local}; + use std::fs; + + let metadata = fs::metadata(file_path).expect("Failed to get metadata"); + let modification_time = metadata + .modified() + .expect("Failed to get modification time"); + let modification_time: DateTime = modification_time.into(); + let modification_time: String = modification_time + .format("%Y-%m-%d %H:%M:%S%.9f %z") + .to_string(); + + modification_time +} + #[must_use] pub fn diff( expected: &[u8], @@ -271,7 +287,9 @@ pub fn diff( context_size: usize, stop_early: bool, ) -> Vec { - let mut output = format!("*** {expected_filename}\t\n--- {actual_filename}\t\n").into_bytes(); + let expected_file_modified_time = get_modification_time(expected_filename); + let actual_file_modified_time = get_modification_time(actual_filename); + let mut output = format!("*** {expected_filename}\t{expected_file_modified_time}\n--- {actual_filename}\t{actual_file_modified_time}\n").into_bytes(); let diff_results = make_diff(expected, actual, context_size, stop_early); if diff_results.is_empty() { return Vec::new(); From 8d65c2baddac1226cc6d172acf3998b24d47e722 Mon Sep 17 00:00:00 2001 From: Olivier Tilloy Date: Mon, 4 Mar 2024 21:41:27 +0100 Subject: [PATCH 045/194] Implement -t/--expand-tabs option --- Cargo.lock | 7 +++ Cargo.toml | 1 + src/context_diff.rs | 29 +++++++++--- src/ed_diff.rs | 23 +++++---- src/lib.rs | 1 + src/main.rs | 16 +++++-- src/normal_diff.rs | 26 +++++----- src/params.rs | 53 +++++++++++++++++++++ src/unified_diff.rs | 21 +++++++-- src/utils.rs | 112 ++++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 255 insertions(+), 34 deletions(-) create mode 100644 src/utils.rs diff --git a/Cargo.lock b/Cargo.lock index 92ad218..9dc8153 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -83,6 +83,7 @@ dependencies = [ "pretty_assertions", "same-file", "tempfile", + "unicode-width", ] [[package]] @@ -313,6 +314,12 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + [[package]] name = "wait-timeout" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 3804a81..7eeb35b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ path = "src/main.rs" [dependencies] diff = "0.1.10" same-file = "1.0.6" +unicode-width = "0.1.11" [dev-dependencies] pretty_assertions = "1" diff --git a/src/context_diff.rs b/src/context_diff.rs index af262a3..408821f 100644 --- a/src/context_diff.rs +++ b/src/context_diff.rs @@ -6,6 +6,8 @@ use std::collections::VecDeque; use std::io::Write; +use crate::utils::do_write_line; + #[derive(Debug, PartialEq)] pub enum DiffLine { Context(Vec), @@ -270,6 +272,7 @@ pub fn diff( actual_filename: &str, context_size: usize, stop_early: bool, + expand_tabs: bool, ) -> Vec { let mut output = format!("*** {expected_filename}\t\n--- {actual_filename}\t\n").into_bytes(); let diff_results = make_diff(expected, actual, context_size, stop_early); @@ -314,17 +317,20 @@ pub fn diff( match line { DiffLine::Context(e) => { write!(output, " ").expect("write to Vec is infallible"); - output.write_all(&e).expect("write to Vec is infallible"); + do_write_line(&mut output, &e, expand_tabs) + .expect("write to Vec is infallible"); writeln!(output).unwrap(); } DiffLine::Change(e) => { write!(output, "! ").expect("write to Vec is infallible"); - output.write_all(&e).expect("write to Vec is infallible"); + do_write_line(&mut output, &e, expand_tabs) + .expect("write to Vec is infallible"); writeln!(output).unwrap(); } DiffLine::Add(e) => { write!(output, "- ").expect("write to Vec is infallible"); - output.write_all(&e).expect("write to Vec is infallible"); + do_write_line(&mut output, &e, expand_tabs) + .expect("write to Vec is infallible"); writeln!(output).unwrap(); } } @@ -341,17 +347,20 @@ pub fn diff( match line { DiffLine::Context(e) => { write!(output, " ").expect("write to Vec is infallible"); - output.write_all(&e).expect("write to Vec is infallible"); + do_write_line(&mut output, &e, expand_tabs) + .expect("write to Vec is infallible"); writeln!(output).unwrap(); } DiffLine::Change(e) => { write!(output, "! ").expect("write to Vec is infallible"); - output.write_all(&e).expect("write to Vec is infallible"); + do_write_line(&mut output, &e, expand_tabs) + .expect("write to Vec is infallible"); writeln!(output).unwrap(); } DiffLine::Add(e) => { write!(output, "+ ").expect("write to Vec is infallible"); - output.write_all(&e).expect("write to Vec is infallible"); + do_write_line(&mut output, &e, expand_tabs) + .expect("write to Vec is infallible"); writeln!(output).unwrap(); } } @@ -424,6 +433,7 @@ mod tests { &format!("{target}/alef"), 2, false, + false, ); File::create(&format!("{target}/ab.diff")) .unwrap() @@ -503,6 +513,7 @@ mod tests { &format!("{target}/alef_"), 2, false, + false, ); File::create(&format!("{target}/ab_.diff")) .unwrap() @@ -585,6 +596,7 @@ mod tests { &format!("{target}/alefx"), 2, false, + false, ); File::create(&format!("{target}/abx.diff")) .unwrap() @@ -670,6 +682,7 @@ mod tests { &format!("{target}/alefr"), 2, false, + false, ); File::create(&format!("{target}/abr.diff")) .unwrap() @@ -715,6 +728,7 @@ mod tests { to_filename, context_size, false, + false, ); let expected_full = [ "*** foo\t", @@ -740,6 +754,7 @@ mod tests { to_filename, context_size, true, + false, ); let expected_brief = ["*** foo\t", "--- bar\t", ""].join("\n"); assert_eq!(diff_brief, expected_brief.as_bytes()); @@ -751,6 +766,7 @@ mod tests { to_filename, context_size, false, + false, ); assert!(nodiff_full.is_empty()); @@ -761,6 +777,7 @@ mod tests { to_filename, context_size, true, + false, ); assert!(nodiff_brief.is_empty()); } diff --git a/src/ed_diff.rs b/src/ed_diff.rs index 7613b22..6d47b9f 100644 --- a/src/ed_diff.rs +++ b/src/ed_diff.rs @@ -5,6 +5,8 @@ use std::io::Write; +use crate::utils::do_write_line; + #[derive(Debug, PartialEq)] struct Mismatch { pub line_number_expected: usize, @@ -107,7 +109,12 @@ fn make_diff(expected: &[u8], actual: &[u8], stop_early: bool) -> Result Result, DiffError> { +pub fn diff( + expected: &[u8], + actual: &[u8], + stop_early: bool, + expand_tabs: bool, +) -> Result, DiffError> { let mut output = Vec::new(); let diff_results = make_diff(expected, actual, stop_early)?; if stop_early && !diff_results.is_empty() { @@ -145,7 +152,7 @@ pub fn diff(expected: &[u8], actual: &[u8], stop_early: bool) -> Result, if actual == b"." { writeln!(&mut output, "..\n.\ns/.//\na").unwrap(); } else { - output.write_all(actual).unwrap(); + do_write_line(&mut output, actual, expand_tabs).unwrap(); writeln!(&mut output).unwrap(); } } @@ -160,7 +167,7 @@ mod tests { use super::*; use pretty_assertions::assert_eq; pub fn diff_w(expected: &[u8], actual: &[u8], filename: &str) -> Result, DiffError> { - let mut output = diff(expected, actual, false)?; + let mut output = diff(expected, actual, false, false)?; writeln!(&mut output, "w {filename}").unwrap(); Ok(output) } @@ -169,7 +176,7 @@ mod tests { fn test_basic() { let from = b"a\n"; let to = b"b\n"; - let diff = diff(from, to, false).unwrap(); + let diff = diff(from, to, false, false).unwrap(); let expected = ["1c", "b", ".", ""].join("\n"); assert_eq!(diff, expected.as_bytes()); } @@ -404,18 +411,18 @@ mod tests { let from = ["a", "b", "c", ""].join("\n"); let to = ["a", "d", "c", ""].join("\n"); - let diff_full = diff(from.as_bytes(), to.as_bytes(), false).unwrap(); + let diff_full = diff(from.as_bytes(), to.as_bytes(), false, false).unwrap(); let expected_full = ["2c", "d", ".", ""].join("\n"); assert_eq!(diff_full, expected_full.as_bytes()); - let diff_brief = diff(from.as_bytes(), to.as_bytes(), true).unwrap(); + let diff_brief = diff(from.as_bytes(), to.as_bytes(), true, false).unwrap(); let expected_brief = "\0".as_bytes(); assert_eq!(diff_brief, expected_brief); - let nodiff_full = diff(from.as_bytes(), from.as_bytes(), false).unwrap(); + let nodiff_full = diff(from.as_bytes(), from.as_bytes(), false, false).unwrap(); assert!(nodiff_full.is_empty()); - let nodiff_brief = diff(from.as_bytes(), from.as_bytes(), true).unwrap(); + let nodiff_brief = diff(from.as_bytes(), from.as_bytes(), true, false).unwrap(); assert!(nodiff_brief.is_empty()); } } diff --git a/src/lib.rs b/src/lib.rs index a78b64d..faf5df2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ pub mod context_diff; pub mod ed_diff; pub mod normal_diff; pub mod unified_diff; +pub mod utils; // Re-export the public functions/types you need pub use context_diff::diff as context_diff; diff --git a/src/main.rs b/src/main.rs index 6ff2a0f..f074cb1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,6 +15,7 @@ mod ed_diff; mod normal_diff; mod params; mod unified_diff; +mod utils; // Exit codes are documented at // https://www.gnu.org/software/diffutils/manual/html_node/Invoking-diff.html. @@ -30,6 +31,7 @@ fn main() -> ExitCode { format, report_identical_files, brief, + expand_tabs, } = parse_params(opts).unwrap_or_else(|error| { eprintln!("{error}"); exit(2); @@ -65,7 +67,7 @@ fn main() -> ExitCode { }; // run diff let result: Vec = match format { - Format::Normal => normal_diff::diff(&from_content, &to_content, brief), + Format::Normal => normal_diff::diff(&from_content, &to_content, brief, expand_tabs), Format::Unified => unified_diff::diff( &from_content, &from.to_string_lossy(), @@ -73,6 +75,7 @@ fn main() -> ExitCode { &to.to_string_lossy(), context_count, brief, + expand_tabs, ), Format::Context => context_diff::diff( &from_content, @@ -81,11 +84,14 @@ fn main() -> ExitCode { &to.to_string_lossy(), context_count, brief, + expand_tabs, ), - Format::Ed => ed_diff::diff(&from_content, &to_content, brief).unwrap_or_else(|error| { - eprintln!("{error}"); - exit(2); - }), + Format::Ed => { + ed_diff::diff(&from_content, &to_content, brief, expand_tabs).unwrap_or_else(|error| { + eprintln!("{error}"); + exit(2); + }) + } }; if brief && !result.is_empty() { println!( diff --git a/src/normal_diff.rs b/src/normal_diff.rs index aeef145..e25a6c6 100644 --- a/src/normal_diff.rs +++ b/src/normal_diff.rs @@ -5,6 +5,8 @@ use std::io::Write; +use crate::utils::do_write_line; + #[derive(Debug, PartialEq)] struct Mismatch { pub line_number_expected: usize, @@ -114,7 +116,7 @@ fn make_diff(expected: &[u8], actual: &[u8], stop_early: bool) -> Vec } #[must_use] -pub fn diff(expected: &[u8], actual: &[u8], stop_early: bool) -> Vec { +pub fn diff(expected: &[u8], actual: &[u8], stop_early: bool, expand_tabs: bool) -> Vec { // See https://www.gnu.org/software/diffutils/manual/html_node/Detailed-Normal.html // for details on the syntax of the normal format. let mut output = Vec::new(); @@ -188,7 +190,7 @@ pub fn diff(expected: &[u8], actual: &[u8], stop_early: bool) -> Vec { } for expected in &result.expected { write!(&mut output, "< ").unwrap(); - output.write_all(expected).unwrap(); + do_write_line(&mut output, expected, expand_tabs).unwrap(); writeln!(&mut output).unwrap(); } if result.expected_missing_nl { @@ -199,7 +201,7 @@ pub fn diff(expected: &[u8], actual: &[u8], stop_early: bool) -> Vec { } for actual in &result.actual { write!(&mut output, "> ").unwrap(); - output.write_all(actual).unwrap(); + do_write_line(&mut output, actual, expand_tabs).unwrap(); writeln!(&mut output).unwrap(); } if result.actual_missing_nl { @@ -220,7 +222,7 @@ mod tests { a.write_all(b"a\n").unwrap(); let mut b = Vec::new(); b.write_all(b"b\n").unwrap(); - let diff = diff(&a, &b, false); + let diff = diff(&a, &b, false, false); let expected = b"1c1\n< a\n---\n> b\n".to_vec(); assert_eq!(diff, expected); } @@ -273,7 +275,7 @@ mod tests { } // This test diff is intentionally reversed. // We want it to turn the alef into bet. - let diff = diff(&alef, &bet, false); + let diff = diff(&alef, &bet, false, false); File::create(&format!("{target}/ab.diff")) .unwrap() .write_all(&diff) @@ -365,7 +367,7 @@ mod tests { } // This test diff is intentionally reversed. // We want it to turn the alef into bet. - let diff = diff(&alef, &bet, false); + let diff = diff(&alef, &bet, false, false); File::create(&format!("{target}/abn.diff")) .unwrap() .write_all(&diff) @@ -439,7 +441,7 @@ mod tests { } // This test diff is intentionally reversed. // We want it to turn the alef into bet. - let diff = diff(&alef, &bet, false); + let diff = diff(&alef, &bet, false, false); File::create(&format!("{target}/ab_.diff")) .unwrap() .write_all(&diff) @@ -517,7 +519,7 @@ mod tests { } // This test diff is intentionally reversed. // We want it to turn the alef into bet. - let diff = diff(&alef, &bet, false); + let diff = diff(&alef, &bet, false, false); File::create(&format!("{target}/abr.diff")) .unwrap() .write_all(&diff) @@ -552,18 +554,18 @@ mod tests { let from = ["a", "b", "c"].join("\n"); let to = ["a", "d", "c"].join("\n"); - let diff_full = diff(from.as_bytes(), to.as_bytes(), false); + let diff_full = diff(from.as_bytes(), to.as_bytes(), false, false); let expected_full = ["2c2", "< b", "---", "> d", ""].join("\n"); assert_eq!(diff_full, expected_full.as_bytes()); - let diff_brief = diff(from.as_bytes(), to.as_bytes(), true); + let diff_brief = diff(from.as_bytes(), to.as_bytes(), true, false); let expected_brief = "\0".as_bytes(); assert_eq!(diff_brief, expected_brief); - let nodiff_full = diff(from.as_bytes(), from.as_bytes(), false); + let nodiff_full = diff(from.as_bytes(), from.as_bytes(), false, false); assert!(nodiff_full.is_empty()); - let nodiff_brief = diff(from.as_bytes(), from.as_bytes(), true); + let nodiff_brief = diff(from.as_bytes(), from.as_bytes(), true, false); assert!(nodiff_brief.is_empty()); } } diff --git a/src/params.rs b/src/params.rs index 661cd37..a576f3d 100644 --- a/src/params.rs +++ b/src/params.rs @@ -27,6 +27,7 @@ pub struct Params { pub context_count: usize, pub report_identical_files: bool, pub brief: bool, + pub expand_tabs: bool, } pub fn parse_params>(opts: I) -> Result { @@ -42,6 +43,7 @@ pub fn parse_params>(opts: I) -> Result>(opts: I) -> Result>(opts: I) -> Result), @@ -241,6 +243,7 @@ pub fn diff( actual_filename: &str, context_size: usize, stop_early: bool, + expand_tabs: bool, ) -> Vec { let mut output = format!("--- {expected_filename}\t\n+++ {actual_filename}\t\n").into_bytes(); let diff_results = make_diff(expected, actual, context_size, stop_early); @@ -371,17 +374,20 @@ pub fn diff( match line { DiffLine::Expected(e) => { write!(output, "-").expect("write to Vec is infallible"); - output.write_all(&e).expect("write to Vec is infallible"); + do_write_line(&mut output, &e, expand_tabs) + .expect("write to Vec is infallible"); writeln!(output).unwrap(); } DiffLine::Context(c) => { write!(output, " ").expect("write to Vec is infallible"); - output.write_all(&c).expect("write to Vec is infallible"); + do_write_line(&mut output, &c, expand_tabs) + .expect("write to Vec is infallible"); writeln!(output).unwrap(); } DiffLine::Actual(r) => { write!(output, "+",).expect("write to Vec is infallible"); - output.write_all(&r).expect("write to Vec is infallible"); + do_write_line(&mut output, &r, expand_tabs) + .expect("write to Vec is infallible"); writeln!(output).unwrap(); } DiffLine::MissingNL => { @@ -454,6 +460,7 @@ mod tests { &format!("{target}/alef"), 2, false, + false, ); File::create(&format!("{target}/ab.diff")) .unwrap() @@ -568,6 +575,7 @@ mod tests { &format!("{target}/alefn"), 2, false, + false, ); File::create(&format!("{target}/abn.diff")) .unwrap() @@ -662,6 +670,7 @@ mod tests { &format!("{target}/alef_"), 2, false, + false, ); File::create(&format!("{target}/ab_.diff")) .unwrap() @@ -741,6 +750,7 @@ mod tests { &format!("{target}/alefx"), 2, false, + false, ); File::create(&format!("{target}/abx.diff")) .unwrap() @@ -825,6 +835,7 @@ mod tests { &format!("{target}/alefr"), 2, false, + false, ); File::create(&format!("{target}/abr.diff")) .unwrap() @@ -869,6 +880,7 @@ mod tests { to_filename, context_size, false, + false, ); let expected_full = [ "--- foo\t", @@ -890,6 +902,7 @@ mod tests { to_filename, context_size, true, + false, ); let expected_brief = ["--- foo\t", "+++ bar\t", ""].join("\n"); assert_eq!(diff_brief, expected_brief.as_bytes()); @@ -901,6 +914,7 @@ mod tests { to_filename, context_size, false, + false, ); assert!(nodiff_full.is_empty()); @@ -911,6 +925,7 @@ mod tests { to_filename, context_size, true, + false, ); assert!(nodiff_brief.is_empty()); } diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..1d13682 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,112 @@ +// This file is part of the uutils diffutils package. +// +// For the full copyright and license information, please view the LICENSE-* +// files that was distributed with this source code. + +use std::io::Write; + +use unicode_width::UnicodeWidthStr; + +/// Replace tabs by spaces in the input line. +/// Correctly handle multi-bytes characters. +/// This assumes that line does not contain any line breaks (if it does, the result is undefined). +pub fn do_expand_tabs(line: &[u8], tabsize: usize) -> Vec { + let tab = b'\t'; + let ntabs = line.iter().filter(|c| **c == tab).count(); + if ntabs == 0 { + return line.to_vec(); + } + let mut result = Vec::with_capacity(line.len() + ntabs * (tabsize - 1)); + let mut offset = 0; + + let mut iter = line.split(|c| *c == tab).peekable(); + while let Some(chunk) = iter.next() { + match String::from_utf8(chunk.to_vec()) { + Ok(s) => offset += UnicodeWidthStr::width(s.as_str()), + Err(_) => offset += chunk.len(), + } + result.extend_from_slice(chunk); + if iter.peek().is_some() { + result.resize(result.len() + tabsize - offset % tabsize, b' '); + offset = 0; + } + } + + result +} + +/// Write a single line to an output stream, expanding tabs to space if necessary. +/// This assumes that line does not contain any line breaks +/// (if it does and tabs are to be expanded to spaces, the result is undefined). +pub fn do_write_line(output: &mut Vec, line: &[u8], expand_tabs: bool) -> std::io::Result<()> { + if expand_tabs { + output.write_all(do_expand_tabs(line, 8).as_slice()) + } else { + output.write_all(line) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + mod expand_tabs { + use super::*; + use pretty_assertions::assert_eq; + + fn assert_tab_expansion(line: &str, tabsize: usize, expected: &str) { + assert_eq!( + do_expand_tabs(line.as_bytes(), tabsize), + expected.as_bytes() + ); + } + + #[test] + fn basics() { + assert_tab_expansion("foo barr baz", 8, "foo barr baz"); + assert_tab_expansion("foo\tbarr\tbaz", 8, "foo barr baz"); + assert_tab_expansion("foo\tbarr\tbaz", 5, "foo barr baz"); + assert_tab_expansion("foo\tbarr\tbaz", 2, "foo barr baz"); + } + + #[test] + fn multibyte_chars() { + assert_tab_expansion("foo\tépée\tbaz", 8, "foo épée baz"); + assert_tab_expansion("foo\t😉\tbaz", 5, "foo 😉 baz"); + + // Note: The Woman Scientist emoji (👩‍🔬) is a ZWJ sequence combining + // the Woman emoji (👩) and the Microscope emoji (🔬). On supported platforms + // it is displayed as a single emoji and should have a print size of 2 columns, + // but terminal emulators tend to not support this, and display the two emojis + // side by side, thus accounting for a print size of 4 columns. + assert_tab_expansion("foo\t👩‍🔬\tbaz", 6, "foo 👩‍🔬 baz"); + } + + #[test] + fn invalid_utf8() { + // [240, 240, 152, 137] is an invalid UTF-8 sequence, so it is handled as 4 bytes + assert_eq!( + do_expand_tabs(&[240, 240, 152, 137, 9, 102, 111, 111], 8), + &[240, 240, 152, 137, 32, 32, 32, 32, 102, 111, 111] + ); + } + } + + mod write_line { + use super::*; + use pretty_assertions::assert_eq; + + fn assert_line_written(line: &str, expand_tabs: bool, expected: &str) { + let mut output: Vec = Vec::new(); + assert!(do_write_line(&mut output, line.as_bytes(), expand_tabs).is_ok()); + assert_eq!(output, expected.as_bytes()); + } + + #[test] + fn basics() { + assert_line_written("foo bar baz", false, "foo bar baz"); + assert_line_written("foo bar\tbaz", false, "foo bar\tbaz"); + assert_line_written("foo bar\tbaz", true, "foo bar baz"); + } + } +} From e0283083f297cd8f9f6e39e6f24cd18ab18e8b02 Mon Sep 17 00:00:00 2001 From: Olivier Tilloy Date: Tue, 5 Mar 2024 18:52:04 +0100 Subject: [PATCH 046/194] Implement --tabsize option --- Cargo.lock | 1 + Cargo.toml | 1 + src/context_diff.rs | 21 +++++-- src/ed_diff.rs | 15 ++--- src/main.rs | 14 +++-- src/normal_diff.rs | 30 ++++++---- src/params.rs | 130 ++++++++++++++++++++++++++++++++++++++++++++ src/unified_diff.rs | 16 +++++- src/utils.rs | 19 ++++--- 9 files changed, 207 insertions(+), 40 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9dc8153..15c81e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -81,6 +81,7 @@ dependencies = [ "diff", "predicates", "pretty_assertions", + "regex", "same-file", "tempfile", "unicode-width", diff --git a/Cargo.toml b/Cargo.toml index 7eeb35b..4ddf5fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ path = "src/main.rs" [dependencies] diff = "0.1.10" +regex = "1.10.3" same-file = "1.0.6" unicode-width = "0.1.11" diff --git a/src/context_diff.rs b/src/context_diff.rs index 408821f..9f1db55 100644 --- a/src/context_diff.rs +++ b/src/context_diff.rs @@ -273,6 +273,7 @@ pub fn diff( context_size: usize, stop_early: bool, expand_tabs: bool, + tabsize: usize, ) -> Vec { let mut output = format!("*** {expected_filename}\t\n--- {actual_filename}\t\n").into_bytes(); let diff_results = make_diff(expected, actual, context_size, stop_early); @@ -317,19 +318,19 @@ pub fn diff( match line { DiffLine::Context(e) => { write!(output, " ").expect("write to Vec is infallible"); - do_write_line(&mut output, &e, expand_tabs) + do_write_line(&mut output, &e, expand_tabs, tabsize) .expect("write to Vec is infallible"); writeln!(output).unwrap(); } DiffLine::Change(e) => { write!(output, "! ").expect("write to Vec is infallible"); - do_write_line(&mut output, &e, expand_tabs) + do_write_line(&mut output, &e, expand_tabs, tabsize) .expect("write to Vec is infallible"); writeln!(output).unwrap(); } DiffLine::Add(e) => { write!(output, "- ").expect("write to Vec is infallible"); - do_write_line(&mut output, &e, expand_tabs) + do_write_line(&mut output, &e, expand_tabs, tabsize) .expect("write to Vec is infallible"); writeln!(output).unwrap(); } @@ -347,19 +348,19 @@ pub fn diff( match line { DiffLine::Context(e) => { write!(output, " ").expect("write to Vec is infallible"); - do_write_line(&mut output, &e, expand_tabs) + do_write_line(&mut output, &e, expand_tabs, tabsize) .expect("write to Vec is infallible"); writeln!(output).unwrap(); } DiffLine::Change(e) => { write!(output, "! ").expect("write to Vec is infallible"); - do_write_line(&mut output, &e, expand_tabs) + do_write_line(&mut output, &e, expand_tabs, tabsize) .expect("write to Vec is infallible"); writeln!(output).unwrap(); } DiffLine::Add(e) => { write!(output, "+ ").expect("write to Vec is infallible"); - do_write_line(&mut output, &e, expand_tabs) + do_write_line(&mut output, &e, expand_tabs, tabsize) .expect("write to Vec is infallible"); writeln!(output).unwrap(); } @@ -434,6 +435,7 @@ mod tests { 2, false, false, + 8, ); File::create(&format!("{target}/ab.diff")) .unwrap() @@ -514,6 +516,7 @@ mod tests { 2, false, false, + 8, ); File::create(&format!("{target}/ab_.diff")) .unwrap() @@ -597,6 +600,7 @@ mod tests { 2, false, false, + 8, ); File::create(&format!("{target}/abx.diff")) .unwrap() @@ -683,6 +687,7 @@ mod tests { 2, false, false, + 8, ); File::create(&format!("{target}/abr.diff")) .unwrap() @@ -729,6 +734,7 @@ mod tests { context_size, false, false, + 8, ); let expected_full = [ "*** foo\t", @@ -755,6 +761,7 @@ mod tests { context_size, true, false, + 8, ); let expected_brief = ["*** foo\t", "--- bar\t", ""].join("\n"); assert_eq!(diff_brief, expected_brief.as_bytes()); @@ -767,6 +774,7 @@ mod tests { context_size, false, false, + 8, ); assert!(nodiff_full.is_empty()); @@ -778,6 +786,7 @@ mod tests { context_size, true, false, + 8, ); assert!(nodiff_brief.is_empty()); } diff --git a/src/ed_diff.rs b/src/ed_diff.rs index 6d47b9f..c02289c 100644 --- a/src/ed_diff.rs +++ b/src/ed_diff.rs @@ -114,6 +114,7 @@ pub fn diff( actual: &[u8], stop_early: bool, expand_tabs: bool, + tabsize: usize, ) -> Result, DiffError> { let mut output = Vec::new(); let diff_results = make_diff(expected, actual, stop_early)?; @@ -152,7 +153,7 @@ pub fn diff( if actual == b"." { writeln!(&mut output, "..\n.\ns/.//\na").unwrap(); } else { - do_write_line(&mut output, actual, expand_tabs).unwrap(); + do_write_line(&mut output, actual, expand_tabs, tabsize).unwrap(); writeln!(&mut output).unwrap(); } } @@ -167,7 +168,7 @@ mod tests { use super::*; use pretty_assertions::assert_eq; pub fn diff_w(expected: &[u8], actual: &[u8], filename: &str) -> Result, DiffError> { - let mut output = diff(expected, actual, false, false)?; + let mut output = diff(expected, actual, false, false, 8)?; writeln!(&mut output, "w {filename}").unwrap(); Ok(output) } @@ -176,7 +177,7 @@ mod tests { fn test_basic() { let from = b"a\n"; let to = b"b\n"; - let diff = diff(from, to, false, false).unwrap(); + let diff = diff(from, to, false, false, 8).unwrap(); let expected = ["1c", "b", ".", ""].join("\n"); assert_eq!(diff, expected.as_bytes()); } @@ -411,18 +412,18 @@ mod tests { let from = ["a", "b", "c", ""].join("\n"); let to = ["a", "d", "c", ""].join("\n"); - let diff_full = diff(from.as_bytes(), to.as_bytes(), false, false).unwrap(); + let diff_full = diff(from.as_bytes(), to.as_bytes(), false, false, 8).unwrap(); let expected_full = ["2c", "d", ".", ""].join("\n"); assert_eq!(diff_full, expected_full.as_bytes()); - let diff_brief = diff(from.as_bytes(), to.as_bytes(), true, false).unwrap(); + let diff_brief = diff(from.as_bytes(), to.as_bytes(), true, false, 8).unwrap(); let expected_brief = "\0".as_bytes(); assert_eq!(diff_brief, expected_brief); - let nodiff_full = diff(from.as_bytes(), from.as_bytes(), false, false).unwrap(); + let nodiff_full = diff(from.as_bytes(), from.as_bytes(), false, false, 8).unwrap(); assert!(nodiff_full.is_empty()); - let nodiff_brief = diff(from.as_bytes(), from.as_bytes(), true, false).unwrap(); + let nodiff_brief = diff(from.as_bytes(), from.as_bytes(), true, false, 8).unwrap(); assert!(nodiff_brief.is_empty()); } } diff --git a/src/main.rs b/src/main.rs index f074cb1..2a6d4ca 100644 --- a/src/main.rs +++ b/src/main.rs @@ -32,6 +32,7 @@ fn main() -> ExitCode { report_identical_files, brief, expand_tabs, + tabsize, } = parse_params(opts).unwrap_or_else(|error| { eprintln!("{error}"); exit(2); @@ -67,7 +68,9 @@ fn main() -> ExitCode { }; // run diff let result: Vec = match format { - Format::Normal => normal_diff::diff(&from_content, &to_content, brief, expand_tabs), + Format::Normal => { + normal_diff::diff(&from_content, &to_content, brief, expand_tabs, tabsize) + } Format::Unified => unified_diff::diff( &from_content, &from.to_string_lossy(), @@ -76,6 +79,7 @@ fn main() -> ExitCode { context_count, brief, expand_tabs, + tabsize, ), Format::Context => context_diff::diff( &from_content, @@ -85,13 +89,13 @@ fn main() -> ExitCode { context_count, brief, expand_tabs, + tabsize, ), - Format::Ed => { - ed_diff::diff(&from_content, &to_content, brief, expand_tabs).unwrap_or_else(|error| { + Format::Ed => ed_diff::diff(&from_content, &to_content, brief, expand_tabs, tabsize) + .unwrap_or_else(|error| { eprintln!("{error}"); exit(2); - }) - } + }), }; if brief && !result.is_empty() { println!( diff --git a/src/normal_diff.rs b/src/normal_diff.rs index e25a6c6..b26de77 100644 --- a/src/normal_diff.rs +++ b/src/normal_diff.rs @@ -116,7 +116,13 @@ fn make_diff(expected: &[u8], actual: &[u8], stop_early: bool) -> Vec } #[must_use] -pub fn diff(expected: &[u8], actual: &[u8], stop_early: bool, expand_tabs: bool) -> Vec { +pub fn diff( + expected: &[u8], + actual: &[u8], + stop_early: bool, + expand_tabs: bool, + tabsize: usize, +) -> Vec { // See https://www.gnu.org/software/diffutils/manual/html_node/Detailed-Normal.html // for details on the syntax of the normal format. let mut output = Vec::new(); @@ -190,7 +196,7 @@ pub fn diff(expected: &[u8], actual: &[u8], stop_early: bool, expand_tabs: bool) } for expected in &result.expected { write!(&mut output, "< ").unwrap(); - do_write_line(&mut output, expected, expand_tabs).unwrap(); + do_write_line(&mut output, expected, expand_tabs, tabsize).unwrap(); writeln!(&mut output).unwrap(); } if result.expected_missing_nl { @@ -201,7 +207,7 @@ pub fn diff(expected: &[u8], actual: &[u8], stop_early: bool, expand_tabs: bool) } for actual in &result.actual { write!(&mut output, "> ").unwrap(); - do_write_line(&mut output, actual, expand_tabs).unwrap(); + do_write_line(&mut output, actual, expand_tabs, tabsize).unwrap(); writeln!(&mut output).unwrap(); } if result.actual_missing_nl { @@ -222,7 +228,7 @@ mod tests { a.write_all(b"a\n").unwrap(); let mut b = Vec::new(); b.write_all(b"b\n").unwrap(); - let diff = diff(&a, &b, false, false); + let diff = diff(&a, &b, false, false, 8); let expected = b"1c1\n< a\n---\n> b\n".to_vec(); assert_eq!(diff, expected); } @@ -275,7 +281,7 @@ mod tests { } // This test diff is intentionally reversed. // We want it to turn the alef into bet. - let diff = diff(&alef, &bet, false, false); + let diff = diff(&alef, &bet, false, false, 8); File::create(&format!("{target}/ab.diff")) .unwrap() .write_all(&diff) @@ -367,7 +373,7 @@ mod tests { } // This test diff is intentionally reversed. // We want it to turn the alef into bet. - let diff = diff(&alef, &bet, false, false); + let diff = diff(&alef, &bet, false, false, 8); File::create(&format!("{target}/abn.diff")) .unwrap() .write_all(&diff) @@ -441,7 +447,7 @@ mod tests { } // This test diff is intentionally reversed. // We want it to turn the alef into bet. - let diff = diff(&alef, &bet, false, false); + let diff = diff(&alef, &bet, false, false, 8); File::create(&format!("{target}/ab_.diff")) .unwrap() .write_all(&diff) @@ -519,7 +525,7 @@ mod tests { } // This test diff is intentionally reversed. // We want it to turn the alef into bet. - let diff = diff(&alef, &bet, false, false); + let diff = diff(&alef, &bet, false, false, 8); File::create(&format!("{target}/abr.diff")) .unwrap() .write_all(&diff) @@ -554,18 +560,18 @@ mod tests { let from = ["a", "b", "c"].join("\n"); let to = ["a", "d", "c"].join("\n"); - let diff_full = diff(from.as_bytes(), to.as_bytes(), false, false); + let diff_full = diff(from.as_bytes(), to.as_bytes(), false, false, 8); let expected_full = ["2c2", "< b", "---", "> d", ""].join("\n"); assert_eq!(diff_full, expected_full.as_bytes()); - let diff_brief = diff(from.as_bytes(), to.as_bytes(), true, false); + let diff_brief = diff(from.as_bytes(), to.as_bytes(), true, false, 8); let expected_brief = "\0".as_bytes(); assert_eq!(diff_brief, expected_brief); - let nodiff_full = diff(from.as_bytes(), from.as_bytes(), false, false); + let nodiff_full = diff(from.as_bytes(), from.as_bytes(), false, false, 8); assert!(nodiff_full.is_empty()); - let nodiff_brief = diff(from.as_bytes(), from.as_bytes(), true, false); + let nodiff_brief = diff(from.as_bytes(), from.as_bytes(), true, false, 8); assert!(nodiff_brief.is_empty()); } } diff --git a/src/params.rs b/src/params.rs index a576f3d..f511e7c 100644 --- a/src/params.rs +++ b/src/params.rs @@ -1,5 +1,7 @@ use std::ffi::{OsStr, OsString}; +use regex::Regex; + #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum Format { Normal, @@ -8,6 +10,8 @@ pub enum Format { Ed, } +const DEFAULT_TABSIZE: usize = 8; + #[cfg(unix)] fn osstr_bytes(osstr: &OsStr) -> &[u8] { use std::os::unix::ffi::OsStrExt; @@ -28,6 +32,7 @@ pub struct Params { pub report_identical_files: bool, pub brief: bool, pub expand_tabs: bool, + pub tabsize: usize, } pub fn parse_params>(opts: I) -> Result { @@ -44,6 +49,8 @@ pub fn parse_params>(opts: I) -> Result\d+)$").unwrap(); + let mut tabsize = DEFAULT_TABSIZE; while let Some(param) = opts.next() { if param == "--" { break; @@ -70,6 +77,22 @@ pub fn parse_params>(opts: I) -> Result() { + Ok(num) => num, + Err(_) => return Err(format!("invalid tabsize «{}»", tabsize_str)), + }; + continue; + } let p = osstr_bytes(¶m); if p.first() == Some(&b'-') && p.get(1) != Some(&b'-') { let mut bit = p[1..].iter().copied().peekable(); @@ -154,6 +177,7 @@ pub fn parse_params>(opts: I) -> Result Vec { let mut output = format!("--- {expected_filename}\t\n+++ {actual_filename}\t\n").into_bytes(); let diff_results = make_diff(expected, actual, context_size, stop_early); @@ -374,19 +375,19 @@ pub fn diff( match line { DiffLine::Expected(e) => { write!(output, "-").expect("write to Vec is infallible"); - do_write_line(&mut output, &e, expand_tabs) + do_write_line(&mut output, &e, expand_tabs, tabsize) .expect("write to Vec is infallible"); writeln!(output).unwrap(); } DiffLine::Context(c) => { write!(output, " ").expect("write to Vec is infallible"); - do_write_line(&mut output, &c, expand_tabs) + do_write_line(&mut output, &c, expand_tabs, tabsize) .expect("write to Vec is infallible"); writeln!(output).unwrap(); } DiffLine::Actual(r) => { write!(output, "+",).expect("write to Vec is infallible"); - do_write_line(&mut output, &r, expand_tabs) + do_write_line(&mut output, &r, expand_tabs, tabsize) .expect("write to Vec is infallible"); writeln!(output).unwrap(); } @@ -461,6 +462,7 @@ mod tests { 2, false, false, + 8, ); File::create(&format!("{target}/ab.diff")) .unwrap() @@ -576,6 +578,7 @@ mod tests { 2, false, false, + 8, ); File::create(&format!("{target}/abn.diff")) .unwrap() @@ -671,6 +674,7 @@ mod tests { 2, false, false, + 8, ); File::create(&format!("{target}/ab_.diff")) .unwrap() @@ -751,6 +755,7 @@ mod tests { 2, false, false, + 8, ); File::create(&format!("{target}/abx.diff")) .unwrap() @@ -836,6 +841,7 @@ mod tests { 2, false, false, + 8, ); File::create(&format!("{target}/abr.diff")) .unwrap() @@ -881,6 +887,7 @@ mod tests { context_size, false, false, + 8, ); let expected_full = [ "--- foo\t", @@ -903,6 +910,7 @@ mod tests { context_size, true, false, + 8, ); let expected_brief = ["--- foo\t", "+++ bar\t", ""].join("\n"); assert_eq!(diff_brief, expected_brief.as_bytes()); @@ -915,6 +923,7 @@ mod tests { context_size, false, false, + 8, ); assert!(nodiff_full.is_empty()); @@ -926,6 +935,7 @@ mod tests { context_size, true, false, + 8, ); assert!(nodiff_brief.is_empty()); } diff --git a/src/utils.rs b/src/utils.rs index 1d13682..94d950f 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -38,9 +38,14 @@ pub fn do_expand_tabs(line: &[u8], tabsize: usize) -> Vec { /// Write a single line to an output stream, expanding tabs to space if necessary. /// This assumes that line does not contain any line breaks /// (if it does and tabs are to be expanded to spaces, the result is undefined). -pub fn do_write_line(output: &mut Vec, line: &[u8], expand_tabs: bool) -> std::io::Result<()> { +pub fn do_write_line( + output: &mut Vec, + line: &[u8], + expand_tabs: bool, + tabsize: usize, +) -> std::io::Result<()> { if expand_tabs { - output.write_all(do_expand_tabs(line, 8).as_slice()) + output.write_all(do_expand_tabs(line, tabsize).as_slice()) } else { output.write_all(line) } @@ -96,17 +101,17 @@ mod tests { use super::*; use pretty_assertions::assert_eq; - fn assert_line_written(line: &str, expand_tabs: bool, expected: &str) { + fn assert_line_written(line: &str, expand_tabs: bool, tabsize: usize, expected: &str) { let mut output: Vec = Vec::new(); - assert!(do_write_line(&mut output, line.as_bytes(), expand_tabs).is_ok()); + assert!(do_write_line(&mut output, line.as_bytes(), expand_tabs, tabsize).is_ok()); assert_eq!(output, expected.as_bytes()); } #[test] fn basics() { - assert_line_written("foo bar baz", false, "foo bar baz"); - assert_line_written("foo bar\tbaz", false, "foo bar\tbaz"); - assert_line_written("foo bar\tbaz", true, "foo bar baz"); + assert_line_written("foo bar baz", false, 8, "foo bar baz"); + assert_line_written("foo bar\tbaz", false, 8, "foo bar\tbaz"); + assert_line_written("foo bar\tbaz", true, 8, "foo bar baz"); } } } From cfc68d58bcd0bfd1b339a84cb70950fbca875569 Mon Sep 17 00:00:00 2001 From: Olivier Tilloy Date: Tue, 19 Mar 2024 19:00:39 +0100 Subject: [PATCH 047/194] Fix fuzzers' invokations --- fuzz/fuzz_targets/fuzz_ed.rs | 2 +- fuzz/fuzz_targets/fuzz_normal.rs | 2 +- fuzz/fuzz_targets/fuzz_patch.rs | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/fuzz/fuzz_targets/fuzz_ed.rs b/fuzz/fuzz_targets/fuzz_ed.rs index 5c5132e..69461d1 100644 --- a/fuzz/fuzz_targets/fuzz_ed.rs +++ b/fuzz/fuzz_targets/fuzz_ed.rs @@ -8,7 +8,7 @@ use std::io::Write; use std::process::Command; fn diff_w(expected: &[u8], actual: &[u8], filename: &str) -> Result, DiffError> { - let mut output = ed_diff::diff(expected, actual)?; + let mut output = ed_diff::diff(expected, actual, false, false, 8)?; writeln!(&mut output, "w {filename}").unwrap(); Ok(output) } diff --git a/fuzz/fuzz_targets/fuzz_normal.rs b/fuzz/fuzz_targets/fuzz_normal.rs index a44ece3..2d38641 100644 --- a/fuzz/fuzz_targets/fuzz_normal.rs +++ b/fuzz/fuzz_targets/fuzz_normal.rs @@ -21,7 +21,7 @@ fuzz_target!(|x: (Vec, Vec)| { } else { return }*/ - let diff = normal_diff::diff(&from, &to); + let diff = normal_diff::diff(&from, &to, false, false, 8); File::create("target/fuzz.file.original") .unwrap() .write_all(&from) diff --git a/fuzz/fuzz_targets/fuzz_patch.rs b/fuzz/fuzz_targets/fuzz_patch.rs index d353523..15e4967 100644 --- a/fuzz/fuzz_targets/fuzz_patch.rs +++ b/fuzz/fuzz_targets/fuzz_patch.rs @@ -26,6 +26,9 @@ fuzz_target!(|x: (Vec, Vec, u8)| { &to, "target/fuzz.file", context as usize, + false, + false, + 8, ); File::create("target/fuzz.file.original") .unwrap() From f2fd2127ed866222639019ac61298c843f778187 Mon Sep 17 00:00:00 2001 From: Olivier Tilloy Date: Tue, 19 Mar 2024 19:02:26 +0100 Subject: [PATCH 048/194] Politely ask clippy to not complain about too many arguments --- src/context_diff.rs | 1 + src/unified_diff.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/context_diff.rs b/src/context_diff.rs index 9f1db55..d9c61b8 100644 --- a/src/context_diff.rs +++ b/src/context_diff.rs @@ -265,6 +265,7 @@ fn make_diff( } #[must_use] +#[allow(clippy::too_many_arguments)] pub fn diff( expected: &[u8], expected_filename: &str, diff --git a/src/unified_diff.rs b/src/unified_diff.rs index 5af52e9..0d3ec38 100644 --- a/src/unified_diff.rs +++ b/src/unified_diff.rs @@ -236,6 +236,7 @@ fn make_diff( } #[must_use] +#[allow(clippy::too_many_arguments)] pub fn diff( expected: &[u8], expected_filename: &str, From f60fefaf6e5d7f92e8eb58ef07d3107ea9b37e8c Mon Sep 17 00:00:00 2001 From: Olivier Tilloy Date: Sat, 30 Mar 2024 23:50:58 +0100 Subject: [PATCH 049/194] Implement the Default trait for Params --- src/params.rs | 169 ++++++++++++++------------------------------------ 1 file changed, 48 insertions(+), 121 deletions(-) diff --git a/src/params.rs b/src/params.rs index f511e7c..6a6d261 100644 --- a/src/params.rs +++ b/src/params.rs @@ -2,16 +2,15 @@ use std::ffi::{OsStr, OsString}; use regex::Regex; -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub enum Format { + #[default] Normal, Unified, Context, Ed, } -const DEFAULT_TABSIZE: usize = 8; - #[cfg(unix)] fn osstr_bytes(osstr: &OsStr) -> &[u8] { use std::os::unix::ffi::OsStrExt; @@ -35,6 +34,21 @@ pub struct Params { pub tabsize: usize, } +impl Default for Params { + fn default() -> Self { + Self { + from: OsString::default(), + to: OsString::default(), + format: Format::default(), + context_count: 3, + report_identical_files: false, + brief: false, + expand_tabs: false, + tabsize: 8, + } + } +} + pub fn parse_params>(opts: I) -> Result { let mut opts = opts.into_iter(); // parse CLI @@ -42,15 +56,11 @@ pub fn parse_params>(opts: I) -> Result ".to_string()); }; + let mut params = Params::default(); let mut from = None; let mut to = None; let mut format = None; - let mut context_count = 3; - let mut report_identical_files = false; - let mut brief = false; - let mut expand_tabs = false; let tabsize_re = Regex::new(r"^--tabsize=(?\d+)$").unwrap(); - let mut tabsize = DEFAULT_TABSIZE; while let Some(param) = opts.next() { if param == "--" { break; @@ -66,15 +76,15 @@ pub fn parse_params>(opts: I) -> Result>(opts: I) -> Result() { + params.tabsize = match tabsize_str.parse::() { Ok(num) => num, Err(_) => return Err(format!("invalid tabsize «{}»", tabsize_str)), }; @@ -101,10 +111,10 @@ pub fn parse_params>(opts: I) -> Result { - context_count = (b - b'0') as usize; + params.context_count = (b - b'0') as usize; while let Some(b'0'..=b'9') = bit.peek() { - context_count *= 10; - context_count += (bit.next().unwrap() - b'0') as usize; + params.context_count *= 10; + params.context_count += (bit.next().unwrap() - b'0') as usize; } } b'c' => { @@ -138,7 +148,7 @@ pub fn parse_params>(opts: I) -> Result>(opts: I) -> Result ", exe.to_string_lossy())); } } - let from = if let Some(from) = from { + params.from = if let Some(from) = from { from } else if let Some(param) = opts.next() { param } else { return Err(format!("Usage: {} ", exe.to_string_lossy())); }; - let to = if let Some(to) = to { + params.to = if let Some(to) = to { to } else if let Some(param) = opts.next() { param } else { return Err(format!("Usage: {} ", exe.to_string_lossy())); }; - let format = format.unwrap_or(Format::Normal); - Ok(Params { - from, - to, - format, - context_count, - report_identical_files, - brief, - expand_tabs, - tabsize, - }) + params.format = format.unwrap_or(Format::default()); + Ok(params) } #[cfg(test)] @@ -193,12 +194,7 @@ mod tests { Ok(Params { from: os("foo"), to: os("bar"), - format: Format::Normal, - context_count: 3, - report_identical_files: false, - brief: false, - expand_tabs: false, - tabsize: DEFAULT_TABSIZE, + ..Default::default() }), parse_params([os("diff"), os("foo"), os("bar")].iter().cloned()) ); @@ -210,11 +206,7 @@ mod tests { from: os("foo"), to: os("bar"), format: Format::Ed, - context_count: 3, - report_identical_files: false, - brief: false, - expand_tabs: false, - tabsize: DEFAULT_TABSIZE, + ..Default::default() }), parse_params([os("diff"), os("-e"), os("foo"), os("bar")].iter().cloned()) ); @@ -227,10 +219,7 @@ mod tests { to: os("bar"), format: Format::Unified, context_count: 54, - report_identical_files: false, - brief: false, - expand_tabs: false, - tabsize: DEFAULT_TABSIZE, + ..Default::default() }), parse_params( [os("diff"), os("-u54"), os("foo"), os("bar")] @@ -244,10 +233,7 @@ mod tests { to: os("bar"), format: Format::Unified, context_count: 54, - report_identical_files: false, - brief: false, - expand_tabs: false, - tabsize: DEFAULT_TABSIZE, + ..Default::default() }), parse_params( [os("diff"), os("-U54"), os("foo"), os("bar")] @@ -261,10 +247,7 @@ mod tests { to: os("bar"), format: Format::Unified, context_count: 54, - report_identical_files: false, - brief: false, - expand_tabs: false, - tabsize: DEFAULT_TABSIZE, + ..Default::default() }), parse_params( [os("diff"), os("-U"), os("54"), os("foo"), os("bar")] @@ -278,10 +261,7 @@ mod tests { to: os("bar"), format: Format::Context, context_count: 54, - report_identical_files: false, - brief: false, - expand_tabs: false, - tabsize: DEFAULT_TABSIZE, + ..Default::default() }), parse_params( [os("diff"), os("-c54"), os("foo"), os("bar")] @@ -296,12 +276,7 @@ mod tests { Ok(Params { from: os("foo"), to: os("bar"), - format: Format::Normal, - context_count: 3, - report_identical_files: false, - brief: false, - expand_tabs: false, - tabsize: DEFAULT_TABSIZE, + ..Default::default() }), parse_params([os("diff"), os("foo"), os("bar")].iter().cloned()) ); @@ -309,12 +284,8 @@ mod tests { Ok(Params { from: os("foo"), to: os("bar"), - format: Format::Normal, - context_count: 3, report_identical_files: true, - brief: false, - expand_tabs: false, - tabsize: DEFAULT_TABSIZE, + ..Default::default() }), parse_params([os("diff"), os("-s"), os("foo"), os("bar")].iter().cloned()) ); @@ -322,12 +293,8 @@ mod tests { Ok(Params { from: os("foo"), to: os("bar"), - format: Format::Normal, - context_count: 3, report_identical_files: true, - brief: false, - expand_tabs: false, - tabsize: DEFAULT_TABSIZE, + ..Default::default() }), parse_params( [ @@ -347,12 +314,7 @@ mod tests { Ok(Params { from: os("foo"), to: os("bar"), - format: Format::Normal, - context_count: 3, - report_identical_files: false, - brief: false, - expand_tabs: false, - tabsize: 8, + ..Default::default() }), parse_params([os("diff"), os("foo"), os("bar")].iter().cloned()) ); @@ -360,12 +322,8 @@ mod tests { Ok(Params { from: os("foo"), to: os("bar"), - format: Format::Normal, - context_count: 3, - report_identical_files: false, brief: true, - expand_tabs: false, - tabsize: 8, + ..Default::default() }), parse_params([os("diff"), os("-q"), os("foo"), os("bar")].iter().cloned()) ); @@ -373,12 +331,8 @@ mod tests { Ok(Params { from: os("foo"), to: os("bar"), - format: Format::Normal, - context_count: 3, - report_identical_files: false, brief: true, - expand_tabs: false, - tabsize: 8, + ..Default::default() }), parse_params( [os("diff"), os("--brief"), os("foo"), os("bar"),] @@ -393,12 +347,7 @@ mod tests { Ok(Params { from: os("foo"), to: os("bar"), - format: Format::Normal, - context_count: 3, - report_identical_files: false, - brief: false, - expand_tabs: false, - tabsize: DEFAULT_TABSIZE, + ..Default::default() }), parse_params([os("diff"), os("foo"), os("bar")].iter().cloned()) ); @@ -407,12 +356,8 @@ mod tests { Ok(Params { from: os("foo"), to: os("bar"), - format: Format::Normal, - context_count: 3, - report_identical_files: false, - brief: false, expand_tabs: true, - tabsize: DEFAULT_TABSIZE, + ..Default::default() }), parse_params( [os("diff"), os(option), os("foo"), os("bar")] @@ -428,12 +373,7 @@ mod tests { Ok(Params { from: os("foo"), to: os("bar"), - format: Format::Normal, - context_count: 3, - report_identical_files: false, - brief: false, - expand_tabs: false, - tabsize: DEFAULT_TABSIZE, + ..Default::default() }), parse_params([os("diff"), os("foo"), os("bar")].iter().cloned()) ); @@ -441,12 +381,8 @@ mod tests { Ok(Params { from: os("foo"), to: os("bar"), - format: Format::Normal, - context_count: 3, - report_identical_files: false, - brief: false, - expand_tabs: false, tabsize: 0, + ..Default::default() }), parse_params( [os("diff"), os("--tabsize=0"), os("foo"), os("bar")] @@ -458,12 +394,8 @@ mod tests { Ok(Params { from: os("foo"), to: os("bar"), - format: Format::Normal, - context_count: 3, - report_identical_files: false, - brief: false, - expand_tabs: false, tabsize: 42, + ..Default::default() }), parse_params( [os("diff"), os("--tabsize=42"), os("foo"), os("bar")] @@ -519,12 +451,7 @@ mod tests { Ok(Params { from: os("-g"), to: os("-h"), - format: Format::Normal, - context_count: 3, - report_identical_files: false, - brief: false, - expand_tabs: false, - tabsize: DEFAULT_TABSIZE, + ..Default::default() }), parse_params([os("diff"), os("--"), os("-g"), os("-h")].iter().cloned()) ); From 9ff8f89626ee8a144fc25e21a9dab3e7af9c28cb Mon Sep 17 00:00:00 2001 From: Tanmay Patil Date: Thu, 28 Mar 2024 23:32:46 +0530 Subject: [PATCH 050/194] Fix tests --- Cargo.lock | 5 +++-- Cargo.toml | 1 + src/context_diff.rs | 31 +++++++++++++++++++++++++------ 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6235103..4453f09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -129,6 +129,7 @@ dependencies = [ "diff", "predicates", "pretty_assertions", + "regex", "same-file", "tempfile", ] @@ -301,9 +302,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.3" +version = "1.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" dependencies = [ "aho-corasick", "memchr", diff --git a/Cargo.toml b/Cargo.toml index 940e3d1..32f0cda 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ path = "src/main.rs" [dependencies] chrono = "0.4.35" diff = "0.1.10" +regex = "1.10.4" same-file = "1.0.6" [dev-dependencies] diff --git a/src/context_diff.rs b/src/context_diff.rs index fa09f7c..bb1dfe1 100644 --- a/src/context_diff.rs +++ b/src/context_diff.rs @@ -433,11 +433,12 @@ mod tests { if f != 2 { bet.write_all(b"l\n").unwrap(); } + let _ = File::create(&format!("{target}/aalef")).unwrap(); // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff( &alef, - "a/alef", + &format!("{target}/aalef"), &bet, &format!("{target}/alef"), 2, @@ -512,11 +513,12 @@ mod tests { if f != 2 { bet.write_all(b"l\n").unwrap(); } + let _ = File::create(&format!("{target}/aalef_")).unwrap(); // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff( &alef, - "a/alef_", + &format!("{target}/aalef_"), &bet, &format!("{target}/alef_"), 2, @@ -594,11 +596,12 @@ mod tests { if alef.is_empty() && bet.is_empty() { continue; }; + let _ = File::create(&format!("{target}/aalefx")).unwrap(); // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff( &alef, - "a/alefx", + &format!("{target}/aalefx"), &bet, &format!("{target}/alefx"), 2, @@ -679,11 +682,12 @@ mod tests { if f != 2 { bet.write_all(b"f\n").unwrap(); } + let _ = File::create(&format!("{target}/aalefr")).unwrap(); // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff( &alef, - "a/alefr", + &format!("{target}/aalefr"), &bet, &format!("{target}/alefr"), 2, @@ -720,9 +724,15 @@ mod tests { #[test] fn test_stop_early() { + use regex::Regex; + use std::fs::File; + use std::str; + let from_filename = "foo"; + let _ = File::create(&format!("foo")).unwrap(); let from = vec!["a", "b", "c", ""].join("\n"); let to_filename = "bar"; + let _ = File::create(&format!("bar")).unwrap(); let to = vec!["a", "d", "c", ""].join("\n"); let context_size: usize = 3; @@ -734,6 +744,11 @@ mod tests { context_size, false, ); + + let diff_full_text = str::from_utf8(&diff_full).unwrap(); + let re = Regex::new(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+ [+-]\d{4}").unwrap(); + let diff_full = re.replace_all(diff_full_text, ""); + let expected_full = vec![ "*** foo\t", "--- bar\t", @@ -749,7 +764,7 @@ mod tests { "", ] .join("\n"); - assert_eq!(diff_full, expected_full.as_bytes()); + assert_eq!(diff_full, expected_full); let diff_brief = diff( from.as_bytes(), @@ -759,8 +774,12 @@ mod tests { context_size, true, ); + + let diff_brief_text = str::from_utf8(&diff_brief).unwrap(); + let re = Regex::new(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+ [+-]\d{4}").unwrap(); + let diff_brief = re.replace_all(diff_brief_text, ""); let expected_brief = vec!["*** foo\t", "--- bar\t", ""].join("\n"); - assert_eq!(diff_brief, expected_brief.as_bytes()); + assert_eq!(diff_brief, expected_brief); let nodiff_full = diff( from.as_bytes(), From 80c9944bf79ddf71c73324b58faabd4408bd5f69 Mon Sep 17 00:00:00 2001 From: Tanmay Patil Date: Sun, 31 Mar 2024 22:57:51 +0530 Subject: [PATCH 051/194] Create foo/bar in target/context-diff --- src/context_diff.rs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/context_diff.rs b/src/context_diff.rs index 9df10d0..cad851c 100644 --- a/src/context_diff.rs +++ b/src/context_diff.rs @@ -747,11 +747,14 @@ mod tests { use std::fs::File; use std::str; - let from_filename = "foo"; - let _ = File::create(&format!("foo")).unwrap(); + let target = "target/context-diff"; + // test all possible six-line files. + let _ = std::fs::create_dir(target); + let from_filename = &format!("{target}/foo"); + let _ = File::create(from_filename).unwrap(); let from = ["a", "b", "c", ""].join("\n"); - let to_filename = "bar"; - let _ = File::create(&format!("bar")).unwrap(); + let to_filename = &format!("{target}/bar"); + let _ = File::create(to_filename).unwrap(); let to = ["a", "d", "c", ""].join("\n"); let context_size: usize = 3; @@ -771,8 +774,8 @@ mod tests { let diff_full = re.replace_all(diff_full_text, ""); let expected_full = [ - "*** foo\t", - "--- bar\t", + "*** target/context-diff/foo\t", + "--- target/context-diff/bar\t", "***************", "*** 1,3 ****", " a", @@ -801,7 +804,7 @@ mod tests { let diff_brief_text = str::from_utf8(&diff_brief).unwrap(); let re = Regex::new(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+ [+-]\d{4}").unwrap(); let diff_brief = re.replace_all(diff_brief_text, ""); - let expected_brief = ["*** foo\t", "--- bar\t", ""].join("\n"); + let expected_brief = ["*** target/context-diff/foo\t", "--- target/context-diff/bar\t", ""].join("\n"); assert_eq!(diff_brief, expected_brief); let nodiff_full = diff( From e6a0ba28c5900498824d9bdcc2c979dafdd0d2ed Mon Sep 17 00:00:00 2001 From: Olivier Tilloy Date: Sun, 31 Mar 2024 21:27:28 +0200 Subject: [PATCH 052/194] Pass a Params reference to the various diff() functions, instead of a long list of arguments --- src/context_diff.rs | 131 ++++++++++++++++++++---------------------- src/ed_diff.rs | 43 ++++++++------ src/lib.rs | 1 + src/main.rs | 68 ++++++---------------- src/normal_diff.rs | 49 +++++++++------- src/unified_diff.rs | 137 +++++++++++++++++++++----------------------- 6 files changed, 203 insertions(+), 226 deletions(-) diff --git a/src/context_diff.rs b/src/context_diff.rs index d9c61b8..9c567d8 100644 --- a/src/context_diff.rs +++ b/src/context_diff.rs @@ -6,6 +6,7 @@ use std::collections::VecDeque; use std::io::Write; +use crate::params::Params; use crate::utils::do_write_line; #[derive(Debug, PartialEq)] @@ -265,23 +266,18 @@ fn make_diff( } #[must_use] -#[allow(clippy::too_many_arguments)] -pub fn diff( - expected: &[u8], - expected_filename: &str, - actual: &[u8], - actual_filename: &str, - context_size: usize, - stop_early: bool, - expand_tabs: bool, - tabsize: usize, -) -> Vec { - let mut output = format!("*** {expected_filename}\t\n--- {actual_filename}\t\n").into_bytes(); - let diff_results = make_diff(expected, actual, context_size, stop_early); +pub fn diff(expected: &[u8], actual: &[u8], params: &Params) -> Vec { + let mut output = format!( + "*** {0}\t\n--- {1}\t\n", + params.from.to_string_lossy(), + params.to.to_string_lossy() + ) + .into_bytes(); + let diff_results = make_diff(expected, actual, params.context_count, params.brief); if diff_results.is_empty() { return Vec::new(); } - if stop_early { + if params.brief { return output; } for result in diff_results { @@ -319,19 +315,19 @@ pub fn diff( match line { DiffLine::Context(e) => { write!(output, " ").expect("write to Vec is infallible"); - do_write_line(&mut output, &e, expand_tabs, tabsize) + do_write_line(&mut output, &e, params.expand_tabs, params.tabsize) .expect("write to Vec is infallible"); writeln!(output).unwrap(); } DiffLine::Change(e) => { write!(output, "! ").expect("write to Vec is infallible"); - do_write_line(&mut output, &e, expand_tabs, tabsize) + do_write_line(&mut output, &e, params.expand_tabs, params.tabsize) .expect("write to Vec is infallible"); writeln!(output).unwrap(); } DiffLine::Add(e) => { write!(output, "- ").expect("write to Vec is infallible"); - do_write_line(&mut output, &e, expand_tabs, tabsize) + do_write_line(&mut output, &e, params.expand_tabs, params.tabsize) .expect("write to Vec is infallible"); writeln!(output).unwrap(); } @@ -349,19 +345,19 @@ pub fn diff( match line { DiffLine::Context(e) => { write!(output, " ").expect("write to Vec is infallible"); - do_write_line(&mut output, &e, expand_tabs, tabsize) + do_write_line(&mut output, &e, params.expand_tabs, params.tabsize) .expect("write to Vec is infallible"); writeln!(output).unwrap(); } DiffLine::Change(e) => { write!(output, "! ").expect("write to Vec is infallible"); - do_write_line(&mut output, &e, expand_tabs, tabsize) + do_write_line(&mut output, &e, params.expand_tabs, params.tabsize) .expect("write to Vec is infallible"); writeln!(output).unwrap(); } DiffLine::Add(e) => { write!(output, "+ ").expect("write to Vec is infallible"); - do_write_line(&mut output, &e, expand_tabs, tabsize) + do_write_line(&mut output, &e, params.expand_tabs, params.tabsize) .expect("write to Vec is infallible"); writeln!(output).unwrap(); } @@ -430,13 +426,13 @@ mod tests { // We want it to turn the alef into bet. let diff = diff( &alef, - "a/alef", &bet, - &format!("{target}/alef"), - 2, - false, - false, - 8, + &Params { + from: "a/alef".into(), + to: (&format!("{target}/alef")).into(), + context_count: 2, + ..Default::default() + }, ); File::create(&format!("{target}/ab.diff")) .unwrap() @@ -511,13 +507,13 @@ mod tests { // We want it to turn the alef into bet. let diff = diff( &alef, - "a/alef_", &bet, - &format!("{target}/alef_"), - 2, - false, - false, - 8, + &Params { + from: "a/alef_".into(), + to: (&format!("{target}/alef_")).into(), + context_count: 2, + ..Default::default() + }, ); File::create(&format!("{target}/ab_.diff")) .unwrap() @@ -595,13 +591,13 @@ mod tests { // We want it to turn the alef into bet. let diff = diff( &alef, - "a/alefx", &bet, - &format!("{target}/alefx"), - 2, - false, - false, - 8, + &Params { + from: "a/alefx".into(), + to: (&format!("{target}/alefx")).into(), + context_count: 2, + ..Default::default() + }, ); File::create(&format!("{target}/abx.diff")) .unwrap() @@ -682,13 +678,13 @@ mod tests { // We want it to turn the alef into bet. let diff = diff( &alef, - "a/alefr", &bet, - &format!("{target}/alefr"), - 2, - false, - false, - 8, + &Params { + from: "a/alefr".into(), + to: (&format!("{target}/alefr")).into(), + context_count: 2, + ..Default::default() + }, ); File::create(&format!("{target}/abr.diff")) .unwrap() @@ -725,17 +721,15 @@ mod tests { let from = ["a", "b", "c", ""].join("\n"); let to_filename = "bar"; let to = ["a", "d", "c", ""].join("\n"); - let context_size: usize = 3; let diff_full = diff( from.as_bytes(), - from_filename, to.as_bytes(), - to_filename, - context_size, - false, - false, - 8, + &Params { + from: from_filename.into(), + to: to_filename.into(), + ..Default::default() + }, ); let expected_full = [ "*** foo\t", @@ -756,38 +750,37 @@ mod tests { let diff_brief = diff( from.as_bytes(), - from_filename, to.as_bytes(), - to_filename, - context_size, - true, - false, - 8, + &Params { + from: from_filename.into(), + to: to_filename.into(), + brief: true, + ..Default::default() + }, ); let expected_brief = ["*** foo\t", "--- bar\t", ""].join("\n"); assert_eq!(diff_brief, expected_brief.as_bytes()); let nodiff_full = diff( from.as_bytes(), - from_filename, from.as_bytes(), - to_filename, - context_size, - false, - false, - 8, + &Params { + from: from_filename.into(), + to: to_filename.into(), + ..Default::default() + }, ); assert!(nodiff_full.is_empty()); let nodiff_brief = diff( from.as_bytes(), - from_filename, from.as_bytes(), - to_filename, - context_size, - true, - false, - 8, + &Params { + from: from_filename.into(), + to: to_filename.into(), + brief: true, + ..Default::default() + }, ); assert!(nodiff_brief.is_empty()); } diff --git a/src/ed_diff.rs b/src/ed_diff.rs index c02289c..1be37a5 100644 --- a/src/ed_diff.rs +++ b/src/ed_diff.rs @@ -5,6 +5,7 @@ use std::io::Write; +use crate::params::Params; use crate::utils::do_write_line; #[derive(Debug, PartialEq)] @@ -109,16 +110,10 @@ fn make_diff(expected: &[u8], actual: &[u8], stop_early: bool) -> Result Result, DiffError> { +pub fn diff(expected: &[u8], actual: &[u8], params: &Params) -> Result, DiffError> { let mut output = Vec::new(); - let diff_results = make_diff(expected, actual, stop_early)?; - if stop_early && !diff_results.is_empty() { + let diff_results = make_diff(expected, actual, params.brief)?; + if params.brief && !diff_results.is_empty() { write!(&mut output, "\0").unwrap(); return Ok(output); } @@ -153,7 +148,7 @@ pub fn diff( if actual == b"." { writeln!(&mut output, "..\n.\ns/.//\na").unwrap(); } else { - do_write_line(&mut output, actual, expand_tabs, tabsize).unwrap(); + do_write_line(&mut output, actual, params.expand_tabs, params.tabsize).unwrap(); writeln!(&mut output).unwrap(); } } @@ -168,7 +163,7 @@ mod tests { use super::*; use pretty_assertions::assert_eq; pub fn diff_w(expected: &[u8], actual: &[u8], filename: &str) -> Result, DiffError> { - let mut output = diff(expected, actual, false, false, 8)?; + let mut output = diff(expected, actual, &Params::default())?; writeln!(&mut output, "w {filename}").unwrap(); Ok(output) } @@ -177,7 +172,7 @@ mod tests { fn test_basic() { let from = b"a\n"; let to = b"b\n"; - let diff = diff(from, to, false, false, 8).unwrap(); + let diff = diff(from, to, &Params::default()).unwrap(); let expected = ["1c", "b", ".", ""].join("\n"); assert_eq!(diff, expected.as_bytes()); } @@ -412,18 +407,34 @@ mod tests { let from = ["a", "b", "c", ""].join("\n"); let to = ["a", "d", "c", ""].join("\n"); - let diff_full = diff(from.as_bytes(), to.as_bytes(), false, false, 8).unwrap(); + let diff_full = diff(from.as_bytes(), to.as_bytes(), &Params::default()).unwrap(); let expected_full = ["2c", "d", ".", ""].join("\n"); assert_eq!(diff_full, expected_full.as_bytes()); - let diff_brief = diff(from.as_bytes(), to.as_bytes(), true, false, 8).unwrap(); + let diff_brief = diff( + from.as_bytes(), + to.as_bytes(), + &Params { + brief: true, + ..Default::default() + }, + ) + .unwrap(); let expected_brief = "\0".as_bytes(); assert_eq!(diff_brief, expected_brief); - let nodiff_full = diff(from.as_bytes(), from.as_bytes(), false, false, 8).unwrap(); + let nodiff_full = diff(from.as_bytes(), from.as_bytes(), &Params::default()).unwrap(); assert!(nodiff_full.is_empty()); - let nodiff_brief = diff(from.as_bytes(), from.as_bytes(), true, false, 8).unwrap(); + let nodiff_brief = diff( + from.as_bytes(), + from.as_bytes(), + &Params { + brief: true, + ..Default::default() + }, + ) + .unwrap(); assert!(nodiff_brief.is_empty()); } } diff --git a/src/lib.rs b/src/lib.rs index faf5df2..7ed36a6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ pub mod context_diff; pub mod ed_diff; pub mod normal_diff; +pub mod params; pub mod unified_diff; pub mod utils; diff --git a/src/main.rs b/src/main.rs index 2a6d4ca..dab0eff 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ // For the full copyright and license information, please view the LICENSE-* // files that was distributed with this source code. -use crate::params::{parse_params, Format, Params}; +use crate::params::{parse_params, Format}; use std::env; use std::fs; @@ -24,42 +24,33 @@ mod utils; // and 2 means trouble. fn main() -> ExitCode { let opts = env::args_os(); - let Params { - from, - to, - context_count, - format, - report_identical_files, - brief, - expand_tabs, - tabsize, - } = parse_params(opts).unwrap_or_else(|error| { + let params = parse_params(opts).unwrap_or_else(|error| { eprintln!("{error}"); exit(2); }); // if from and to are the same file, no need to perform any comparison let maybe_report_identical_files = || { - if report_identical_files { + if params.report_identical_files { println!( "Files {} and {} are identical", - from.to_string_lossy(), - to.to_string_lossy(), + params.from.to_string_lossy(), + params.to.to_string_lossy(), ) } }; - if same_file::is_same_file(&from, &to).unwrap_or(false) { + if same_file::is_same_file(¶ms.from, ¶ms.to).unwrap_or(false) { maybe_report_identical_files(); return ExitCode::SUCCESS; } // read files - let from_content = match fs::read(&from) { + let from_content = match fs::read(¶ms.from) { Ok(from_content) => from_content, Err(e) => { eprintln!("Failed to read from-file: {e}"); return ExitCode::from(2); } }; - let to_content = match fs::read(&to) { + let to_content = match fs::read(¶ms.to) { Ok(to_content) => to_content, Err(e) => { eprintln!("Failed to read to-file: {e}"); @@ -67,41 +58,20 @@ fn main() -> ExitCode { } }; // run diff - let result: Vec = match format { - Format::Normal => { - normal_diff::diff(&from_content, &to_content, brief, expand_tabs, tabsize) - } - Format::Unified => unified_diff::diff( - &from_content, - &from.to_string_lossy(), - &to_content, - &to.to_string_lossy(), - context_count, - brief, - expand_tabs, - tabsize, - ), - Format::Context => context_diff::diff( - &from_content, - &from.to_string_lossy(), - &to_content, - &to.to_string_lossy(), - context_count, - brief, - expand_tabs, - tabsize, - ), - Format::Ed => ed_diff::diff(&from_content, &to_content, brief, expand_tabs, tabsize) - .unwrap_or_else(|error| { - eprintln!("{error}"); - exit(2); - }), + let result: Vec = match params.format { + Format::Normal => normal_diff::diff(&from_content, &to_content, ¶ms), + Format::Unified => unified_diff::diff(&from_content, &to_content, ¶ms), + Format::Context => context_diff::diff(&from_content, &to_content, ¶ms), + Format::Ed => ed_diff::diff(&from_content, &to_content, ¶ms).unwrap_or_else(|error| { + eprintln!("{error}"); + exit(2); + }), }; - if brief && !result.is_empty() { + if params.brief && !result.is_empty() { println!( "Files {} and {} differ", - from.to_string_lossy(), - to.to_string_lossy() + params.from.to_string_lossy(), + params.to.to_string_lossy() ); } else { io::stdout().write_all(&result).unwrap(); diff --git a/src/normal_diff.rs b/src/normal_diff.rs index b26de77..cfa389c 100644 --- a/src/normal_diff.rs +++ b/src/normal_diff.rs @@ -5,6 +5,7 @@ use std::io::Write; +use crate::params::Params; use crate::utils::do_write_line; #[derive(Debug, PartialEq)] @@ -116,18 +117,12 @@ fn make_diff(expected: &[u8], actual: &[u8], stop_early: bool) -> Vec } #[must_use] -pub fn diff( - expected: &[u8], - actual: &[u8], - stop_early: bool, - expand_tabs: bool, - tabsize: usize, -) -> Vec { +pub fn diff(expected: &[u8], actual: &[u8], params: &Params) -> Vec { // See https://www.gnu.org/software/diffutils/manual/html_node/Detailed-Normal.html // for details on the syntax of the normal format. let mut output = Vec::new(); - let diff_results = make_diff(expected, actual, stop_early); - if stop_early && !diff_results.is_empty() { + let diff_results = make_diff(expected, actual, params.brief); + if params.brief && !diff_results.is_empty() { write!(&mut output, "\0").unwrap(); return output; } @@ -196,7 +191,7 @@ pub fn diff( } for expected in &result.expected { write!(&mut output, "< ").unwrap(); - do_write_line(&mut output, expected, expand_tabs, tabsize).unwrap(); + do_write_line(&mut output, expected, params.expand_tabs, params.tabsize).unwrap(); writeln!(&mut output).unwrap(); } if result.expected_missing_nl { @@ -207,7 +202,7 @@ pub fn diff( } for actual in &result.actual { write!(&mut output, "> ").unwrap(); - do_write_line(&mut output, actual, expand_tabs, tabsize).unwrap(); + do_write_line(&mut output, actual, params.expand_tabs, params.tabsize).unwrap(); writeln!(&mut output).unwrap(); } if result.actual_missing_nl { @@ -228,7 +223,7 @@ mod tests { a.write_all(b"a\n").unwrap(); let mut b = Vec::new(); b.write_all(b"b\n").unwrap(); - let diff = diff(&a, &b, false, false, 8); + let diff = diff(&a, &b, &Params::default()); let expected = b"1c1\n< a\n---\n> b\n".to_vec(); assert_eq!(diff, expected); } @@ -281,7 +276,7 @@ mod tests { } // This test diff is intentionally reversed. // We want it to turn the alef into bet. - let diff = diff(&alef, &bet, false, false, 8); + let diff = diff(&alef, &bet, &Params::default()); File::create(&format!("{target}/ab.diff")) .unwrap() .write_all(&diff) @@ -373,7 +368,7 @@ mod tests { } // This test diff is intentionally reversed. // We want it to turn the alef into bet. - let diff = diff(&alef, &bet, false, false, 8); + let diff = diff(&alef, &bet, &Params::default()); File::create(&format!("{target}/abn.diff")) .unwrap() .write_all(&diff) @@ -447,7 +442,7 @@ mod tests { } // This test diff is intentionally reversed. // We want it to turn the alef into bet. - let diff = diff(&alef, &bet, false, false, 8); + let diff = diff(&alef, &bet, &Params::default()); File::create(&format!("{target}/ab_.diff")) .unwrap() .write_all(&diff) @@ -525,7 +520,7 @@ mod tests { } // This test diff is intentionally reversed. // We want it to turn the alef into bet. - let diff = diff(&alef, &bet, false, false, 8); + let diff = diff(&alef, &bet, &Params::default()); File::create(&format!("{target}/abr.diff")) .unwrap() .write_all(&diff) @@ -560,18 +555,32 @@ mod tests { let from = ["a", "b", "c"].join("\n"); let to = ["a", "d", "c"].join("\n"); - let diff_full = diff(from.as_bytes(), to.as_bytes(), false, false, 8); + let diff_full = diff(from.as_bytes(), to.as_bytes(), &Params::default()); let expected_full = ["2c2", "< b", "---", "> d", ""].join("\n"); assert_eq!(diff_full, expected_full.as_bytes()); - let diff_brief = diff(from.as_bytes(), to.as_bytes(), true, false, 8); + let diff_brief = diff( + from.as_bytes(), + to.as_bytes(), + &Params { + brief: true, + ..Default::default() + }, + ); let expected_brief = "\0".as_bytes(); assert_eq!(diff_brief, expected_brief); - let nodiff_full = diff(from.as_bytes(), from.as_bytes(), false, false, 8); + let nodiff_full = diff(from.as_bytes(), from.as_bytes(), &Params::default()); assert!(nodiff_full.is_empty()); - let nodiff_brief = diff(from.as_bytes(), from.as_bytes(), true, false, 8); + let nodiff_brief = diff( + from.as_bytes(), + from.as_bytes(), + &Params { + brief: true, + ..Default::default() + }, + ); assert!(nodiff_brief.is_empty()); } } diff --git a/src/unified_diff.rs b/src/unified_diff.rs index 0d3ec38..3af51c2 100644 --- a/src/unified_diff.rs +++ b/src/unified_diff.rs @@ -6,6 +6,7 @@ use std::collections::VecDeque; use std::io::Write; +use crate::params::Params; use crate::utils::do_write_line; #[derive(Debug, PartialEq)] @@ -236,23 +237,18 @@ fn make_diff( } #[must_use] -#[allow(clippy::too_many_arguments)] -pub fn diff( - expected: &[u8], - expected_filename: &str, - actual: &[u8], - actual_filename: &str, - context_size: usize, - stop_early: bool, - expand_tabs: bool, - tabsize: usize, -) -> Vec { - let mut output = format!("--- {expected_filename}\t\n+++ {actual_filename}\t\n").into_bytes(); - let diff_results = make_diff(expected, actual, context_size, stop_early); +pub fn diff(expected: &[u8], actual: &[u8], params: &Params) -> Vec { + let mut output = format!( + "--- {0}\t\n+++ {1}\t\n", + params.from.to_string_lossy(), + params.to.to_string_lossy() + ) + .into_bytes(); + let diff_results = make_diff(expected, actual, params.context_count, params.brief); if diff_results.is_empty() { return Vec::new(); } - if stop_early { + if params.brief { return output; } for result in diff_results { @@ -376,19 +372,19 @@ pub fn diff( match line { DiffLine::Expected(e) => { write!(output, "-").expect("write to Vec is infallible"); - do_write_line(&mut output, &e, expand_tabs, tabsize) + do_write_line(&mut output, &e, params.expand_tabs, params.tabsize) .expect("write to Vec is infallible"); writeln!(output).unwrap(); } DiffLine::Context(c) => { write!(output, " ").expect("write to Vec is infallible"); - do_write_line(&mut output, &c, expand_tabs, tabsize) + do_write_line(&mut output, &c, params.expand_tabs, params.tabsize) .expect("write to Vec is infallible"); writeln!(output).unwrap(); } DiffLine::Actual(r) => { write!(output, "+",).expect("write to Vec is infallible"); - do_write_line(&mut output, &r, expand_tabs, tabsize) + do_write_line(&mut output, &r, params.expand_tabs, params.tabsize) .expect("write to Vec is infallible"); writeln!(output).unwrap(); } @@ -457,13 +453,13 @@ mod tests { // We want it to turn the alef into bet. let diff = diff( &alef, - "a/alef", &bet, - &format!("{target}/alef"), - 2, - false, - false, - 8, + &Params { + from: "a/alef".into(), + to: (&format!("{target}/alef")).into(), + context_count: 2, + ..Default::default() + }, ); File::create(&format!("{target}/ab.diff")) .unwrap() @@ -573,13 +569,13 @@ mod tests { // We want it to turn the alef into bet. let diff = diff( &alef, - "a/alefn", &bet, - &format!("{target}/alefn"), - 2, - false, - false, - 8, + &Params { + from: "a/alefn".into(), + to: (&format!("{target}/alefn")).into(), + context_count: 2, + ..Default::default() + }, ); File::create(&format!("{target}/abn.diff")) .unwrap() @@ -669,13 +665,13 @@ mod tests { // We want it to turn the alef into bet. let diff = diff( &alef, - "a/alef_", &bet, - &format!("{target}/alef_"), - 2, - false, - false, - 8, + &Params { + from: "a/alef_".into(), + to: (&format!("{target}/alef_")).into(), + context_count: 2, + ..Default::default() + }, ); File::create(&format!("{target}/ab_.diff")) .unwrap() @@ -750,13 +746,13 @@ mod tests { // We want it to turn the alef into bet. let diff = diff( &alef, - "a/alefx", &bet, - &format!("{target}/alefx"), - 2, - false, - false, - 8, + &Params { + from: "a/alefx".into(), + to: (&format!("{target}/alefx")).into(), + context_count: 2, + ..Default::default() + }, ); File::create(&format!("{target}/abx.diff")) .unwrap() @@ -836,13 +832,13 @@ mod tests { // We want it to turn the alef into bet. let diff = diff( &alef, - "a/alefr", &bet, - &format!("{target}/alefr"), - 2, - false, - false, - 8, + &Params { + from: "a/alefr".into(), + to: (&format!("{target}/alefr")).into(), + context_count: 2, + ..Default::default() + }, ); File::create(&format!("{target}/abr.diff")) .unwrap() @@ -878,17 +874,15 @@ mod tests { let from = ["a", "b", "c", ""].join("\n"); let to_filename = "bar"; let to = ["a", "d", "c", ""].join("\n"); - let context_size: usize = 3; let diff_full = diff( from.as_bytes(), - from_filename, to.as_bytes(), - to_filename, - context_size, - false, - false, - 8, + &Params { + from: from_filename.into(), + to: to_filename.into(), + ..Default::default() + }, ); let expected_full = [ "--- foo\t", @@ -905,38 +899,37 @@ mod tests { let diff_brief = diff( from.as_bytes(), - from_filename, to.as_bytes(), - to_filename, - context_size, - true, - false, - 8, + &Params { + from: from_filename.into(), + to: to_filename.into(), + brief: true, + ..Default::default() + }, ); let expected_brief = ["--- foo\t", "+++ bar\t", ""].join("\n"); assert_eq!(diff_brief, expected_brief.as_bytes()); let nodiff_full = diff( from.as_bytes(), - from_filename, from.as_bytes(), - to_filename, - context_size, - false, - false, - 8, + &Params { + from: from_filename.into(), + to: to_filename.into(), + ..Default::default() + }, ); assert!(nodiff_full.is_empty()); let nodiff_brief = diff( from.as_bytes(), - from_filename, from.as_bytes(), - to_filename, - context_size, - true, - false, - 8, + &Params { + from: from_filename.into(), + to: to_filename.into(), + brief: true, + ..Default::default() + }, ); assert!(nodiff_brief.is_empty()); } From e9f0630aafbfd8ac33271b59cba8f9cf2be73e7e Mon Sep 17 00:00:00 2001 From: Olivier Tilloy Date: Sun, 31 Mar 2024 23:39:43 +0200 Subject: [PATCH 053/194] Update fuzzers --- fuzz/fuzz_targets/fuzz_ed.rs | 3 ++- fuzz/fuzz_targets/fuzz_normal.rs | 3 ++- fuzz/fuzz_targets/fuzz_patch.rs | 13 +++++++------ 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/fuzz/fuzz_targets/fuzz_ed.rs b/fuzz/fuzz_targets/fuzz_ed.rs index 69461d1..7c38fda 100644 --- a/fuzz/fuzz_targets/fuzz_ed.rs +++ b/fuzz/fuzz_targets/fuzz_ed.rs @@ -3,12 +3,13 @@ extern crate libfuzzer_sys; use diffutilslib::ed_diff; use diffutilslib::ed_diff::DiffError; +use diffutilslib::params::Params; use std::fs::{self, File}; use std::io::Write; use std::process::Command; fn diff_w(expected: &[u8], actual: &[u8], filename: &str) -> Result, DiffError> { - let mut output = ed_diff::diff(expected, actual, false, false, 8)?; + let mut output = ed_diff::diff(expected, actual, &Params::default())?; writeln!(&mut output, "w {filename}").unwrap(); Ok(output) } diff --git a/fuzz/fuzz_targets/fuzz_normal.rs b/fuzz/fuzz_targets/fuzz_normal.rs index 2d38641..6b1e6b9 100644 --- a/fuzz/fuzz_targets/fuzz_normal.rs +++ b/fuzz/fuzz_targets/fuzz_normal.rs @@ -2,6 +2,7 @@ #[macro_use] extern crate libfuzzer_sys; use diffutilslib::normal_diff; +use diffutilslib::params::Params; use std::fs::{self, File}; use std::io::Write; @@ -21,7 +22,7 @@ fuzz_target!(|x: (Vec, Vec)| { } else { return }*/ - let diff = normal_diff::diff(&from, &to, false, false, 8); + let diff = normal_diff::diff(&from, &to, &Params::default()); File::create("target/fuzz.file.original") .unwrap() .write_all(&from) diff --git a/fuzz/fuzz_targets/fuzz_patch.rs b/fuzz/fuzz_targets/fuzz_patch.rs index 15e4967..4dea4b5 100644 --- a/fuzz/fuzz_targets/fuzz_patch.rs +++ b/fuzz/fuzz_targets/fuzz_patch.rs @@ -1,6 +1,7 @@ #![no_main] #[macro_use] extern crate libfuzzer_sys; +use diffutilslib::params::Params; use diffutilslib::unified_diff; use std::fs::{self, File}; use std::io::Write; @@ -22,13 +23,13 @@ fuzz_target!(|x: (Vec, Vec, u8)| { }*/ let diff = unified_diff::diff( &from, - "a/fuzz.file", &to, - "target/fuzz.file", - context as usize, - false, - false, - 8, + &Params { + from: "a/fuzz.file".into(), + to: "target/fuzz.file".into(), + context_count: context as usize, + ..Default::default() + } ); File::create("target/fuzz.file.original") .unwrap() From 6a73657b3a1014ffa23b23a5238fb7b8980994ca Mon Sep 17 00:00:00 2001 From: Olivier Tilloy Date: Mon, 1 Apr 2024 00:26:30 +0200 Subject: [PATCH 054/194] README: minor grammar correction --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1308648..62be861 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![CodeCov](https://codecov.io/gh/uutils/diffutils/branch/master/graph/badge.svg)](https://codecov.io/gh/uutils/diffutils) -The goal of this package is to be a dropped in replacement for the [diffutils commands](https://www.gnu.org/software/diffutils/) in Rust. +The goal of this package is to be a drop-in replacement for the [diffutils commands](https://www.gnu.org/software/diffutils/) in Rust. Based on the incomplete diff generator in https://github.com/rust-lang/rust/blob/master/src/tools/compiletest/src/runtest.rs, and made to be compatible with GNU's diff and patch tools. From 76c4714f78ffbe09554b6ced8a302a7e1274f980 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 1 Apr 2024 23:15:23 +0200 Subject: [PATCH 055/194] Disable the fail fast (closes: #40) --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d416f74..8b8e474 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,7 @@ jobs: name: cargo check runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: os: [ubuntu-latest, macOS-latest, windows-latest] steps: @@ -21,6 +22,7 @@ jobs: name: cargo test runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: os: [ubuntu-latest, macOS-latest, windows-latest] steps: @@ -41,6 +43,7 @@ jobs: name: cargo clippy -- -D warnings runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: os: [ubuntu-latest, macOS-latest, windows-latest] steps: @@ -70,6 +73,7 @@ jobs: name: Code Coverage runs-on: ${{ matrix.job.os }} strategy: + fail-fast: false matrix: job: - { os: ubuntu-latest , features: unix } From 589039ab4c6f61406e7f23094edbe2bbfb2557b2 Mon Sep 17 00:00:00 2001 From: Olivier Tilloy Date: Tue, 2 Apr 2024 18:22:40 +0200 Subject: [PATCH 056/194] Fix the link to the Codecov badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 62be861..552df09 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![License](http://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/uutils/diffutils/blob/main/LICENSE) [![dependency status](https://deps.rs/repo/github/uutils/diffutils/status.svg)](https://deps.rs/repo/github/uutils/diffutils) -[![CodeCov](https://codecov.io/gh/uutils/diffutils/branch/master/graph/badge.svg)](https://codecov.io/gh/uutils/diffutils) +[![CodeCov](https://codecov.io/gh/uutils/diffutils/branch/main/graph/badge.svg)](https://codecov.io/gh/uutils/diffutils) The goal of this package is to be a drop-in replacement for the [diffutils commands](https://www.gnu.org/software/diffutils/) in Rust. From a213272d0c8a8490258e8e983c31963a129b962b Mon Sep 17 00:00:00 2001 From: Olivier Tilloy Date: Tue, 2 Apr 2024 20:17:02 +0200 Subject: [PATCH 057/194] Add tests for when '-' is used to signify to use standard input --- src/params.rs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/params.rs b/src/params.rs index 6a6d261..b913a40 100644 --- a/src/params.rs +++ b/src/params.rs @@ -457,6 +457,35 @@ mod tests { ); } #[test] + fn default_to_stdin() { + assert_eq!( + Ok(Params { + from: os("foo"), + to: os("/dev/stdin"), + ..Default::default() + }), + parse_params([os("diff"), os("foo"), os("-")].iter().cloned()) + ); + assert_eq!( + Ok(Params { + from: os("/dev/stdin"), + to: os("bar"), + ..Default::default() + }), + parse_params([os("diff"), os("-"), os("bar")].iter().cloned()) + ); + assert_eq!( + Ok(Params { + from: os("/dev/stdin"), + to: os("/dev/stdin"), + ..Default::default() + }), + parse_params([os("diff"), os("-"), os("-")].iter().cloned()) + ); + assert!(parse_params([os("diff"), os("foo"), os("bar"), os("-")].iter().cloned()).is_err()); + assert!(parse_params([os("diff"), os("-"), os("-"), os("-")].iter().cloned()).is_err()); + } + #[test] fn unknown_argument() { assert!( parse_params([os("diff"), os("-g"), os("foo"), os("bar")].iter().cloned()).is_err() From b8fada8faa568e8e8024632bf8184a8194274732 Mon Sep 17 00:00:00 2001 From: Olivier Tilloy Date: Tue, 2 Apr 2024 21:10:41 +0200 Subject: [PATCH 058/194] Add unit tests for missing arguments --- src/params.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/params.rs b/src/params.rs index b913a40..7017646 100644 --- a/src/params.rs +++ b/src/params.rs @@ -486,6 +486,11 @@ mod tests { assert!(parse_params([os("diff"), os("-"), os("-"), os("-")].iter().cloned()).is_err()); } #[test] + fn missing_arguments() { + assert!(parse_params([os("diff")].iter().cloned()).is_err()); + assert!(parse_params([os("diff"), os("foo")].iter().cloned()).is_err()); + } + #[test] fn unknown_argument() { assert!( parse_params([os("diff"), os("-g"), os("foo"), os("bar")].iter().cloned()).is_err() From 6a152cdc7f203408331c3e337a86b2200303e1a3 Mon Sep 17 00:00:00 2001 From: Olivier Tilloy Date: Tue, 2 Apr 2024 22:34:42 +0200 Subject: [PATCH 059/194] Add an integration test for reading data from stdin --- tests/integration.rs | 46 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/tests/integration.rs b/tests/integration.rs index be1320e..5ad624e 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -3,10 +3,9 @@ // For the full copyright and license information, please view the LICENSE-* // files that was distributed with this source code. -use assert_cmd::prelude::*; +use assert_cmd::cmd::Command; use predicates::prelude::*; use std::io::Write; -use std::process::Command; use tempfile::NamedTempFile; // Integration tests for the diffutils command @@ -161,3 +160,46 @@ fn missing_newline() -> Result<(), Box> { .stderr(predicate::str::starts_with("No newline at end of file")); Ok(()) } + +#[test] +fn read_from_stdin() -> Result<(), Box> { + let mut file1 = NamedTempFile::new()?; + file1.write_all("foo\n".as_bytes())?; + let mut file2 = NamedTempFile::new()?; + file2.write_all("bar\n".as_bytes())?; + + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("-u") + .arg(file1.path()) + .arg("-") + .write_stdin("bar\n"); + cmd.assert() + .code(predicate::eq(1)) + .failure() + .stdout(predicate::eq(format!( + "--- {}\t\n+++ /dev/stdin\t\n@@ -1 +1 @@\n-foo\n+bar\n", + file1.path().to_string_lossy() + ))); + + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("-u") + .arg("-") + .arg(file2.path()) + .write_stdin("foo\n"); + cmd.assert() + .code(predicate::eq(1)) + .failure() + .stdout(predicate::eq(format!( + "--- /dev/stdin\t\n+++ {}\t\n@@ -1 +1 @@\n-foo\n+bar\n", + file2.path().to_string_lossy() + ))); + + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("-u").arg("-").arg("-").write_stdin("foo\n"); + cmd.assert() + .code(predicate::eq(0)) + .success() + .stdout(predicate::str::is_empty()); + + Ok(()) +} From 5b814f853040a0d2e6204b65e8e19e2354fe72f1 Mon Sep 17 00:00:00 2001 From: Tanmay Patil Date: Wed, 3 Apr 2024 10:50:52 +0530 Subject: [PATCH 060/194] Fix tests --- src/context_diff.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/context_diff.rs b/src/context_diff.rs index 9254e9b..d60b6ef 100644 --- a/src/context_diff.rs +++ b/src/context_diff.rs @@ -444,6 +444,7 @@ mod tests { bet.write_all(b"l\n").unwrap(); } let _ = File::create(&format!("{target}/aalef")).unwrap(); + let mut fa = File::create(&format!("{target}/alef")).unwrap(); // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff( @@ -460,7 +461,6 @@ mod tests { .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create(&format!("{target}/alef")).unwrap(); fa.write_all(&alef[..]).unwrap(); let mut fb = File::create(&format!("{target}/bet")).unwrap(); fb.write_all(&bet[..]).unwrap(); @@ -526,6 +526,7 @@ mod tests { bet.write_all(b"l\n").unwrap(); } let _ = File::create(&format!("{target}/aalef_")).unwrap(); + let mut fa = File::create(&format!("{target}/alef_")).unwrap(); // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff( @@ -542,7 +543,6 @@ mod tests { .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create(&format!("{target}/alef_")).unwrap(); fa.write_all(&alef[..]).unwrap(); let mut fb = File::create(&format!("{target}/bet_")).unwrap(); fb.write_all(&bet[..]).unwrap(); @@ -611,6 +611,7 @@ mod tests { continue; }; let _ = File::create(&format!("{target}/aalefx")).unwrap(); + let mut fa = File::create(&format!("{target}/alefx")).unwrap(); // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff( @@ -627,7 +628,6 @@ mod tests { .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create(&format!("{target}/alefx")).unwrap(); fa.write_all(&alef[..]).unwrap(); let mut fb = File::create(&format!("{target}/betx")).unwrap(); fb.write_all(&bet[..]).unwrap(); @@ -699,6 +699,7 @@ mod tests { bet.write_all(b"f\n").unwrap(); } let _ = File::create(&format!("{target}/aalefr")).unwrap(); + let mut fa = File::create(&format!("{target}/alefr")).unwrap(); // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff( @@ -715,7 +716,6 @@ mod tests { .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create(&format!("{target}/alefr")).unwrap(); fa.write_all(&alef[..]).unwrap(); let mut fb = File::create(&format!("{target}/betr")).unwrap(); fb.write_all(&bet[..]).unwrap(); From a3a372ff362992df34193c2b8d4c9026f9eb576e Mon Sep 17 00:00:00 2001 From: Tanmay Patil Date: Thu, 4 Apr 2024 00:09:57 +0530 Subject: [PATCH 061/194] Display modification times of input files in unified diff --- src/context_diff.rs | 19 +----------- src/unified_diff.rs | 70 +++++++++++++++++++++++++++++++++------------ src/utils.rs | 16 +++++++++++ 3 files changed, 68 insertions(+), 37 deletions(-) diff --git a/src/context_diff.rs b/src/context_diff.rs index d60b6ef..b34295f 100644 --- a/src/context_diff.rs +++ b/src/context_diff.rs @@ -8,6 +8,7 @@ use std::io::Write; use crate::params::Params; use crate::utils::do_write_line; +use crate::utils::get_modification_time; #[derive(Debug, PartialEq)] pub enum DiffLine { @@ -265,24 +266,7 @@ fn make_diff( results } -fn get_modification_time(file_path: &str) -> String { - use chrono::{DateTime, Local}; - use std::fs; - - let metadata = fs::metadata(file_path).expect("Failed to get metadata"); - let modification_time = metadata - .modified() - .expect("Failed to get modification time"); - let modification_time: DateTime = modification_time.into(); - let modification_time: String = modification_time - .format("%Y-%m-%d %H:%M:%S%.9f %z") - .to_string(); - - modification_time -} - #[must_use] -#[allow(clippy::too_many_arguments)] pub fn diff(expected: &[u8], actual: &[u8], params: &Params) -> Vec { let from_modified_time = get_modification_time(¶ms.from.to_string_lossy()); let to_modified_time = get_modification_time(¶ms.to.to_string_lossy()); @@ -747,7 +731,6 @@ mod tests { use std::str; let target = "target/context-diff"; - // test all possible six-line files. let _ = std::fs::create_dir(target); let from_filename = &format!("{target}/foo"); let _ = File::create(from_filename).unwrap(); diff --git a/src/unified_diff.rs b/src/unified_diff.rs index 3af51c2..c5eab49 100644 --- a/src/unified_diff.rs +++ b/src/unified_diff.rs @@ -8,6 +8,7 @@ use std::io::Write; use crate::params::Params; use crate::utils::do_write_line; +use crate::utils::get_modification_time; #[derive(Debug, PartialEq)] pub enum DiffLine { @@ -238,10 +239,14 @@ fn make_diff( #[must_use] pub fn diff(expected: &[u8], actual: &[u8], params: &Params) -> Vec { + let from_modified_time = get_modification_time(¶ms.from.to_string_lossy()); + let to_modified_time = get_modification_time(¶ms.to.to_string_lossy()); let mut output = format!( - "--- {0}\t\n+++ {1}\t\n", + "--- {0}\t{1}\n+++ {2}\t{3}\n", params.from.to_string_lossy(), - params.to.to_string_lossy() + from_modified_time, + params.to.to_string_lossy(), + to_modified_time ) .into_bytes(); let diff_results = make_diff(expected, actual, params.context_count, params.brief); @@ -449,13 +454,15 @@ mod tests { if f != 2 { bet.write_all(b"l\n").unwrap(); } + let _ = File::create(&format!("{target}/aalef")).unwrap(); + let mut fa = File::create(&format!("{target}/alef")).unwrap(); // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff( &alef, &bet, &Params { - from: "a/alef".into(), + from: (&format!("{target}/aalef")).into(), to: (&format!("{target}/alef")).into(), context_count: 2, ..Default::default() @@ -465,7 +472,6 @@ mod tests { .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create(&format!("{target}/alef")).unwrap(); fa.write_all(&alef[..]).unwrap(); let mut fb = File::create(&format!("{target}/bet")).unwrap(); fb.write_all(&bet[..]).unwrap(); @@ -565,13 +571,15 @@ mod tests { } _ => unreachable!(), } + let _ = File::create(&format!("{target}/aalefn")).unwrap(); + let mut fa = File::create(&format!("{target}/alefn")).unwrap(); // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff( &alef, &bet, &Params { - from: "a/alefn".into(), + from: (&format!("{target}/aalefn")).into(), to: (&format!("{target}/alefn")).into(), context_count: 2, ..Default::default() @@ -581,7 +589,6 @@ mod tests { .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create(&format!("{target}/alefn")).unwrap(); fa.write_all(&alef[..]).unwrap(); let mut fb = File::create(&format!("{target}/betn")).unwrap(); fb.write_all(&bet[..]).unwrap(); @@ -661,13 +668,15 @@ mod tests { 3 => {} _ => unreachable!(), } + let _ = File::create(&format!("{target}/aalef_")).unwrap(); + let mut fa = File::create(&format!("{target}/alef_")).unwrap(); // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff( &alef, &bet, &Params { - from: "a/alef_".into(), + from: (&format!("{target}/aalef_")).into(), to: (&format!("{target}/alef_")).into(), context_count: 2, ..Default::default() @@ -677,7 +686,6 @@ mod tests { .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create(&format!("{target}/alef_")).unwrap(); fa.write_all(&alef[..]).unwrap(); let mut fb = File::create(&format!("{target}/bet_")).unwrap(); fb.write_all(&bet[..]).unwrap(); @@ -742,13 +750,15 @@ mod tests { if f != 2 { bet.write_all(b"l\n").unwrap(); } + let _ = File::create(&format!("{target}/aalefx")).unwrap(); + let mut fa = File::create(&format!("{target}/alefx")).unwrap(); // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff( &alef, &bet, &Params { - from: "a/alefx".into(), + from: (&format!("{target}/aalefx")).into(), to: (&format!("{target}/alefx")).into(), context_count: 2, ..Default::default() @@ -758,7 +768,6 @@ mod tests { .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create(&format!("{target}/alefx")).unwrap(); fa.write_all(&alef[..]).unwrap(); let mut fb = File::create(&format!("{target}/betx")).unwrap(); fb.write_all(&bet[..]).unwrap(); @@ -828,13 +837,15 @@ mod tests { if f != 2 { bet.write_all(b"f\n").unwrap(); } + let _ = File::create(&format!("{target}/aalefr")).unwrap(); + let mut fa = File::create(&format!("{target}/alefr")).unwrap(); // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff( &alef, &bet, &Params { - from: "a/alefr".into(), + from: (&format!("{target}/aalefr")).into(), to: (&format!("{target}/alefr")).into(), context_count: 2, ..Default::default() @@ -844,7 +855,6 @@ mod tests { .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create(&format!("{target}/alefr")).unwrap(); fa.write_all(&alef[..]).unwrap(); let mut fb = File::create(&format!("{target}/betr")).unwrap(); fb.write_all(&bet[..]).unwrap(); @@ -870,9 +880,17 @@ mod tests { #[test] fn test_stop_early() { - let from_filename = "foo"; + use regex::Regex; + use std::fs::File; + use std::str; + + let target = "target/context-diff"; + let _ = std::fs::create_dir(target); + let from_filename = &format!("{target}/foo"); + let _ = File::create(from_filename).unwrap(); let from = ["a", "b", "c", ""].join("\n"); - let to_filename = "bar"; + let to_filename = &format!("{target}/bar"); + let _ = File::create(to_filename).unwrap(); let to = ["a", "d", "c", ""].join("\n"); let diff_full = diff( @@ -884,9 +902,14 @@ mod tests { ..Default::default() }, ); + + let diff_full_text = str::from_utf8(&diff_full).unwrap(); + let re = Regex::new(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+ [+-]\d{4}").unwrap(); + let diff_full = re.replace_all(diff_full_text, ""); + let expected_full = [ - "--- foo\t", - "+++ bar\t", + "--- target/context-diff/foo\t", + "+++ target/context-diff/bar\t", "@@ -1,3 +1,3 @@", " a", "-b", @@ -895,7 +918,7 @@ mod tests { "", ] .join("\n"); - assert_eq!(diff_full, expected_full.as_bytes()); + assert_eq!(diff_full, expected_full); let diff_brief = diff( from.as_bytes(), @@ -907,8 +930,17 @@ mod tests { ..Default::default() }, ); - let expected_brief = ["--- foo\t", "+++ bar\t", ""].join("\n"); - assert_eq!(diff_brief, expected_brief.as_bytes()); + + let diff_brief_text = str::from_utf8(&diff_brief).unwrap(); + let re = Regex::new(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+ [+-]\d{4}").unwrap(); + let diff_brief = re.replace_all(diff_brief_text, ""); + let expected_brief = [ + "--- target/context-diff/foo\t", + "+++ target/context-diff/bar\t", + "", + ] + .join("\n"); + assert_eq!(diff_brief, expected_brief); let nodiff_full = diff( from.as_bytes(), diff --git a/src/utils.rs b/src/utils.rs index 94d950f..e05fca6 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -51,6 +51,22 @@ pub fn do_write_line( } } +pub fn get_modification_time(file_path: &str) -> String { + use chrono::{DateTime, Local}; + use std::fs; + + let metadata = fs::metadata(file_path).expect("Failed to get metadata"); + let modification_time = metadata + .modified() + .expect("Failed to get modification time"); + let modification_time: DateTime = modification_time.into(); + let modification_time: String = modification_time + .format("%Y-%m-%d %H:%M:%S%.9f %z") + .to_string(); + + modification_time +} + #[cfg(test)] mod tests { use super::*; From 281098d75157944a1b1815ccaea271f501c5c25f Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Wed, 3 Apr 2024 23:25:31 +0200 Subject: [PATCH 062/194] Run clippy pedantic fixes --- src/context_diff.rs | 8 ++++---- src/ed_diff.rs | 8 ++++---- src/main.rs | 2 +- src/normal_diff.rs | 12 +++++------- src/params.rs | 2 +- src/unified_diff.rs | 10 +++++----- src/utils.rs | 1 + 7 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/context_diff.rs b/src/context_diff.rs index 9c567d8..f0c1caa 100644 --- a/src/context_diff.rs +++ b/src/context_diff.rs @@ -450,7 +450,7 @@ mod tests { .stdin(File::open(&format!("{target}/ab.diff")).unwrap()) .output() .unwrap(); - assert!(output.status.success(), "{:?}", output); + assert!(output.status.success(), "{output:?}"); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); let alef = fs::read(&format!("{target}/alef")).unwrap(); @@ -531,7 +531,7 @@ mod tests { .stdin(File::open(&format!("{target}/ab_.diff")).unwrap()) .output() .unwrap(); - assert!(output.status.success(), "{:?}", output); + assert!(output.status.success(), "{output:?}"); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); let alef = fs::read(&format!("{target}/alef_")).unwrap(); @@ -615,7 +615,7 @@ mod tests { .stdin(File::open(&format!("{target}/abx.diff")).unwrap()) .output() .unwrap(); - assert!(output.status.success(), "{:?}", output); + assert!(output.status.success(), "{output:?}"); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); let alef = fs::read(&format!("{target}/alefx")).unwrap(); @@ -702,7 +702,7 @@ mod tests { .stdin(File::open(&format!("{target}/abr.diff")).unwrap()) .output() .unwrap(); - assert!(output.status.success(), "{:?}", output); + assert!(output.status.success(), "{output:?}"); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); let alef = fs::read(&format!("{target}/alefr")).unwrap(); diff --git a/src/ed_diff.rs b/src/ed_diff.rs index 1be37a5..d2d8499 100644 --- a/src/ed_diff.rs +++ b/src/ed_diff.rs @@ -133,7 +133,7 @@ pub fn diff(expected: &[u8], actual: &[u8], params: &Params) -> Result, expected_count + line_number_expected - 1 ) .unwrap(), - (1, _) => writeln!(&mut output, "{}c", line_number_expected).unwrap(), + (1, _) => writeln!(&mut output, "{line_number_expected}c").unwrap(), _ => writeln!( &mut output, "{},{}c", @@ -241,7 +241,7 @@ mod tests { .stdin(File::open("target/ab.ed").unwrap()) .output() .unwrap(); - assert!(output.status.success(), "{:?}", output); + assert!(output.status.success(), "{output:?}"); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); let alef = fs::read(&format!("{target}/alef")).unwrap(); @@ -312,7 +312,7 @@ mod tests { .stdin(File::open("target/ab_.ed").unwrap()) .output() .unwrap(); - assert!(output.status.success(), "{:?}", output); + assert!(output.status.success(), "{output:?}"); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); let alef = fs::read("target/alef_").unwrap(); @@ -389,7 +389,7 @@ mod tests { .stdin(File::open("target/abr.ed").unwrap()) .output() .unwrap(); - assert!(output.status.success(), "{:?}", output); + assert!(output.status.success(), "{output:?}"); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); let alef = fs::read(&format!("{target}/alefr")).unwrap(); diff --git a/src/main.rs b/src/main.rs index dab0eff..a19e646 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,7 +35,7 @@ fn main() -> ExitCode { "Files {} and {} are identical", params.from.to_string_lossy(), params.to.to_string_lossy(), - ) + ); } }; if same_file::is_same_file(¶ms.from, ¶ms.to).unwrap_or(false) { diff --git a/src/normal_diff.rs b/src/normal_diff.rs index cfa389c..d6f8297 100644 --- a/src/normal_diff.rs +++ b/src/normal_diff.rs @@ -155,9 +155,7 @@ pub fn diff(expected: &[u8], actual: &[u8], params: &Params) -> Vec { // 'c' stands for "Change lines" // exactly one line replaced by one line &mut output, - "{}c{}", - line_number_expected, - line_number_actual + "{line_number_expected}c{line_number_actual}" ) .unwrap(), (1, _) => writeln!( @@ -293,7 +291,7 @@ mod tests { .stdin(File::open(&format!("{target}/ab.diff")).unwrap()) .output() .unwrap(); - assert!(output.status.success(), "{:?}", output); + assert!(output.status.success(), "{output:?}"); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); let alef = fs::read(&format!("{target}/alef")).unwrap(); @@ -386,7 +384,7 @@ mod tests { .stdin(File::open(&format!("{target}/abn.diff")).unwrap()) .output() .unwrap(); - assert!(output.status.success(), "{:?}", output); + assert!(output.status.success(), "{output:?}"); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); let alef = fs::read(&format!("{target}/alefn")).unwrap(); @@ -459,7 +457,7 @@ mod tests { .stdin(File::open(&format!("{target}/ab_.diff")).unwrap()) .output() .unwrap(); - assert!(output.status.success(), "{:?}", output); + assert!(output.status.success(), "{output:?}"); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); let alef = fs::read(&format!("{target}/alef_")).unwrap(); @@ -537,7 +535,7 @@ mod tests { .stdin(File::open(&format!("{target}/abr.diff")).unwrap()) .output() .unwrap(); - assert!(output.status.success(), "{:?}", output); + assert!(output.status.success(), "{output:?}"); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); let alef = fs::read(&format!("{target}/alefr")).unwrap(); diff --git a/src/params.rs b/src/params.rs index 7017646..74e8d15 100644 --- a/src/params.rs +++ b/src/params.rs @@ -99,7 +99,7 @@ pub fn parse_params>(opts: I) -> Result() { Ok(num) => num, - Err(_) => return Err(format!("invalid tabsize «{}»", tabsize_str)), + Err(_) => return Err(format!("invalid tabsize «{tabsize_str}»")), }; continue; } diff --git a/src/unified_diff.rs b/src/unified_diff.rs index 3af51c2..97ce764 100644 --- a/src/unified_diff.rs +++ b/src/unified_diff.rs @@ -494,7 +494,7 @@ mod tests { .unwrap(); println!("{}", String::from_utf8_lossy(&output.stdout)); println!("{}", String::from_utf8_lossy(&output.stderr)); - assert!(output.status.success(), "{:?}", output); + assert!(output.status.success(), "{output:?}"); let alef = fs::read(&format!("{target}/alef")).unwrap(); assert_eq!(alef, bet); } @@ -592,7 +592,7 @@ mod tests { .stdin(File::open(&format!("{target}/abn.diff")).unwrap()) .output() .unwrap(); - assert!(output.status.success(), "{:?}", output); + assert!(output.status.success(), "{output:?}"); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); let alef = fs::read(&format!("{target}/alefn")).unwrap(); @@ -688,7 +688,7 @@ mod tests { .stdin(File::open(&format!("{target}/ab_.diff")).unwrap()) .output() .unwrap(); - assert!(output.status.success(), "{:?}", output); + assert!(output.status.success(), "{output:?}"); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); let alef = fs::read(&format!("{target}/alef_")).unwrap(); @@ -769,7 +769,7 @@ mod tests { .stdin(File::open(&format!("{target}/abx.diff")).unwrap()) .output() .unwrap(); - assert!(output.status.success(), "{:?}", output); + assert!(output.status.success(), "{output:?}"); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); let alef = fs::read(&format!("{target}/alefx")).unwrap(); @@ -855,7 +855,7 @@ mod tests { .stdin(File::open(&format!("{target}/abr.diff")).unwrap()) .output() .unwrap(); - assert!(output.status.success(), "{:?}", output); + assert!(output.status.success(), "{output:?}"); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); let alef = fs::read(&format!("{target}/alefr")).unwrap(); diff --git a/src/utils.rs b/src/utils.rs index 94d950f..c503fd9 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -10,6 +10,7 @@ use unicode_width::UnicodeWidthStr; /// Replace tabs by spaces in the input line. /// Correctly handle multi-bytes characters. /// This assumes that line does not contain any line breaks (if it does, the result is undefined). +#[must_use] pub fn do_expand_tabs(line: &[u8], tabsize: usize) -> Vec { let tab = b'\t'; let ntabs = line.iter().filter(|c| **c == tab).count(); From bbfca84e1776d54a326925d289d02a931832701d Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Thu, 4 Apr 2024 00:29:50 +0200 Subject: [PATCH 063/194] chore: wow shiny new cargo-dist CI --- .github/workflows/release.yml | 266 ++++++++++++++++++++++++++++++++++ Cargo.toml | 18 +++ 2 files changed, 284 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..502a5ff --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,266 @@ +# Copyright 2022-2023, axodotdev +# SPDX-License-Identifier: MIT or Apache-2.0 +# +# CI that: +# +# * checks for a Git Tag that looks like a release +# * builds artifacts with cargo-dist (archives, installers, hashes) +# * uploads those artifacts to temporary workflow zip +# * on success, uploads the artifacts to a Github Release +# +# Note that the Github Release will be created with a generated +# title/body based on your changelogs. + +name: Release + +permissions: + contents: write + +# This task will run whenever you push a git tag that looks like a version +# like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc. +# Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where +# PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION +# must be a Cargo-style SemVer Version (must have at least major.minor.patch). +# +# If PACKAGE_NAME is specified, then the announcement will be for that +# package (erroring out if it doesn't have the given version or isn't cargo-dist-able). +# +# If PACKAGE_NAME isn't specified, then the announcement will be for all +# (cargo-dist-able) packages in the workspace with that version (this mode is +# intended for workspaces with only one dist-able package, or with all dist-able +# packages versioned/released in lockstep). +# +# If you push multiple tags at once, separate instances of this workflow will +# spin up, creating an independent announcement for each one. However Github +# will hard limit this to 3 tags per commit, as it will assume more tags is a +# mistake. +# +# If there's a prerelease-style suffix to the version, then the release(s) +# will be marked as a prerelease. +on: + push: + tags: + - '**[0-9]+.[0-9]+.[0-9]+*' + pull_request: + +jobs: + # Run 'cargo dist plan' (or host) to determine what tasks we need to do + plan: + runs-on: ubuntu-latest + outputs: + val: ${{ steps.plan.outputs.manifest }} + tag: ${{ !github.event.pull_request && github.ref_name || '' }} + tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }} + publishing: ${{ !github.event.pull_request }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install cargo-dist + # we specify bash to get pipefail; it guards against the `curl` command + # failing. otherwise `sh` won't catch that `curl` returned non-0 + shell: bash + run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.12.0/cargo-dist-installer.sh | sh" + # sure would be cool if github gave us proper conditionals... + # so here's a doubly-nested ternary-via-truthiness to try to provide the best possible + # functionality based on whether this is a pull_request, and whether it's from a fork. + # (PRs run on the *source* but secrets are usually on the *target* -- that's *good* + # but also really annoying to build CI around when it needs secrets to work right.) + - id: plan + run: | + cargo dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json + echo "cargo dist ran successfully" + cat plan-dist-manifest.json + echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT" + - name: "Upload dist-manifest.json" + uses: actions/upload-artifact@v4 + with: + name: artifacts-plan-dist-manifest + path: plan-dist-manifest.json + + # Build and packages all the platform-specific things + build-local-artifacts: + name: build-local-artifacts (${{ join(matrix.targets, ', ') }}) + # Let the initial task tell us to not run (currently very blunt) + needs: + - plan + if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }} + strategy: + fail-fast: false + # Target platforms/runners are computed by cargo-dist in create-release. + # Each member of the matrix has the following arguments: + # + # - runner: the github runner + # - dist-args: cli flags to pass to cargo dist + # - install-dist: expression to run to install cargo-dist on the runner + # + # Typically there will be: + # - 1 "global" task that builds universal installers + # - N "local" tasks that build each platform's binaries and platform-specific installers + matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }} + runs-on: ${{ matrix.runner }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - uses: swatinem/rust-cache@v2 + - name: Install cargo-dist + run: ${{ matrix.install_dist }} + # Get the dist-manifest + - name: Fetch local artifacts + uses: actions/download-artifact@v4 + with: + pattern: artifacts-* + path: target/distrib/ + merge-multiple: true + - name: Install dependencies + run: | + ${{ matrix.packages_install }} + - name: Build artifacts + run: | + # Actually do builds and make zips and whatnot + cargo dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json + echo "cargo dist ran successfully" + - id: cargo-dist + name: Post-build + # We force bash here just because github makes it really hard to get values up + # to "real" actions without writing to env-vars, and writing to env-vars has + # inconsistent syntax between shell and powershell. + shell: bash + run: | + # Parse out what we just built and upload it to scratch storage + echo "paths<> "$GITHUB_OUTPUT" + jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + + cp dist-manifest.json "$BUILD_MANIFEST_NAME" + - name: "Upload artifacts" + uses: actions/upload-artifact@v4 + with: + name: artifacts-build-local-${{ join(matrix.targets, '_') }} + path: | + ${{ steps.cargo-dist.outputs.paths }} + ${{ env.BUILD_MANIFEST_NAME }} + + # Build and package all the platform-agnostic(ish) things + build-global-artifacts: + needs: + - plan + - build-local-artifacts + runs-on: "ubuntu-20.04" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install cargo-dist + shell: bash + run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.12.0/cargo-dist-installer.sh | sh" + # Get all the local artifacts for the global tasks to use (for e.g. checksums) + - name: Fetch local artifacts + uses: actions/download-artifact@v4 + with: + pattern: artifacts-* + path: target/distrib/ + merge-multiple: true + - id: cargo-dist + shell: bash + run: | + cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json + echo "cargo dist ran successfully" + + # Parse out what we just built and upload it to scratch storage + echo "paths<> "$GITHUB_OUTPUT" + jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + + cp dist-manifest.json "$BUILD_MANIFEST_NAME" + - name: "Upload artifacts" + uses: actions/upload-artifact@v4 + with: + name: artifacts-build-global + path: | + ${{ steps.cargo-dist.outputs.paths }} + ${{ env.BUILD_MANIFEST_NAME }} + # Determines if we should publish/announce + host: + needs: + - plan + - build-local-artifacts + - build-global-artifacts + # Only run if we're "publishing", and only if local and global didn't fail (skipped is fine) + if: ${{ always() && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + runs-on: "ubuntu-20.04" + outputs: + val: ${{ steps.host.outputs.manifest }} + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install cargo-dist + run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.12.0/cargo-dist-installer.sh | sh" + # Fetch artifacts from scratch-storage + - name: Fetch artifacts + uses: actions/download-artifact@v4 + with: + pattern: artifacts-* + path: target/distrib/ + merge-multiple: true + # This is a harmless no-op for Github Releases, hosting for that happens in "announce" + - id: host + shell: bash + run: | + cargo dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json + echo "artifacts uploaded and released successfully" + cat dist-manifest.json + echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" + - name: "Upload dist-manifest.json" + uses: actions/upload-artifact@v4 + with: + # Overwrite the previous copy + name: artifacts-dist-manifest + path: dist-manifest.json + + # Create a Github Release while uploading all files to it + announce: + needs: + - plan + - host + # use "always() && ..." to allow us to wait for all publish jobs while + # still allowing individual publish jobs to skip themselves (for prereleases). + # "host" however must run to completion, no skipping allowed! + if: ${{ always() && needs.host.result == 'success' }} + runs-on: "ubuntu-20.04" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: "Download Github Artifacts" + uses: actions/download-artifact@v4 + with: + pattern: artifacts-* + path: artifacts + merge-multiple: true + - name: Cleanup + run: | + # Remove the granular manifests + rm -f artifacts/*-dist-manifest.json + - name: Create Github Release + uses: ncipollo/release-action@v1 + with: + tag: ${{ needs.plan.outputs.tag }} + name: ${{ fromJson(needs.host.outputs.val).announcement_title }} + body: ${{ fromJson(needs.host.outputs.val).announcement_github_body }} + prerelease: ${{ fromJson(needs.host.outputs.val).announcement_is_prerelease }} + artifacts: "artifacts/*" diff --git a/Cargo.toml b/Cargo.toml index 4ddf5fb..7a3126d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,3 +25,21 @@ pretty_assertions = "1" assert_cmd = "2.0.14" predicates = "3.1.0" tempfile = "3.10.0" + +# The profile that 'cargo dist' will build with +[profile.dist] +inherits = "release" +lto = "thin" + +# Config for 'cargo dist' +[workspace.metadata.dist] +# The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax) +cargo-dist-version = "0.12.0" +# CI backends to support +ci = ["github"] +# The installers to generate for each app +installers = [] +# Target platforms to build apps for (Rust target-triple syntax) +targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"] +# Publish jobs to run in CI +pr-run-mode = "plan" From 44ef772e4ae5c3e388995162b0e428613d55a4b8 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Thu, 4 Apr 2024 00:30:46 +0200 Subject: [PATCH 064/194] release: version 0.4.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 15c81e6..43bbdbf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -75,7 +75,7 @@ checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" [[package]] name = "diffutils" -version = "0.3.0" +version = "0.4.0" dependencies = [ "assert_cmd", "diff", diff --git a/Cargo.toml b/Cargo.toml index 7a3126d..b512a7b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "diffutils" -version = "0.3.0" +version = "0.4.0" edition = "2021" description = "A CLI app for generating diff files" license = "MIT OR Apache-2.0" From 6be94d8683fea64d50eecb92c81e485c35033794 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 3 Apr 2024 22:31:44 +0000 Subject: [PATCH 065/194] chore(deps): update rust crate tempfile to 3.10.1 --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 43bbdbf..69c69d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -293,9 +293,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.10.0" +version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand", diff --git a/Cargo.toml b/Cargo.toml index b512a7b..6de6dcc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ unicode-width = "0.1.11" pretty_assertions = "1" assert_cmd = "2.0.14" predicates = "3.1.0" -tempfile = "3.10.0" +tempfile = "3.10.1" # The profile that 'cargo dist' will build with [profile.dist] From 2d9e625a5bea309ef88e25677775d73e4c4fa322 Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Thu, 4 Apr 2024 08:30:54 +0200 Subject: [PATCH 066/194] Disable tests on Windows that use ed --- src/ed_diff.rs | 45 +++++++++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/src/ed_diff.rs b/src/ed_diff.rs index d2d8499..ba39046 100644 --- a/src/ed_diff.rs +++ b/src/ed_diff.rs @@ -236,12 +236,15 @@ mod tests { fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; - let output = Command::new("ed") - .arg(&format!("{target}/alef")) - .stdin(File::open("target/ab.ed").unwrap()) - .output() - .unwrap(); - assert!(output.status.success(), "{output:?}"); + #[cfg(not(windows))] // there's no ed on windows + { + let output = Command::new("ed") + .arg(&format!("{target}/alef")) + .stdin(File::open("target/ab.ed").unwrap()) + .output() + .unwrap(); + assert!(output.status.success(), "{output:?}"); + } //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); let alef = fs::read(&format!("{target}/alef")).unwrap(); @@ -307,12 +310,15 @@ mod tests { fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; - let output = Command::new("ed") - .arg("target/alef_") - .stdin(File::open("target/ab_.ed").unwrap()) - .output() - .unwrap(); - assert!(output.status.success(), "{output:?}"); + #[cfg(not(windows))] // there's no ed on windows + { + let output = Command::new("ed") + .arg("target/alef_") + .stdin(File::open("target/ab_.ed").unwrap()) + .output() + .unwrap(); + assert!(output.status.success(), "{output:?}"); + } //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); let alef = fs::read("target/alef_").unwrap(); @@ -384,12 +390,15 @@ mod tests { fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; - let output = Command::new("ed") - .arg(&format!("{target}/alefr")) - .stdin(File::open("target/abr.ed").unwrap()) - .output() - .unwrap(); - assert!(output.status.success(), "{output:?}"); + #[cfg(not(windows))] // there's no ed on windows + { + let output = Command::new("ed") + .arg(&format!("{target}/alefr")) + .stdin(File::open("target/abr.ed").unwrap()) + .output() + .unwrap(); + assert!(output.status.success(), "{output:?}"); + } //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); let alef = fs::read(&format!("{target}/alefr")).unwrap(); From 72da7fca402680988b3846e625d40fe00165f656 Mon Sep 17 00:00:00 2001 From: Tanmay Patil Date: Thu, 4 Apr 2024 20:01:11 +0530 Subject: [PATCH 067/194] Show current time if fs::metadata errors --- src/utils.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index 1b600c1..e0592ef 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -55,11 +55,12 @@ pub fn do_write_line( pub fn get_modification_time(file_path: &str) -> String { use chrono::{DateTime, Local}; use std::fs; + use std::time::SystemTime; + + let modification_time: SystemTime = fs::metadata(file_path) + .and_then(|m| m.modified()) + .unwrap_or(SystemTime::now()); - let metadata = fs::metadata(file_path).expect("Failed to get metadata"); - let modification_time = metadata - .modified() - .expect("Failed to get modification time"); let modification_time: DateTime = modification_time.into(); let modification_time: String = modification_time .format("%Y-%m-%d %H:%M:%S%.9f %z") From c325291696b10de5a8fde65b5322d40163e2f74d Mon Sep 17 00:00:00 2001 From: Olivier Tilloy Date: Fri, 5 Apr 2024 23:22:26 +0200 Subject: [PATCH 068/194] Unit test to verify that conflicting output styles result in an error --- src/params.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/params.rs b/src/params.rs index 74e8d15..18f8949 100644 --- a/src/params.rs +++ b/src/params.rs @@ -502,4 +502,15 @@ mod tests { fn empty() { assert!(parse_params([].iter().cloned()).is_err()); } + #[test] + fn conflicting_output_styles() { + for (arg1, arg2) in [("-u", "-c"), ("-u", "-e"), ("-c", "-u"), ("-c", "-U42")] { + assert!(parse_params( + [os("diff"), os(arg1), os(arg2), os("foo"), os("bar")] + .iter() + .cloned() + ) + .is_err()); + } + } } From 6dc34fed44ea7bd8f87de41b40785f9cd6486cba Mon Sep 17 00:00:00 2001 From: Olivier Tilloy Date: Wed, 3 Apr 2024 22:18:12 +0200 Subject: [PATCH 069/194] Handle the rewrite of "-" to "/dev/stdin" in main to leave the filenames unchanged (fixes #46) --- src/main.rs | 14 ++++++++++---- src/params.rs | 12 ++++++------ tests/integration.rs | 27 ++++++++++++++++----------- 3 files changed, 32 insertions(+), 21 deletions(-) diff --git a/src/main.rs b/src/main.rs index a19e646..de4bbbb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,7 @@ use crate::params::{parse_params, Format}; use std::env; - +use std::ffi::OsString; use std::fs; use std::io::{self, Write}; use std::process::{exit, ExitCode}; @@ -38,19 +38,25 @@ fn main() -> ExitCode { ); } }; - if same_file::is_same_file(¶ms.from, ¶ms.to).unwrap_or(false) { + if params.from == "-" && params.to == "-" + || same_file::is_same_file(¶ms.from, ¶ms.to).unwrap_or(false) + { maybe_report_identical_files(); return ExitCode::SUCCESS; } // read files - let from_content = match fs::read(¶ms.from) { + fn read_file_contents(filepath: &OsString) -> io::Result> { + let stdin = OsString::from("/dev/stdin"); + fs::read(if filepath == "-" { &stdin } else { filepath }) + } + let from_content = match read_file_contents(¶ms.from) { Ok(from_content) => from_content, Err(e) => { eprintln!("Failed to read from-file: {e}"); return ExitCode::from(2); } }; - let to_content = match fs::read(¶ms.to) { + let to_content = match read_file_contents(¶ms.to) { Ok(to_content) => to_content, Err(e) => { eprintln!("Failed to read to-file: {e}"); diff --git a/src/params.rs b/src/params.rs index 18f8949..7e9cc78 100644 --- a/src/params.rs +++ b/src/params.rs @@ -67,9 +67,9 @@ pub fn parse_params>(opts: I) -> Result ", exe.to_string_lossy())); } @@ -461,14 +461,14 @@ mod tests { assert_eq!( Ok(Params { from: os("foo"), - to: os("/dev/stdin"), + to: os("-"), ..Default::default() }), parse_params([os("diff"), os("foo"), os("-")].iter().cloned()) ); assert_eq!( Ok(Params { - from: os("/dev/stdin"), + from: os("-"), to: os("bar"), ..Default::default() }), @@ -476,8 +476,8 @@ mod tests { ); assert_eq!( Ok(Params { - from: os("/dev/stdin"), - to: os("/dev/stdin"), + from: os("-"), + to: os("-"), ..Default::default() }), parse_params([os("diff"), os("-"), os("-")].iter().cloned()) diff --git a/tests/integration.rs b/tests/integration.rs index 5ad624e..240d60b 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -22,25 +22,30 @@ fn unknown_param() -> Result<(), Box> { } #[test] -fn cannot_read_from_file() -> Result<(), Box> { +fn cannot_read_files() -> Result<(), Box> { + let file = NamedTempFile::new()?; + let mut cmd = Command::cargo_bin("diffutils")?; - cmd.arg("foo.txt").arg("bar.txt"); + cmd.arg("foo.txt").arg(file.path()); cmd.assert() .code(predicate::eq(2)) .failure() .stderr(predicate::str::starts_with("Failed to read from-file")); - Ok(()) -} -#[test] -fn cannot_read_to_file() -> Result<(), Box> { - let file = NamedTempFile::new()?; let mut cmd = Command::cargo_bin("diffutils")?; - cmd.arg(file.path()).arg("bar.txt"); + cmd.arg(file.path()).arg("foo.txt"); cmd.assert() .code(predicate::eq(2)) .failure() .stderr(predicate::str::starts_with("Failed to read to-file")); + + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("foo.txt").arg("foo.txt"); + cmd.assert() + .code(predicate::eq(2)) + .failure() + .stderr(predicate::str::starts_with("Failed to read from-file")); + Ok(()) } @@ -177,7 +182,7 @@ fn read_from_stdin() -> Result<(), Box> { .code(predicate::eq(1)) .failure() .stdout(predicate::eq(format!( - "--- {}\t\n+++ /dev/stdin\t\n@@ -1 +1 @@\n-foo\n+bar\n", + "--- {}\t\n+++ -\t\n@@ -1 +1 @@\n-foo\n+bar\n", file1.path().to_string_lossy() ))); @@ -190,12 +195,12 @@ fn read_from_stdin() -> Result<(), Box> { .code(predicate::eq(1)) .failure() .stdout(predicate::eq(format!( - "--- /dev/stdin\t\n+++ {}\t\n@@ -1 +1 @@\n-foo\n+bar\n", + "--- -\t\n+++ {}\t\n@@ -1 +1 @@\n-foo\n+bar\n", file2.path().to_string_lossy() ))); let mut cmd = Command::cargo_bin("diffutils")?; - cmd.arg("-u").arg("-").arg("-").write_stdin("foo\n"); + cmd.arg("-u").arg("-").arg("-"); cmd.assert() .code(predicate::eq(0)) .success() From 84ad116845db263cfb04234fb509011f59cbb8f1 Mon Sep 17 00:00:00 2001 From: Olivier Tilloy Date: Thu, 4 Apr 2024 22:14:03 +0200 Subject: [PATCH 070/194] Use io::stdin() to read from standard input in a portable manner --- src/main.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index de4bbbb..6858f5e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,7 @@ use crate::params::{parse_params, Format}; use std::env; use std::ffi::OsString; use std::fs; -use std::io::{self, Write}; +use std::io::{self, Read, Write}; use std::process::{exit, ExitCode}; mod context_diff; @@ -46,8 +46,12 @@ fn main() -> ExitCode { } // read files fn read_file_contents(filepath: &OsString) -> io::Result> { - let stdin = OsString::from("/dev/stdin"); - fs::read(if filepath == "-" { &stdin } else { filepath }) + if filepath == "-" { + let mut content = Vec::new(); + io::stdin().read_to_end(&mut content).and(Ok(content)) + } else { + fs::read(filepath) + } } let from_content = match read_file_contents(¶ms.from) { Ok(from_content) => from_content, From e1c319f96b371bf80864399490a39a74ecef2e6b Mon Sep 17 00:00:00 2001 From: Olivier Tilloy Date: Mon, 8 Apr 2024 22:36:14 +0200 Subject: [PATCH 071/194] Add an integration test for reading from "/dev/stdin" on unix-like systems --- tests/integration.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/integration.rs b/tests/integration.rs index 240d60b..8c0491a 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -206,5 +206,21 @@ fn read_from_stdin() -> Result<(), Box> { .success() .stdout(predicate::str::is_empty()); + #[cfg(unix)] + { + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("-u") + .arg(file1.path()) + .arg("/dev/stdin") + .write_stdin("bar\n"); + cmd.assert() + .code(predicate::eq(1)) + .failure() + .stdout(predicate::eq(format!( + "--- {}\t\n+++ /dev/stdin\t\n@@ -1 +1 @@\n-foo\n+bar\n", + file1.path().to_string_lossy() + ))); + } + Ok(()) } From 00e18a6b0cfcfa76f70d3a8f8829d975d789131c Mon Sep 17 00:00:00 2001 From: Tanmay Patil Date: Wed, 10 Apr 2024 22:20:48 +0530 Subject: [PATCH 072/194] Define assert_diff_eq macro for context&unified diff comparison --- Cargo.toml | 2 +- src/context_diff.rs | 14 +++----------- src/lib.rs | 1 + src/macros.rs | 13 +++++++++++++ src/main.rs | 1 + src/unified_diff.rs | 14 +++----------- tests/integration.rs | 27 +++++++++++++++++---------- 7 files changed, 39 insertions(+), 33 deletions(-) create mode 100644 src/macros.rs diff --git a/Cargo.toml b/Cargo.toml index ea8a1e1..20cffa6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ path = "src/main.rs" [dependencies] chrono = "0.4.35" diff = "0.1.10" -regex = "1.10.4" +regex = "1.10.3" same-file = "1.0.6" unicode-width = "0.1.11" diff --git a/src/context_diff.rs b/src/context_diff.rs index 80c602d..93c38a5 100644 --- a/src/context_diff.rs +++ b/src/context_diff.rs @@ -726,9 +726,8 @@ mod tests { #[test] fn test_stop_early() { - use regex::Regex; + use crate::assert_diff_eq; use std::fs::File; - use std::str; let target = "target/context-diff"; let _ = std::fs::create_dir(target); @@ -749,10 +748,6 @@ mod tests { }, ); - let diff_full_text = str::from_utf8(&diff_full).unwrap(); - let re = Regex::new(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+ [+-]\d{4}").unwrap(); - let diff_full = re.replace_all(diff_full_text, ""); - let expected_full = [ "*** target/context-diff/foo\t", "--- target/context-diff/bar\t", @@ -768,7 +763,7 @@ mod tests { "", ] .join("\n"); - assert_eq!(diff_full, expected_full); + assert_diff_eq!(diff_full, expected_full); let diff_brief = diff( from.as_bytes(), @@ -781,16 +776,13 @@ mod tests { }, ); - let diff_brief_text = str::from_utf8(&diff_brief).unwrap(); - let re = Regex::new(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+ [+-]\d{4}").unwrap(); - let diff_brief = re.replace_all(diff_brief_text, ""); let expected_brief = [ "*** target/context-diff/foo\t", "--- target/context-diff/bar\t", "", ] .join("\n"); - assert_eq!(diff_brief, expected_brief); + assert_diff_eq!(diff_brief, expected_brief); let nodiff_full = diff( from.as_bytes(), diff --git a/src/lib.rs b/src/lib.rs index 7ed36a6..0bb911b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ pub mod context_diff; pub mod ed_diff; +pub mod macros; pub mod normal_diff; pub mod params; pub mod unified_diff; diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 0000000..401bff4 --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,13 @@ +#[macro_export] +macro_rules! assert_diff_eq { + ($actual:expr, $expected:expr) => {{ + use regex::Regex; + use std::str; + + let diff = str::from_utf8(&$actual).unwrap(); + let re = Regex::new(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+ [+-]\d{4}").unwrap(); + let actual = re.replace_all(diff, ""); + + assert_eq!(actual, $expected); + }}; +} diff --git a/src/main.rs b/src/main.rs index a19e646..624bc3b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,7 @@ use std::process::{exit, ExitCode}; mod context_diff; mod ed_diff; +mod macros; mod normal_diff; mod params; mod unified_diff; diff --git a/src/unified_diff.rs b/src/unified_diff.rs index 62dd198..57925e1 100644 --- a/src/unified_diff.rs +++ b/src/unified_diff.rs @@ -880,9 +880,8 @@ mod tests { #[test] fn test_stop_early() { - use regex::Regex; + use crate::assert_diff_eq; use std::fs::File; - use std::str; let target = "target/context-diff"; let _ = std::fs::create_dir(target); @@ -903,10 +902,6 @@ mod tests { }, ); - let diff_full_text = str::from_utf8(&diff_full).unwrap(); - let re = Regex::new(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+ [+-]\d{4}").unwrap(); - let diff_full = re.replace_all(diff_full_text, ""); - let expected_full = [ "--- target/context-diff/foo\t", "+++ target/context-diff/bar\t", @@ -918,7 +913,7 @@ mod tests { "", ] .join("\n"); - assert_eq!(diff_full, expected_full); + assert_diff_eq!(diff_full, expected_full); let diff_brief = diff( from.as_bytes(), @@ -931,16 +926,13 @@ mod tests { }, ); - let diff_brief_text = str::from_utf8(&diff_brief).unwrap(); - let re = Regex::new(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+ [+-]\d{4}").unwrap(); - let diff_brief = re.replace_all(diff_brief_text, ""); let expected_brief = [ "--- target/context-diff/foo\t", "+++ target/context-diff/bar\t", "", ] .join("\n"); - assert_eq!(diff_brief, expected_brief); + assert_diff_eq!(diff_brief, expected_brief); let nodiff_full = diff( from.as_bytes(), diff --git a/tests/integration.rs b/tests/integration.rs index 5ad624e..4291ef4 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -4,6 +4,7 @@ // files that was distributed with this source code. use assert_cmd::cmd::Command; +use diffutilslib::assert_diff_eq; use predicates::prelude::*; use std::io::Write; use tempfile::NamedTempFile; @@ -173,26 +174,32 @@ fn read_from_stdin() -> Result<(), Box> { .arg(file1.path()) .arg("-") .write_stdin("bar\n"); - cmd.assert() - .code(predicate::eq(1)) - .failure() - .stdout(predicate::eq(format!( + cmd.assert().code(predicate::eq(1)).failure(); + + let output = cmd.output().unwrap().stdout; + assert_diff_eq!( + output, + format!( "--- {}\t\n+++ /dev/stdin\t\n@@ -1 +1 @@\n-foo\n+bar\n", file1.path().to_string_lossy() - ))); + ) + ); let mut cmd = Command::cargo_bin("diffutils")?; cmd.arg("-u") .arg("-") .arg(file2.path()) .write_stdin("foo\n"); - cmd.assert() - .code(predicate::eq(1)) - .failure() - .stdout(predicate::eq(format!( + cmd.assert().code(predicate::eq(1)).failure(); + + let output = cmd.output().unwrap().stdout; + assert_diff_eq!( + output, + format!( "--- /dev/stdin\t\n+++ {}\t\n@@ -1 +1 @@\n-foo\n+bar\n", file2.path().to_string_lossy() - ))); + ) + ); let mut cmd = Command::cargo_bin("diffutils")?; cmd.arg("-u").arg("-").arg("-").write_stdin("foo\n"); From 0a77fe12b9f4c9794a25dfe3e2ac89ba335e2dcc Mon Sep 17 00:00:00 2001 From: Tanmay Patil Date: Wed, 10 Apr 2024 23:01:28 +0530 Subject: [PATCH 073/194] Add tests for get_modification_time function --- src/utils.rs | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/utils.rs b/src/utils.rs index e0592ef..4320ed6 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -52,6 +52,8 @@ pub fn do_write_line( } } +/// Retrieves the modification time of the input file specified by file path +/// If an error occurs, it returns the current system time pub fn get_modification_time(file_path: &str) -> String { use chrono::{DateTime, Local}; use std::fs; @@ -132,4 +134,40 @@ mod tests { assert_line_written("foo bar\tbaz", true, 8, "foo bar baz"); } } + + mod modification_time { + use super::*; + + #[test] + fn set_time() { + use chrono::{DateTime, Local}; + use std::fs::File; + use std::time::SystemTime; + + let target = "target/utils"; + let _ = std::fs::create_dir(target); + let filename = &format!("{target}/foo"); + let temp = File::create(filename).unwrap(); + + // set file modification time equal to current time + let current = SystemTime::now(); + let _ = temp.set_modified(current); + + // format current time + let current: DateTime = current.into(); + let current: String = current.format("%Y-%m-%d %H:%M:%S%.9f %z").to_string(); + + // verify + assert_eq!(current, get_modification_time(filename)); + } + + #[test] + fn invalid_file() { + let invalid_file = "target/utils/invalid-file"; + + let m_time = get_modification_time(invalid_file); + + assert!(!m_time.is_empty()); + } + } } From 900e1c3a680718fe502380d349d03846ecd59722 Mon Sep 17 00:00:00 2001 From: Tanmay Patil Date: Sun, 14 Apr 2024 13:43:30 +0530 Subject: [PATCH 074/194] Tests: Replace modification time in diff with "TIMESTAMP" placeholder --- src/context_diff.rs | 8 ++++---- src/macros.rs | 14 +++++++++++++- src/unified_diff.rs | 8 ++++---- tests/integration.rs | 6 +++--- 4 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/context_diff.rs b/src/context_diff.rs index 93c38a5..8a1b831 100644 --- a/src/context_diff.rs +++ b/src/context_diff.rs @@ -749,8 +749,8 @@ mod tests { ); let expected_full = [ - "*** target/context-diff/foo\t", - "--- target/context-diff/bar\t", + "*** target/context-diff/foo\tTIMESTAMP", + "--- target/context-diff/bar\tTIMESTAMP", "***************", "*** 1,3 ****", " a", @@ -777,8 +777,8 @@ mod tests { ); let expected_brief = [ - "*** target/context-diff/foo\t", - "--- target/context-diff/bar\t", + "*** target/context-diff/foo\tTIMESTAMP", + "--- target/context-diff/bar\tTIMESTAMP", "", ] .join("\n"); diff --git a/src/macros.rs b/src/macros.rs index 401bff4..7a3c693 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -1,3 +1,15 @@ +// asserts equality of the actual diff and expected diff +// considering datetime varitations +// +// It replaces the modification time in the actual diff +// with placeholer "TIMESTAMP" and then asserts the equality +// +// For eg. +// let brief = "*** fruits_old.txt\t2024-03-24 23:43:05.189597645 +0530\n +// --- fruits_new.txt\t2024-03-24 23:35:08.922581904 +0530\n"; +// +// replaced = "*** fruits_old.txt\tTIMESTAMP\n +// --- fruits_new.txt\tTIMESTAMP\n"; #[macro_export] macro_rules! assert_diff_eq { ($actual:expr, $expected:expr) => {{ @@ -6,7 +18,7 @@ macro_rules! assert_diff_eq { let diff = str::from_utf8(&$actual).unwrap(); let re = Regex::new(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+ [+-]\d{4}").unwrap(); - let actual = re.replace_all(diff, ""); + let actual = re.replace_all(diff, "TIMESTAMP"); assert_eq!(actual, $expected); }}; diff --git a/src/unified_diff.rs b/src/unified_diff.rs index 57925e1..ed1226f 100644 --- a/src/unified_diff.rs +++ b/src/unified_diff.rs @@ -903,8 +903,8 @@ mod tests { ); let expected_full = [ - "--- target/context-diff/foo\t", - "+++ target/context-diff/bar\t", + "--- target/context-diff/foo\tTIMESTAMP", + "+++ target/context-diff/bar\tTIMESTAMP", "@@ -1,3 +1,3 @@", " a", "-b", @@ -927,8 +927,8 @@ mod tests { ); let expected_brief = [ - "--- target/context-diff/foo\t", - "+++ target/context-diff/bar\t", + "--- target/context-diff/foo\tTIMESTAMP", + "+++ target/context-diff/bar\tTIMESTAMP", "", ] .join("\n"); diff --git a/tests/integration.rs b/tests/integration.rs index 661aa58..853ba4d 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -185,7 +185,7 @@ fn read_from_stdin() -> Result<(), Box> { assert_diff_eq!( output, format!( - "--- {}\t\n+++ -\t\n@@ -1 +1 @@\n-foo\n+bar\n", + "--- {}\tTIMESTAMP\n+++ -\tTIMESTAMP\n@@ -1 +1 @@\n-foo\n+bar\n", file1.path().to_string_lossy() ) ); @@ -201,7 +201,7 @@ fn read_from_stdin() -> Result<(), Box> { assert_diff_eq!( output, format!( - "--- -\t\n+++ {}\t\n@@ -1 +1 @@\n-foo\n+bar\n", + "--- -\tTIMESTAMP\n+++ {}\tTIMESTAMP\n@@ -1 +1 @@\n-foo\n+bar\n", file2.path().to_string_lossy() ) ); @@ -226,7 +226,7 @@ fn read_from_stdin() -> Result<(), Box> { assert_diff_eq!( output, format!( - "--- {}\t\n+++ /dev/stdin\t\n@@ -1 +1 @@\n-foo\n+bar\n", + "--- {}\tTIMESTAMP\n+++ /dev/stdin\tTIMESTAMP\n@@ -1 +1 @@\n-foo\n+bar\n", file1.path().to_string_lossy() ) ); From 33783d094e7ec94060ef719cabeeb0547d86d389 Mon Sep 17 00:00:00 2001 From: Tanmay Patil Date: Sun, 14 Apr 2024 17:16:53 +0530 Subject: [PATCH 075/194] Improve tests --- src/utils.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index 4320ed6..7fb48cc 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -163,11 +163,18 @@ mod tests { #[test] fn invalid_file() { + use chrono::{DateTime, Local}; + use std::time::SystemTime; + let invalid_file = "target/utils/invalid-file"; - let m_time = get_modification_time(invalid_file); + // store current time before calling `get_modification_time` + // Because the file is invalid, it will return SystemTime::now() + // which will be greater than previously saved time + let current_time: DateTime = SystemTime::now().into(); + let m_time: DateTime = get_modification_time(invalid_file).parse().unwrap(); - assert!(!m_time.is_empty()); + assert!(m_time > current_time); } } } From ba7cb0aef9dcdeb1930c5adab23c61bedb7878dd Mon Sep 17 00:00:00 2001 From: Tanmay Patil Date: Sun, 14 Apr 2024 18:24:00 +0530 Subject: [PATCH 076/194] Do not create dummy files Since we now returning SystemTime::now() for invalid file input, there is no need to crate dummy files --- src/context_diff.rs | 40 +++++++++++++--------------------------- src/unified_diff.rs | 45 +++++++++++++++------------------------------ 2 files changed, 28 insertions(+), 57 deletions(-) diff --git a/src/context_diff.rs b/src/context_diff.rs index 8a1b831..e276ce5 100644 --- a/src/context_diff.rs +++ b/src/context_diff.rs @@ -427,15 +427,13 @@ mod tests { if f != 2 { bet.write_all(b"l\n").unwrap(); } - let _ = File::create(&format!("{target}/aalef")).unwrap(); - let mut fa = File::create(&format!("{target}/alef")).unwrap(); // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff( &alef, &bet, &Params { - from: (&format!("{target}/aalef")).into(), + from: "a/alef".into(), to: (&format!("{target}/alef")).into(), context_count: 2, ..Default::default() @@ -445,6 +443,7 @@ mod tests { .unwrap() .write_all(&diff) .unwrap(); + let mut fa = File::create(&format!("{target}/alef")).unwrap(); fa.write_all(&alef[..]).unwrap(); let mut fb = File::create(&format!("{target}/bet")).unwrap(); fb.write_all(&bet[..]).unwrap(); @@ -509,15 +508,13 @@ mod tests { if f != 2 { bet.write_all(b"l\n").unwrap(); } - let _ = File::create(&format!("{target}/aalef_")).unwrap(); - let mut fa = File::create(&format!("{target}/alef_")).unwrap(); // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff( &alef, &bet, &Params { - from: (&format!("{target}/aalef_")).into(), + from: "a/alef_".into(), to: (&format!("{target}/alef_")).into(), context_count: 2, ..Default::default() @@ -527,6 +524,7 @@ mod tests { .unwrap() .write_all(&diff) .unwrap(); + let mut fa = File::create(&format!("{target}/alef_")).unwrap(); fa.write_all(&alef[..]).unwrap(); let mut fb = File::create(&format!("{target}/bet_")).unwrap(); fb.write_all(&bet[..]).unwrap(); @@ -594,15 +592,13 @@ mod tests { if alef.is_empty() && bet.is_empty() { continue; }; - let _ = File::create(&format!("{target}/aalefx")).unwrap(); - let mut fa = File::create(&format!("{target}/alefx")).unwrap(); // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff( &alef, &bet, &Params { - from: (&format!("{target}/aalefx")).into(), + from: "a/alefx".into(), to: (&format!("{target}/alefx")).into(), context_count: 2, ..Default::default() @@ -612,6 +608,7 @@ mod tests { .unwrap() .write_all(&diff) .unwrap(); + let mut fa = File::create(&format!("{target}/alefx")).unwrap(); fa.write_all(&alef[..]).unwrap(); let mut fb = File::create(&format!("{target}/betx")).unwrap(); fb.write_all(&bet[..]).unwrap(); @@ -682,15 +679,13 @@ mod tests { if f != 2 { bet.write_all(b"f\n").unwrap(); } - let _ = File::create(&format!("{target}/aalefr")).unwrap(); - let mut fa = File::create(&format!("{target}/alefr")).unwrap(); // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff( &alef, &bet, &Params { - from: (&format!("{target}/aalefr")).into(), + from: "a/alefr".into(), to: (&format!("{target}/alefr")).into(), context_count: 2, ..Default::default() @@ -700,6 +695,7 @@ mod tests { .unwrap() .write_all(&diff) .unwrap(); + let mut fa = File::create(&format!("{target}/alefr")).unwrap(); fa.write_all(&alef[..]).unwrap(); let mut fb = File::create(&format!("{target}/betr")).unwrap(); fb.write_all(&bet[..]).unwrap(); @@ -727,15 +723,10 @@ mod tests { #[test] fn test_stop_early() { use crate::assert_diff_eq; - use std::fs::File; - let target = "target/context-diff"; - let _ = std::fs::create_dir(target); - let from_filename = &format!("{target}/foo"); - let _ = File::create(from_filename).unwrap(); + let from_filename = "foo"; let from = ["a", "b", "c", ""].join("\n"); - let to_filename = &format!("{target}/bar"); - let _ = File::create(to_filename).unwrap(); + let to_filename = "bar"; let to = ["a", "d", "c", ""].join("\n"); let diff_full = diff( @@ -749,8 +740,8 @@ mod tests { ); let expected_full = [ - "*** target/context-diff/foo\tTIMESTAMP", - "--- target/context-diff/bar\tTIMESTAMP", + "*** foo\tTIMESTAMP", + "--- bar\tTIMESTAMP", "***************", "*** 1,3 ****", " a", @@ -776,12 +767,7 @@ mod tests { }, ); - let expected_brief = [ - "*** target/context-diff/foo\tTIMESTAMP", - "--- target/context-diff/bar\tTIMESTAMP", - "", - ] - .join("\n"); + let expected_brief = ["*** foo\tTIMESTAMP", "--- bar\tTIMESTAMP", ""].join("\n"); assert_diff_eq!(diff_brief, expected_brief); let nodiff_full = diff( diff --git a/src/unified_diff.rs b/src/unified_diff.rs index ed1226f..11299d7 100644 --- a/src/unified_diff.rs +++ b/src/unified_diff.rs @@ -454,15 +454,13 @@ mod tests { if f != 2 { bet.write_all(b"l\n").unwrap(); } - let _ = File::create(&format!("{target}/aalef")).unwrap(); - let mut fa = File::create(&format!("{target}/alef")).unwrap(); // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff( &alef, &bet, &Params { - from: (&format!("{target}/aalef")).into(), + from: "a/alef".into(), to: (&format!("{target}/alef")).into(), context_count: 2, ..Default::default() @@ -472,6 +470,7 @@ mod tests { .unwrap() .write_all(&diff) .unwrap(); + let mut fa = File::create(&format!("{target}/alef")).unwrap(); fa.write_all(&alef[..]).unwrap(); let mut fb = File::create(&format!("{target}/bet")).unwrap(); fb.write_all(&bet[..]).unwrap(); @@ -571,15 +570,13 @@ mod tests { } _ => unreachable!(), } - let _ = File::create(&format!("{target}/aalefn")).unwrap(); - let mut fa = File::create(&format!("{target}/alefn")).unwrap(); // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff( &alef, &bet, &Params { - from: (&format!("{target}/aalefn")).into(), + from: "a/alefn".into(), to: (&format!("{target}/alefn")).into(), context_count: 2, ..Default::default() @@ -589,6 +586,7 @@ mod tests { .unwrap() .write_all(&diff) .unwrap(); + let mut fa = File::create(&format!("{target}/alefn")).unwrap(); fa.write_all(&alef[..]).unwrap(); let mut fb = File::create(&format!("{target}/betn")).unwrap(); fb.write_all(&bet[..]).unwrap(); @@ -668,15 +666,13 @@ mod tests { 3 => {} _ => unreachable!(), } - let _ = File::create(&format!("{target}/aalef_")).unwrap(); - let mut fa = File::create(&format!("{target}/alef_")).unwrap(); // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff( &alef, &bet, &Params { - from: (&format!("{target}/aalef_")).into(), + from: "a/alef_".into(), to: (&format!("{target}/alef_")).into(), context_count: 2, ..Default::default() @@ -686,6 +682,7 @@ mod tests { .unwrap() .write_all(&diff) .unwrap(); + let mut fa = File::create(&format!("{target}/alef_")).unwrap(); fa.write_all(&alef[..]).unwrap(); let mut fb = File::create(&format!("{target}/bet_")).unwrap(); fb.write_all(&bet[..]).unwrap(); @@ -750,15 +747,13 @@ mod tests { if f != 2 { bet.write_all(b"l\n").unwrap(); } - let _ = File::create(&format!("{target}/aalefx")).unwrap(); - let mut fa = File::create(&format!("{target}/alefx")).unwrap(); // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff( &alef, &bet, &Params { - from: (&format!("{target}/aalefx")).into(), + from: "a/alefx".into(), to: (&format!("{target}/alefx")).into(), context_count: 2, ..Default::default() @@ -768,6 +763,7 @@ mod tests { .unwrap() .write_all(&diff) .unwrap(); + let mut fa = File::create(&format!("{target}/alefx")).unwrap(); fa.write_all(&alef[..]).unwrap(); let mut fb = File::create(&format!("{target}/betx")).unwrap(); fb.write_all(&bet[..]).unwrap(); @@ -837,15 +833,13 @@ mod tests { if f != 2 { bet.write_all(b"f\n").unwrap(); } - let _ = File::create(&format!("{target}/aalefr")).unwrap(); - let mut fa = File::create(&format!("{target}/alefr")).unwrap(); // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff( &alef, &bet, &Params { - from: (&format!("{target}/aalefr")).into(), + from: "a/alefr".into(), to: (&format!("{target}/alefr")).into(), context_count: 2, ..Default::default() @@ -855,6 +849,7 @@ mod tests { .unwrap() .write_all(&diff) .unwrap(); + let mut fa = File::create(&format!("{target}/alefr")).unwrap(); fa.write_all(&alef[..]).unwrap(); let mut fb = File::create(&format!("{target}/betr")).unwrap(); fb.write_all(&bet[..]).unwrap(); @@ -881,15 +876,10 @@ mod tests { #[test] fn test_stop_early() { use crate::assert_diff_eq; - use std::fs::File; - let target = "target/context-diff"; - let _ = std::fs::create_dir(target); - let from_filename = &format!("{target}/foo"); - let _ = File::create(from_filename).unwrap(); + let from_filename = "foo"; let from = ["a", "b", "c", ""].join("\n"); - let to_filename = &format!("{target}/bar"); - let _ = File::create(to_filename).unwrap(); + let to_filename = "bar"; let to = ["a", "d", "c", ""].join("\n"); let diff_full = diff( @@ -903,8 +893,8 @@ mod tests { ); let expected_full = [ - "--- target/context-diff/foo\tTIMESTAMP", - "+++ target/context-diff/bar\tTIMESTAMP", + "--- foo\tTIMESTAMP", + "+++ bar\tTIMESTAMP", "@@ -1,3 +1,3 @@", " a", "-b", @@ -926,12 +916,7 @@ mod tests { }, ); - let expected_brief = [ - "--- target/context-diff/foo\tTIMESTAMP", - "+++ target/context-diff/bar\tTIMESTAMP", - "", - ] - .join("\n"); + let expected_brief = ["--- foo\tTIMESTAMP", "+++ bar\tTIMESTAMP", ""].join("\n"); assert_diff_eq!(diff_brief, expected_brief); let nodiff_full = diff( From 54c02bdf0bbda37c463dc1099d188636583838fc Mon Sep 17 00:00:00 2001 From: Tanmay Patil Date: Tue, 16 Apr 2024 10:17:09 +0530 Subject: [PATCH 077/194] Use NamedTempFile instead of manually creating files --- src/utils.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index 7fb48cc..7dc38e4 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -141,24 +141,20 @@ mod tests { #[test] fn set_time() { use chrono::{DateTime, Local}; - use std::fs::File; use std::time::SystemTime; + use tempfile::NamedTempFile; - let target = "target/utils"; - let _ = std::fs::create_dir(target); - let filename = &format!("{target}/foo"); - let temp = File::create(filename).unwrap(); - + let temp = NamedTempFile::new().unwrap(); // set file modification time equal to current time let current = SystemTime::now(); - let _ = temp.set_modified(current); + let _ = temp.as_file().set_modified(current); // format current time let current: DateTime = current.into(); let current: String = current.format("%Y-%m-%d %H:%M:%S%.9f %z").to_string(); // verify - assert_eq!(current, get_modification_time(filename)); + assert_eq!(current, get_modification_time(&temp.path().to_string_lossy())); } #[test] From aedd0684d199a5b9e0a57e48b7e9a42f6f0ea7ad Mon Sep 17 00:00:00 2001 From: Tanmay Patil Date: Tue, 16 Apr 2024 10:26:32 +0530 Subject: [PATCH 078/194] Replace only the first two occurences of timestamp regex --- src/macros.rs | 2 +- src/utils.rs | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/macros.rs b/src/macros.rs index 7a3c693..1f1a994 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -18,7 +18,7 @@ macro_rules! assert_diff_eq { let diff = str::from_utf8(&$actual).unwrap(); let re = Regex::new(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+ [+-]\d{4}").unwrap(); - let actual = re.replace_all(diff, "TIMESTAMP"); + let actual = re.replacen(diff, 2, "TIMESTAMP"); assert_eq!(actual, $expected); }}; diff --git a/src/utils.rs b/src/utils.rs index 7dc38e4..561f2b9 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -154,7 +154,10 @@ mod tests { let current: String = current.format("%Y-%m-%d %H:%M:%S%.9f %z").to_string(); // verify - assert_eq!(current, get_modification_time(&temp.path().to_string_lossy())); + assert_eq!( + current, + get_modification_time(&temp.path().to_string_lossy()) + ); } #[test] From 1149a247dd01d4d560f844e9dae32ea2542cb5fd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 16 Apr 2024 16:20:52 +0000 Subject: [PATCH 079/194] Update Rust crate chrono to 0.4.38 --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cdbb0f6..d70d307 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -90,9 +90,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.35" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", diff --git a/Cargo.toml b/Cargo.toml index 9a53418..753fc09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ name = "diffutils" path = "src/main.rs" [dependencies] -chrono = "0.4.35" +chrono = "0.4.38" diff = "0.1.10" regex = "1.10.3" same-file = "1.0.6" From 7f7821f5585e71462879c7750e3307daff419fc4 Mon Sep 17 00:00:00 2001 From: Olivier Tilloy Date: Tue, 16 Apr 2024 18:37:59 +0200 Subject: [PATCH 080/194] Use the private Codecov token stored as a secret, to work around rate-limiting issues like https://github.com/codecov/codecov-action/issues/557 --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8b8e474..9af3854 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -143,9 +143,8 @@ jobs: echo "report=${COVERAGE_REPORT_FILE}" >> $GITHUB_OUTPUT - name: Upload coverage results (to Codecov.io) uses: codecov/codecov-action@v3 - # if: steps.vars.outputs.HAS_CODECOV_TOKEN with: - # token: ${{ secrets.CODECOV_TOKEN }} + token: ${{ secrets.CODECOV_TOKEN }} file: ${{ steps.coverage.outputs.report }} ## flags: IntegrationTests, UnitTests, ${{ steps.vars.outputs.CODECOV_FLAGS }} flags: ${{ steps.vars.outputs.CODECOV_FLAGS }} From b8efad6b90ac665d9756710b2fa722cce7fc3229 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 16 Apr 2024 19:35:01 +0000 Subject: [PATCH 081/194] Update Rust crate diff to 0.1.13 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 753fc09..1c16c15 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ path = "src/main.rs" [dependencies] chrono = "0.4.38" -diff = "0.1.10" +diff = "0.1.13" regex = "1.10.3" same-file = "1.0.6" unicode-width = "0.1.11" From fcec7277c9ac6bad264a08475993e5a47da35855 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 16 Apr 2024 19:35:04 +0000 Subject: [PATCH 082/194] Update codecov/codecov-action action to v4 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9af3854..77b9218 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -142,7 +142,7 @@ jobs: grcov . --output-type lcov --output-path "${COVERAGE_REPORT_FILE}" --branch --ignore build.rs --ignore "vendor/*" --ignore "/*" --ignore "[a-zA-Z]:/*" --excl-br-line "^\s*((debug_)?assert(_eq|_ne)?!|#\[derive\()" echo "report=${COVERAGE_REPORT_FILE}" >> $GITHUB_OUTPUT - name: Upload coverage results (to Codecov.io) - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} file: ${{ steps.coverage.outputs.report }} From 674974d5e66762fc22995595d2456aaca1f040c9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 16 Apr 2024 19:54:53 +0000 Subject: [PATCH 083/194] Update Rust crate regex to 1.10.4 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 1c16c15..0e8dab9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ path = "src/main.rs" [dependencies] chrono = "0.4.38" diff = "0.1.13" -regex = "1.10.3" +regex = "1.10.4" same-file = "1.0.6" unicode-width = "0.1.11" From bf104648c1e9b217ef4109221d0eebe42284398c Mon Sep 17 00:00:00 2001 From: Olivier Tilloy Date: Sat, 20 Apr 2024 19:17:42 +0200 Subject: [PATCH 084/194] CI: On Windows, use GNU's patch.exe instead of Strawberry Perl patch --- .github/workflows/ci.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 77b9218..be76d96 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,6 +28,10 @@ jobs: steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable + - name: set up PATH on Windows + # Needed to use GNU's patch.exe instead of Strawberry Perl patch + if: runner.os == 'Windows' + run: echo "C:\Program Files\Git\usr\bin" >> $env:GITHUB_PATH - run: cargo test fmt: @@ -103,6 +107,10 @@ jobs: - name: rust toolchain ~ install uses: dtolnay/rust-toolchain@nightly + - name: set up PATH on Windows + # Needed to use GNU's patch.exe instead of Strawberry Perl patch + if: runner.os == 'Windows' + run: echo "C:\Program Files\Git\usr\bin" >> $env:GITHUB_PATH - name: Test run: cargo test ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} --no-fail-fast env: From 831348d1fc7c457cc5c79828c2d2cb9f4120a8b0 Mon Sep 17 00:00:00 2001 From: Olivier Tilloy Date: Sat, 20 Apr 2024 23:30:20 +0200 Subject: [PATCH 085/194] Fix file path in ed diff tests --- src/ed_diff.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/ed_diff.rs b/src/ed_diff.rs index ba39046..3a524d3 100644 --- a/src/ed_diff.rs +++ b/src/ed_diff.rs @@ -226,7 +226,7 @@ mod tests { // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff_w(&alef, &bet, &format!("{target}/alef")).unwrap(); - File::create("target/ab.ed") + File::create(&format!("{target}/ab.ed")) .unwrap() .write_all(&diff) .unwrap(); @@ -240,7 +240,7 @@ mod tests { { let output = Command::new("ed") .arg(&format!("{target}/alef")) - .stdin(File::open("target/ab.ed").unwrap()) + .stdin(File::open(&format!("{target}/ab.ed")).unwrap()) .output() .unwrap(); assert!(output.status.success(), "{output:?}"); @@ -299,12 +299,12 @@ mod tests { } // This test diff is intentionally reversed. // We want it to turn the alef into bet. - let diff = diff_w(&alef, &bet, "target/alef_").unwrap(); - File::create("target/ab_.ed") + let diff = diff_w(&alef, &bet, &format!("{target}/alef_")).unwrap(); + File::create(&format!("{target}/ab_.ed")) .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create("target/alef_").unwrap(); + let mut fa = File::create(&format!("{target}/alef_")).unwrap(); fa.write_all(&alef[..]).unwrap(); let mut fb = File::create(&format!("{target}/bet_")).unwrap(); fb.write_all(&bet[..]).unwrap(); @@ -313,15 +313,15 @@ mod tests { #[cfg(not(windows))] // there's no ed on windows { let output = Command::new("ed") - .arg("target/alef_") - .stdin(File::open("target/ab_.ed").unwrap()) + .arg(&format!("{target}/alef_")) + .stdin(File::open(&format!("{target}/ab_.ed")).unwrap()) .output() .unwrap(); assert!(output.status.success(), "{output:?}"); } //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read("target/alef_").unwrap(); + let alef = fs::read(&format!("{target}/alef_")).unwrap(); assert_eq!(alef, bet); } } @@ -380,7 +380,7 @@ mod tests { // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff_w(&alef, &bet, &format!("{target}/alefr")).unwrap(); - File::create("target/abr.ed") + File::create(&format!("{target}/abr.ed")) .unwrap() .write_all(&diff) .unwrap(); @@ -394,7 +394,7 @@ mod tests { { let output = Command::new("ed") .arg(&format!("{target}/alefr")) - .stdin(File::open("target/abr.ed").unwrap()) + .stdin(File::open(&format!("{target}/abr.ed")).unwrap()) .output() .unwrap(); assert!(output.status.success(), "{output:?}"); From 14799eea897dff48e3e612e8ad25ae7c40b80793 Mon Sep 17 00:00:00 2001 From: Olivier Tilloy Date: Sun, 21 Apr 2024 00:01:23 +0200 Subject: [PATCH 086/194] Move test assertions in the cfg block where they belong --- src/ed_diff.rs | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/ed_diff.rs b/src/ed_diff.rs index 3a524d3..304a26f 100644 --- a/src/ed_diff.rs +++ b/src/ed_diff.rs @@ -188,9 +188,8 @@ mod tests { for &d in &[0, 1, 2] { for &e in &[0, 1, 2] { for &f in &[0, 1, 2] { - use std::fs::{self, File}; + use std::fs::File; use std::io::Write; - use std::process::Command; let mut alef = Vec::new(); let mut bet = Vec::new(); alef.write_all(if a == 0 { b"a\n" } else { b"b\n" }) @@ -238,17 +237,18 @@ mod tests { let _ = fb; #[cfg(not(windows))] // there's no ed on windows { + use std::process::Command; let output = Command::new("ed") .arg(&format!("{target}/alef")) .stdin(File::open(&format!("{target}/ab.ed")).unwrap()) .output() .unwrap(); assert!(output.status.success(), "{output:?}"); + //println!("{}", String::from_utf8_lossy(&output.stdout)); + //println!("{}", String::from_utf8_lossy(&output.stderr)); + let alef = std::fs::read(&format!("{target}/alef")).unwrap(); + assert_eq!(alef, bet); } - //println!("{}", String::from_utf8_lossy(&output.stdout)); - //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read(&format!("{target}/alef")).unwrap(); - assert_eq!(alef, bet); } } } @@ -268,9 +268,8 @@ mod tests { for &d in &[0, 1, 2] { for &e in &[0, 1, 2] { for &f in &[0, 1, 2] { - use std::fs::{self, File}; + use std::fs::File; use std::io::Write; - use std::process::Command; let mut alef = Vec::new(); let mut bet = Vec::new(); alef.write_all(if a == 0 { b"\n" } else { b"b\n" }).unwrap(); @@ -312,17 +311,18 @@ mod tests { let _ = fb; #[cfg(not(windows))] // there's no ed on windows { + use std::process::Command; let output = Command::new("ed") .arg(&format!("{target}/alef_")) .stdin(File::open(&format!("{target}/ab_.ed")).unwrap()) .output() .unwrap(); assert!(output.status.success(), "{output:?}"); + //println!("{}", String::from_utf8_lossy(&output.stdout)); + //println!("{}", String::from_utf8_lossy(&output.stderr)); + let alef = std::fs::read(&format!("{target}/alef_")).unwrap(); + assert_eq!(alef, bet); } - //println!("{}", String::from_utf8_lossy(&output.stdout)); - //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read(&format!("{target}/alef_")).unwrap(); - assert_eq!(alef, bet); } } } @@ -342,9 +342,8 @@ mod tests { for &d in &[0, 1, 2] { for &e in &[0, 1, 2] { for &f in &[0, 1, 2] { - use std::fs::{self, File}; + use std::fs::File; use std::io::Write; - use std::process::Command; let mut alef = Vec::new(); let mut bet = Vec::new(); alef.write_all(if a == 0 { b"a\n" } else { b"f\n" }) @@ -392,17 +391,18 @@ mod tests { let _ = fb; #[cfg(not(windows))] // there's no ed on windows { + use std::process::Command; let output = Command::new("ed") .arg(&format!("{target}/alefr")) .stdin(File::open(&format!("{target}/abr.ed")).unwrap()) .output() .unwrap(); assert!(output.status.success(), "{output:?}"); + //println!("{}", String::from_utf8_lossy(&output.stdout)); + //println!("{}", String::from_utf8_lossy(&output.stderr)); + let alef = std::fs::read(&format!("{target}/alefr")).unwrap(); + assert_eq!(alef, bet); } - //println!("{}", String::from_utf8_lossy(&output.stdout)); - //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read(&format!("{target}/alefr")).unwrap(); - assert_eq!(alef, bet); } } } From 39d2ece18772bc01c83bffd3365a3fffddc54571 Mon Sep 17 00:00:00 2001 From: Tanmay Patil Date: Sat, 20 Apr 2024 22:12:34 +0530 Subject: [PATCH 087/194] Handle directory-file and file-directory comparisons in the diff GNU diff treats `diff DIRECTORY FILE` as `diff DIRECTORY/FILE FILE` --- src/params.rs | 15 +++++++++++++++ tests/integration.rs | 31 +++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/src/params.rs b/src/params.rs index 7e9cc78..40af9ea 100644 --- a/src/params.rs +++ b/src/params.rs @@ -1,4 +1,5 @@ use std::ffi::{OsStr, OsString}; +use std::path::PathBuf; use regex::Regex; @@ -178,6 +179,20 @@ pub fn parse_params>(opts: I) -> Result ", exe.to_string_lossy())); }; + + // diff DIRECTORY FILE => diff DIRECTORY/FILE FILE + // diff FILE DIRECTORY => diff FILE DIRECTORY/FILE + let mut from_path: PathBuf = PathBuf::from(¶ms.from); + let mut to_path: PathBuf = PathBuf::from(¶ms.to); + + if from_path.is_dir() && to_path.is_file() { + from_path.push(to_path.file_name().unwrap()); + params.from = from_path.into_os_string(); + } else if from_path.is_file() && to_path.is_dir() { + to_path.push(from_path.file_name().unwrap()); + params.to = to_path.into_os_string(); + } + params.format = format.unwrap_or(Format::default()); Ok(params) } diff --git a/tests/integration.rs b/tests/integration.rs index 853ba4d..3f49af3 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -6,6 +6,7 @@ use assert_cmd::cmd::Command; use diffutilslib::assert_diff_eq; use predicates::prelude::*; +use std::fs::File; use std::io::Write; use tempfile::NamedTempFile; @@ -234,3 +235,33 @@ fn read_from_stdin() -> Result<(), Box> { Ok(()) } + +#[test] +fn read_from_directory() -> Result<(), Box> { + let mut cmd = Command::cargo_bin("diffutils")?; + + let target = "target/integration"; + let _ = std::fs::create_dir(target); + let directory = &format!("{target}/d"); + let _ = std::fs::create_dir(directory); + let mut a = File::create(&format!("{target}/a")).unwrap(); + a.write_all(b"a\n").unwrap(); + let mut da = File::create(&format!("{directory}/a")).unwrap(); + da.write_all(b"da\n").unwrap(); + + cmd.arg("-u") + .arg(&format!("{target}/d")) + .arg(&format!("{target}/a")); + cmd.assert().code(predicate::eq(1)).failure(); + + let output = cmd.output().unwrap().stdout; + assert_diff_eq!( + output, + format!( + "--- {}/d/a\tTIMESTAMP\n+++ {}/a\tTIMESTAMP\n@@ -1 +1 @@\n-da\n+a\n", + target, target + ) + ); + + Ok(()) +} From 65993d6a13bbd36b74ae42b8fc3fcefc5793a0cf Mon Sep 17 00:00:00 2001 From: Tanmay Patil Date: Sat, 20 Apr 2024 22:44:17 +0530 Subject: [PATCH 088/194] Add tests for `diff FILE DIRECTORY` --- tests/integration.rs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/tests/integration.rs b/tests/integration.rs index 3f49af3..c73e18e 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -238,8 +238,6 @@ fn read_from_stdin() -> Result<(), Box> { #[test] fn read_from_directory() -> Result<(), Box> { - let mut cmd = Command::cargo_bin("diffutils")?; - let target = "target/integration"; let _ = std::fs::create_dir(target); let directory = &format!("{target}/d"); @@ -249,6 +247,7 @@ fn read_from_directory() -> Result<(), Box> { let mut da = File::create(&format!("{directory}/a")).unwrap(); da.write_all(b"da\n").unwrap(); + let mut cmd = Command::cargo_bin("diffutils")?; cmd.arg("-u") .arg(&format!("{target}/d")) .arg(&format!("{target}/a")); @@ -263,5 +262,20 @@ fn read_from_directory() -> Result<(), Box> { ) ); + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("-u") + .arg(&format!("{target}/a")) + .arg(&format!("{target}/d")); + cmd.assert().code(predicate::eq(1)).failure(); + + let output = cmd.output().unwrap().stdout; + assert_diff_eq!( + output, + format!( + "--- {}/a\tTIMESTAMP\n+++ {}/d/a\tTIMESTAMP\n@@ -1 +1 @@\n-a\n+da\n", + target, target + ) + ); + Ok(()) } From 476e69ee206ad0f858fe7d96dfe73f6e2f486c19 Mon Sep 17 00:00:00 2001 From: Tanmay Patil Date: Sun, 21 Apr 2024 18:06:15 +0530 Subject: [PATCH 089/194] Windows: Fix tests --- tests/integration.rs | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/tests/integration.rs b/tests/integration.rs index c73e18e..f7174fd 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -8,6 +8,7 @@ use diffutilslib::assert_diff_eq; use predicates::prelude::*; use std::fs::File; use std::io::Write; +use std::path::PathBuf; use tempfile::NamedTempFile; // Integration tests for the diffutils command @@ -238,42 +239,45 @@ fn read_from_stdin() -> Result<(), Box> { #[test] fn read_from_directory() -> Result<(), Box> { - let target = "target/integration"; - let _ = std::fs::create_dir(target); - let directory = &format!("{target}/d"); - let _ = std::fs::create_dir(directory); - let mut a = File::create(&format!("{target}/a")).unwrap(); + let target = PathBuf::from("target/integration"); + let _ = std::fs::create_dir(&target); + + let directory = target.join("d"); + let _ = std::fs::create_dir(&directory); + + let a_path = target.join("a"); + let mut a = File::create(&a_path).unwrap(); a.write_all(b"a\n").unwrap(); - let mut da = File::create(&format!("{directory}/a")).unwrap(); + + let da_path = directory.join("a"); + let mut da = File::create(&da_path).unwrap(); da.write_all(b"da\n").unwrap(); let mut cmd = Command::cargo_bin("diffutils")?; - cmd.arg("-u") - .arg(&format!("{target}/d")) - .arg(&format!("{target}/a")); + cmd.arg("-u").arg(&directory).arg(&a_path); cmd.assert().code(predicate::eq(1)).failure(); let output = cmd.output().unwrap().stdout; assert_diff_eq!( output, format!( - "--- {}/d/a\tTIMESTAMP\n+++ {}/a\tTIMESTAMP\n@@ -1 +1 @@\n-da\n+a\n", - target, target + "--- {}\tTIMESTAMP\n+++ {}\tTIMESTAMP\n@@ -1 +1 @@\n-da\n+a\n", + da_path.display(), + a_path.display() ) ); let mut cmd = Command::cargo_bin("diffutils")?; - cmd.arg("-u") - .arg(&format!("{target}/a")) - .arg(&format!("{target}/d")); + cmd.arg("-u").arg(&a_path).arg(&directory); cmd.assert().code(predicate::eq(1)).failure(); let output = cmd.output().unwrap().stdout; assert_diff_eq!( output, format!( - "--- {}/a\tTIMESTAMP\n+++ {}/d/a\tTIMESTAMP\n@@ -1 +1 @@\n-a\n+da\n", - target, target + "--- {}\tTIMESTAMP\n+++ {}\tTIMESTAMP\n@@ -1 +1 @@\n-a\n+da\n", + a_path.display(), + da_path.display() ) ); From 3a8eddfe2c01c6d8134fdaa55376365eaf96b504 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 21 Apr 2024 16:07:01 +0200 Subject: [PATCH 090/194] Fix typos --- src/macros.rs | 2 +- tests/run-upstream-testsuite.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/macros.rs b/src/macros.rs index 1f1a994..90a4eaa 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -2,7 +2,7 @@ // considering datetime varitations // // It replaces the modification time in the actual diff -// with placeholer "TIMESTAMP" and then asserts the equality +// with placeholder "TIMESTAMP" and then asserts the equality // // For eg. // let brief = "*** fruits_old.txt\t2024-03-24 23:43:05.189597645 +0530\n diff --git a/tests/run-upstream-testsuite.sh b/tests/run-upstream-testsuite.sh index d85dfd6..cb59834 100755 --- a/tests/run-upstream-testsuite.sh +++ b/tests/run-upstream-testsuite.sh @@ -19,7 +19,7 @@ # By default it expects a release build of the diffutils binary, but a # different build profile can be specified as an argument # (e.g. 'dev' or 'test'). -# Unless overriden by the $TESTS environment variable, all tests in the test +# Unless overridden by the $TESTS environment variable, all tests in the test # suite will be run. Tests targeting a command that is not yet implemented # (e.g. cmp, diff3 or sdiff) are skipped. From fe28610f210b8addd5a497e7225111a67b308c51 Mon Sep 17 00:00:00 2001 From: Olivier Tilloy Date: Sat, 30 Mar 2024 16:42:07 +0100 Subject: [PATCH 091/194] Parse all valid arguments accepted by GNU diff to request a unified context (with an optional number of lines) --- src/params.rs | 131 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 103 insertions(+), 28 deletions(-) diff --git a/src/params.rs b/src/params.rs index 7e9cc78..c0fdd32 100644 --- a/src/params.rs +++ b/src/params.rs @@ -50,7 +50,7 @@ impl Default for Params { } pub fn parse_params>(opts: I) -> Result { - let mut opts = opts.into_iter(); + let mut opts = opts.into_iter().peekable(); // parse CLI let Some(exe) = opts.next() else { @@ -60,7 +60,10 @@ pub fn parse_params>(opts: I) -> Result\d+)$").unwrap(); + let unified_re = + Regex::new(r"^(-[uU](?\d*)|--unified(=(?\d*))?|-(?\d+)u)$").unwrap(); while let Some(param) = opts.next() { if param == "--" { break; @@ -103,6 +106,40 @@ pub fn parse_params>(opts: I) -> Result().unwrap()); + } + if param == "-U" { + let next_param = opts.peek(); + if next_param.is_some() { + let next_value = next_param + .unwrap() + .to_string_lossy() + .as_ref() + .parse::(); + if next_value.is_ok() { + context_count = Some(next_value.unwrap()); + opts.next(); + } else { + return Err(format!( + "invalid context length '{}'", + next_param.unwrap().to_string_lossy() + )); + } + } + } + continue; + } let p = osstr_bytes(¶m); if p.first() == Some(&b'-') && p.get(1) != Some(&b'-') { let mut bit = p[1..].iter().copied().peekable(); @@ -111,10 +148,12 @@ pub fn parse_params>(opts: I) -> Result { - params.context_count = (b - b'0') as usize; + context_count = Some((b - b'0') as usize); while let Some(b'0'..=b'9') = bit.peek() { - params.context_count *= 10; - params.context_count += (bit.next().unwrap() - b'0') as usize; + context_count = Some(context_count.unwrap() * 10); + context_count = Some( + context_count.unwrap() + (bit.next().unwrap() - b'0') as usize, + ); } } b'c' => { @@ -129,30 +168,6 @@ pub fn parse_params>(opts: I) -> Result { - if format.is_some() && format != Some(Format::Unified) { - return Err("Conflicting output style options".to_string()); - } - format = Some(Format::Unified); - } - b'U' => { - if format.is_some() && format != Some(Format::Unified) { - return Err("Conflicting output style options".to_string()); - } - format = Some(Format::Unified); - let context_count_maybe = if bit.peek().is_some() { - String::from_utf8(bit.collect::>()).ok() - } else { - opts.next().map(|x| x.to_string_lossy().into_owned()) - }; - if let Some(context_count_maybe) = - context_count_maybe.and_then(|x| x.parse().ok()) - { - params.context_count = context_count_maybe; - break; - } - return Err("Invalid context count".to_string()); - } _ => return Err(format!("Unknown option: {}", String::from_utf8_lossy(&[b]))), } } @@ -179,6 +194,9 @@ pub fn parse_params>(opts: I) -> Result ", exe.to_string_lossy())); }; params.format = format.unwrap_or(Format::default()); + if context_count.is_some() { + params.context_count = context_count.unwrap(); + } Ok(params) } @@ -212,6 +230,63 @@ mod tests { ); } #[test] + fn unified_valid() { + for args in [vec!["-u"], vec!["--unified"], vec!["--unified="]] { + let mut params = vec!["diff"]; + params.extend(args); + params.extend(["foo", "bar"]); + assert_eq!( + Ok(Params { + from: os("foo"), + to: os("bar"), + format: Format::Unified, + ..Default::default() + }), + parse_params(params.iter().map(|x| os(x))) + ); + } + for args in [ + vec!["-u42"], + vec!["-U42"], + vec!["-U", "42"], + vec!["--unified=42"], + vec!["-42u"], + ] { + let mut params = vec!["diff"]; + params.extend(args); + params.extend(["foo", "bar"]); + assert_eq!( + Ok(Params { + from: os("foo"), + to: os("bar"), + format: Format::Unified, + context_count: 42, + ..Default::default() + }), + parse_params(params.iter().map(|x| os(x))) + ); + } + } + #[test] + fn unified_invalid() { + for args in [ + vec!["-u", "42"], + vec!["-u=42"], + vec!["-u="], + vec!["-U"], + vec!["-U=42"], + vec!["-U="], + vec!["--unified42"], + vec!["--unified", "42"], + vec!["-42U"], + ] { + let mut params = vec!["diff"]; + params.extend(args); + params.extend(["foo", "bar"]); + assert!(parse_params(params.iter().map(|x| os(x))).is_err()); + } + } + #[test] fn context_count() { assert_eq!( Ok(Params { From 22d973fce6daaa8f1368f4900acfee31eaa956fe Mon Sep 17 00:00:00 2001 From: Olivier Tilloy Date: Tue, 9 Apr 2024 19:12:40 +0200 Subject: [PATCH 092/194] Parse all valid arguments accepted by GNU diff to request a regular context (with an optional number of lines) --- src/params.rs | 110 ++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 93 insertions(+), 17 deletions(-) diff --git a/src/params.rs b/src/params.rs index c0fdd32..f5ac0ee 100644 --- a/src/params.rs +++ b/src/params.rs @@ -62,6 +62,8 @@ pub fn parse_params>(opts: I) -> Result\d+)$").unwrap(); + let context_re = + Regex::new(r"^(-[cC](?\d*)|--context(=(?\d*))?|-(?\d+)c)$").unwrap(); let unified_re = Regex::new(r"^(-[uU](?\d*)|--unified(=(?\d*))?|-(?\d+)u)$").unwrap(); while let Some(param) = opts.next() { @@ -106,6 +108,40 @@ pub fn parse_params>(opts: I) -> Result().unwrap()); + } + if param == "-C" { + let next_param = opts.peek(); + if next_param.is_some() { + let next_value = next_param + .unwrap() + .to_string_lossy() + .as_ref() + .parse::(); + if next_value.is_ok() { + context_count = Some(next_value.unwrap()); + opts.next(); + } else { + return Err(format!( + "invalid context length '{}'", + next_param.unwrap().to_string_lossy() + )); + } + } + } + continue; + } if unified_re.is_match(param.to_string_lossy().as_ref()) { if format.is_some() && format != Some(Format::Unified) { return Err("Conflicting output style options".to_string()); @@ -143,25 +179,8 @@ pub fn parse_params>(opts: I) -> Result { - context_count = Some((b - b'0') as usize); - while let Some(b'0'..=b'9') = bit.peek() { - context_count = Some(context_count.unwrap() * 10); - context_count = Some( - context_count.unwrap() + (bit.next().unwrap() - b'0') as usize, - ); - } - } - b'c' => { - if format.is_some() && format != Some(Format::Context) { - return Err("Conflicting output style options".to_string()); - } - format = Some(Format::Context); - } b'e' => { if format.is_some() && format != Some(Format::Ed) { return Err("Conflicting output style options".to_string()); @@ -230,6 +249,63 @@ mod tests { ); } #[test] + fn context_valid() { + for args in [vec!["-c"], vec!["--context"], vec!["--context="]] { + let mut params = vec!["diff"]; + params.extend(args); + params.extend(["foo", "bar"]); + assert_eq!( + Ok(Params { + from: os("foo"), + to: os("bar"), + format: Format::Context, + ..Default::default() + }), + parse_params(params.iter().map(|x| os(x))) + ); + } + for args in [ + vec!["-c42"], + vec!["-C42"], + vec!["-C", "42"], + vec!["--context=42"], + vec!["-42c"], + ] { + let mut params = vec!["diff"]; + params.extend(args); + params.extend(["foo", "bar"]); + assert_eq!( + Ok(Params { + from: os("foo"), + to: os("bar"), + format: Format::Context, + context_count: 42, + ..Default::default() + }), + parse_params(params.iter().map(|x| os(x))) + ); + } + } + #[test] + fn context_invalid() { + for args in [ + vec!["-c", "42"], + vec!["-c=42"], + vec!["-c="], + vec!["-C"], + vec!["-C=42"], + vec!["-C="], + vec!["--context42"], + vec!["--context", "42"], + vec!["-42C"], + ] { + let mut params = vec!["diff"]; + params.extend(args); + params.extend(["foo", "bar"]); + assert!(parse_params(params.iter().map(|x| os(x))).is_err()); + } + } + #[test] fn unified_valid() { for args in [vec!["-u"], vec!["--unified"], vec!["--unified="]] { let mut params = vec!["diff"]; From 37fe1ae8089bc347b1166d437d32efa904b33914 Mon Sep 17 00:00:00 2001 From: Olivier Tilloy Date: Tue, 9 Apr 2024 19:42:17 +0200 Subject: [PATCH 093/194] Handle --normal, -e and --ed options --- src/params.rs | 80 ++++++++++++++++++++++++++------------------ tests/integration.rs | 2 +- 2 files changed, 48 insertions(+), 34 deletions(-) diff --git a/src/params.rs b/src/params.rs index f5ac0ee..a3cdfe6 100644 --- a/src/params.rs +++ b/src/params.rs @@ -1,4 +1,4 @@ -use std::ffi::{OsStr, OsString}; +use std::ffi::OsString; use regex::Regex; @@ -11,17 +11,6 @@ pub enum Format { Ed, } -#[cfg(unix)] -fn osstr_bytes(osstr: &OsStr) -> &[u8] { - use std::os::unix::ffi::OsStrExt; - osstr.as_bytes() -} - -#[cfg(not(unix))] -fn osstr_bytes(osstr: &OsStr) -> Vec { - osstr.to_string_lossy().bytes().collect() -} - #[derive(Clone, Debug, Eq, PartialEq)] pub struct Params { pub from: OsString, @@ -92,6 +81,20 @@ pub fn parse_params>(opts: I) -> Result>(opts: I) -> Result { - if format.is_some() && format != Some(Format::Ed) { - return Err("Conflicting output style options".to_string()); - } - format = Some(Format::Ed); - } - _ => return Err(format!("Unknown option: {}", String::from_utf8_lossy(&[b]))), - } - } - } else if from.is_none() { + if param.to_string_lossy().starts_with('-') { + return Err(format!("Unknown option: {:?}", param)); + } + if from.is_none() { from = Some(param); } else if to.is_none() { to = Some(param); @@ -235,20 +227,34 @@ mod tests { }), parse_params([os("diff"), os("foo"), os("bar")].iter().cloned()) ); - } - #[test] - fn basics_ed() { assert_eq!( Ok(Params { from: os("foo"), to: os("bar"), - format: Format::Ed, ..Default::default() }), - parse_params([os("diff"), os("-e"), os("foo"), os("bar")].iter().cloned()) + parse_params( + [os("diff"), os("--normal"), os("foo"), os("bar")] + .iter() + .cloned() + ) ); } #[test] + fn basics_ed() { + for arg in ["-e", "--ed"] { + assert_eq!( + Ok(Params { + from: os("foo"), + to: os("bar"), + format: Format::Ed, + ..Default::default() + }), + parse_params([os("diff"), os(arg), os("foo"), os("bar")].iter().cloned()) + ); + } + } + #[test] fn context_valid() { for args in [vec!["-c"], vec!["--context"], vec!["--context="]] { let mut params = vec!["diff"]; @@ -655,7 +661,15 @@ mod tests { } #[test] fn conflicting_output_styles() { - for (arg1, arg2) in [("-u", "-c"), ("-u", "-e"), ("-c", "-u"), ("-c", "-U42")] { + for (arg1, arg2) in [ + ("-u", "-c"), + ("-u", "-e"), + ("-c", "-u"), + ("-c", "-U42"), + ("-u", "--normal"), + ("--normal", "-e"), + ("--context", "--normal"), + ] { assert!(parse_params( [os("diff"), os(arg1), os(arg2), os("foo"), os("bar")] .iter() diff --git a/tests/integration.rs b/tests/integration.rs index 853ba4d..8e4758e 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -18,7 +18,7 @@ fn unknown_param() -> Result<(), Box> { cmd.assert() .code(predicate::eq(2)) .failure() - .stderr(predicate::str::starts_with("Usage: ")); + .stderr(predicate::str::starts_with("Unknown option: \"--foobar\"")); Ok(()) } From b7261a43f47b26cbb4614aaf72a29cd692c53dda Mon Sep 17 00:00:00 2001 From: Olivier Tilloy Date: Thu, 11 Apr 2024 22:47:16 +0200 Subject: [PATCH 094/194] Break out the logic to match context/unified diff params into separate functions, for improved readability --- src/params.rs | 195 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 130 insertions(+), 65 deletions(-) diff --git a/src/params.rs b/src/params.rs index a3cdfe6..88feb47 100644 --- a/src/params.rs +++ b/src/params.rs @@ -49,13 +49,10 @@ pub fn parse_params>(opts: I) -> Result\d+)$").unwrap(); - let context_re = - Regex::new(r"^(-[cC](?\d*)|--context(=(?\d*))?|-(?\d+)c)$").unwrap(); - let unified_re = - Regex::new(r"^(-[uU](?\d*)|--unified(=(?\d*))?|-(?\d+)u)$").unwrap(); while let Some(param) = opts.next() { + let next_param = opts.peek(); if param == "--" { break; } @@ -111,73 +108,43 @@ pub fn parse_params>(opts: I) -> Result().unwrap()); - } - if param == "-C" { - let next_param = opts.peek(); - if next_param.is_some() { - let next_value = next_param - .unwrap() - .to_string_lossy() - .as_ref() - .parse::(); - if next_value.is_ok() { - context_count = Some(next_value.unwrap()); + match match_context_diff_params(¶m, next_param, format) { + Ok(DiffStyleMatch { + is_match, + context_count, + next_param_consumed, + }) => { + if is_match { + format = Some(Format::Context); + if context_count.is_some() { + context = context_count; + } + if next_param_consumed { opts.next(); - } else { - return Err(format!( - "invalid context length '{}'", - next_param.unwrap().to_string_lossy() - )); } + continue; } } - continue; + Err(error) => return Err(error), } - if unified_re.is_match(param.to_string_lossy().as_ref()) { - if format.is_some() && format != Some(Format::Unified) { - return Err("Conflicting output style options".to_string()); - } - format = Some(Format::Unified); - let captures = unified_re.captures(param.to_str().unwrap()).unwrap(); - let num = captures - .name("num1") - .or(captures.name("num2")) - .or(captures.name("num3")); - if num.is_some() && !num.unwrap().as_str().is_empty() { - context_count = Some(num.unwrap().as_str().parse::().unwrap()); - } - if param == "-U" { - let next_param = opts.peek(); - if next_param.is_some() { - let next_value = next_param - .unwrap() - .to_string_lossy() - .as_ref() - .parse::(); - if next_value.is_ok() { - context_count = Some(next_value.unwrap()); + match match_unified_diff_params(¶m, next_param, format) { + Ok(DiffStyleMatch { + is_match, + context_count, + next_param_consumed, + }) => { + if is_match { + format = Some(Format::Unified); + if context_count.is_some() { + context = context_count; + } + if next_param_consumed { opts.next(); - } else { - return Err(format!( - "invalid context length '{}'", - next_param.unwrap().to_string_lossy() - )); } + continue; } } - continue; + Err(error) => return Err(error), } if param.to_string_lossy().starts_with('-') { return Err(format!("Unknown option: {:?}", param)); @@ -205,12 +172,110 @@ pub fn parse_params>(opts: I) -> Result ", exe.to_string_lossy())); }; params.format = format.unwrap_or(Format::default()); - if context_count.is_some() { - params.context_count = context_count.unwrap(); + if let Some(context_count) = context { + params.context_count = context_count; } Ok(params) } +struct DiffStyleMatch { + is_match: bool, + context_count: Option, + next_param_consumed: bool, +} + +fn match_context_diff_params( + param: &OsString, + next_param: Option<&OsString>, + format: Option, +) -> Result { + const CONTEXT_RE: &str = r"^(-[cC](?\d*)|--context(=(?\d*))?|-(?\d+)c)$"; + let regex = Regex::new(CONTEXT_RE).unwrap(); + let is_match = regex.is_match(param.to_string_lossy().as_ref()); + let mut context_count = None; + let mut next_param_consumed = false; + if is_match { + if format.is_some() && format != Some(Format::Context) { + return Err("Conflicting output style options".to_string()); + } + let captures = regex.captures(param.to_str().unwrap()).unwrap(); + let num = captures + .name("num1") + .or(captures.name("num2")) + .or(captures.name("num3")); + if let Some(numvalue) = num { + if !numvalue.as_str().is_empty() { + context_count = Some(numvalue.as_str().parse::().unwrap()); + } + } + if param == "-C" && next_param.is_some() { + match next_param.unwrap().to_string_lossy().parse::() { + Ok(context_size) => { + context_count = Some(context_size); + next_param_consumed = true; + } + Err(_) => { + return Err(format!( + "invalid context length '{}'", + next_param.unwrap().to_string_lossy() + )) + } + } + } + } + Ok(DiffStyleMatch { + is_match, + context_count, + next_param_consumed, + }) +} + +fn match_unified_diff_params( + param: &OsString, + next_param: Option<&OsString>, + format: Option, +) -> Result { + const UNIFIED_RE: &str = r"^(-[uU](?\d*)|--unified(=(?\d*))?|-(?\d+)u)$"; + let regex = Regex::new(UNIFIED_RE).unwrap(); + let is_match = regex.is_match(param.to_string_lossy().as_ref()); + let mut context_count = None; + let mut next_param_consumed = false; + if is_match { + if format.is_some() && format != Some(Format::Unified) { + return Err("Conflicting output style options".to_string()); + } + let captures = regex.captures(param.to_str().unwrap()).unwrap(); + let num = captures + .name("num1") + .or(captures.name("num2")) + .or(captures.name("num3")); + if let Some(numvalue) = num { + if !numvalue.as_str().is_empty() { + context_count = Some(numvalue.as_str().parse::().unwrap()); + } + } + if param == "-U" && next_param.is_some() { + match next_param.unwrap().to_string_lossy().parse::() { + Ok(context_size) => { + context_count = Some(context_size); + next_param_consumed = true; + } + Err(_) => { + return Err(format!( + "invalid context length '{}'", + next_param.unwrap().to_string_lossy() + )) + } + } + } + } + Ok(DiffStyleMatch { + is_match, + context_count, + next_param_consumed, + }) +} + #[cfg(test)] mod tests { use super::*; From 3dc3fdf5cd49dedde32a2faf778a3b987c8246b9 Mon Sep 17 00:00:00 2001 From: Olivier Tilloy Date: Tue, 23 Apr 2024 17:56:32 +0200 Subject: [PATCH 095/194] Un-hardcode a test filename in an integration test (fixes #61) --- tests/integration.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/integration.rs b/tests/integration.rs index 853ba4d..06f7fb6 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -26,22 +26,26 @@ fn unknown_param() -> Result<(), Box> { fn cannot_read_files() -> Result<(), Box> { let file = NamedTempFile::new()?; + let nofile = NamedTempFile::new()?; + let nopath = nofile.into_temp_path(); + std::fs::remove_file(&nopath)?; + let mut cmd = Command::cargo_bin("diffutils")?; - cmd.arg("foo.txt").arg(file.path()); + cmd.arg(&nopath).arg(file.path()); cmd.assert() .code(predicate::eq(2)) .failure() .stderr(predicate::str::starts_with("Failed to read from-file")); let mut cmd = Command::cargo_bin("diffutils")?; - cmd.arg(file.path()).arg("foo.txt"); + cmd.arg(file.path()).arg(&nopath); cmd.assert() .code(predicate::eq(2)) .failure() .stderr(predicate::str::starts_with("Failed to read to-file")); let mut cmd = Command::cargo_bin("diffutils")?; - cmd.arg("foo.txt").arg("foo.txt"); + cmd.arg(&nopath).arg(&nopath); cmd.assert() .code(predicate::eq(2)) .failure() From 0304391bc530c86c59dc9a8b03109ea67fa9d00d Mon Sep 17 00:00:00 2001 From: Tanmay Patil Date: Tue, 23 Apr 2024 22:44:06 +0530 Subject: [PATCH 096/194] Create test files in temporary directory --- tests/integration.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/integration.rs b/tests/integration.rs index f7174fd..3e14c3c 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -8,8 +8,7 @@ use diffutilslib::assert_diff_eq; use predicates::prelude::*; use std::fs::File; use std::io::Write; -use std::path::PathBuf; -use tempfile::NamedTempFile; +use tempfile::{tempdir, NamedTempFile}; // Integration tests for the diffutils command @@ -238,14 +237,13 @@ fn read_from_stdin() -> Result<(), Box> { } #[test] -fn read_from_directory() -> Result<(), Box> { - let target = PathBuf::from("target/integration"); - let _ = std::fs::create_dir(&target); +fn compare_file_to_directory() -> Result<(), Box> { + let tmp_dir = tempdir()?; - let directory = target.join("d"); + let directory = tmp_dir.path().join("d"); let _ = std::fs::create_dir(&directory); - let a_path = target.join("a"); + let a_path = tmp_dir.path().join("a"); let mut a = File::create(&a_path).unwrap(); a.write_all(b"a\n").unwrap(); From 99d4d029857dfeb55c810f571f8ca4105f7281fd Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 27 Apr 2024 13:12:16 +0200 Subject: [PATCH 097/194] add missing copyright --- LICENSE-APACHE | 3 +++ LICENSE-MIT | 3 +++ 2 files changed, 6 insertions(+) diff --git a/LICENSE-APACHE b/LICENSE-APACHE index 1b5ec8b..3d8493e 100644 --- a/LICENSE-APACHE +++ b/LICENSE-APACHE @@ -1,3 +1,6 @@ +Copyright (c) Michael Howell +Copyright (c) uutils developers + Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ diff --git a/LICENSE-MIT b/LICENSE-MIT index 31aa793..ba40932 100644 --- a/LICENSE-MIT +++ b/LICENSE-MIT @@ -1,3 +1,6 @@ +Copyright (c) Michael Howell +Copyright (c) uutils developers + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the From d92132e72154a2f59bd053ee523175b7c235049c Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 27 Apr 2024 13:12:58 +0200 Subject: [PATCH 098/194] version 0.4.1 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d70d307..2244a3b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -122,7 +122,7 @@ checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" [[package]] name = "diffutils" -version = "0.4.0" +version = "0.4.1" dependencies = [ "assert_cmd", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 0e8dab9..f219dbb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "diffutils" -version = "0.4.0" +version = "0.4.1" edition = "2021" description = "A CLI app for generating diff files" license = "MIT OR Apache-2.0" From df778c610b62e693ba4cc4923c2d280db5a97781 Mon Sep 17 00:00:00 2001 From: Olivier Tilloy Date: Mon, 29 Apr 2024 22:55:08 +0200 Subject: [PATCH 099/194] CI: install GNU patch on MacOS (fixes #66) --- .github/workflows/ci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index be76d96..ddf2ea1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,6 +28,9 @@ jobs: steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable + - name: install GNU patch on MacOS + if: runner.os == 'macOS' + run: brew install gpatch - name: set up PATH on Windows # Needed to use GNU's patch.exe instead of Strawberry Perl patch if: runner.os == 'Windows' @@ -107,6 +110,9 @@ jobs: - name: rust toolchain ~ install uses: dtolnay/rust-toolchain@nightly + - name: install GNU patch on MacOS + if: runner.os == 'macOS' + run: brew install gpatch - name: set up PATH on Windows # Needed to use GNU's patch.exe instead of Strawberry Perl patch if: runner.os == 'Windows' From bf9147733debef0a5caa5930401430290786e8e6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 30 Apr 2024 05:30:32 +0000 Subject: [PATCH 100/194] Update Rust crate unicode-width to 0.1.12 --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2244a3b..44d6b6c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -409,9 +409,9 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-width" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" [[package]] name = "wait-timeout" diff --git a/Cargo.toml b/Cargo.toml index f219dbb..94a585e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ chrono = "0.4.38" diff = "0.1.13" regex = "1.10.4" same-file = "1.0.6" -unicode-width = "0.1.11" +unicode-width = "0.1.12" [dev-dependencies] pretty_assertions = "1" From 713bd210ab0a84a8223e4f310dc6622385bec7ed Mon Sep 17 00:00:00 2001 From: Olivier Tilloy Date: Tue, 30 Apr 2024 23:55:49 +0200 Subject: [PATCH 101/194] CI: Update 'cargo dist' to version 0.13.3 --- .github/workflows/release.yml | 31 ++++++++++++++++++------------- Cargo.toml | 2 +- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 502a5ff..4c1f4f8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -# Copyright 2022-2023, axodotdev +# Copyright 2022-2024, axodotdev # SPDX-License-Identifier: MIT or Apache-2.0 # # CI that: @@ -6,9 +6,9 @@ # * checks for a Git Tag that looks like a release # * builds artifacts with cargo-dist (archives, installers, hashes) # * uploads those artifacts to temporary workflow zip -# * on success, uploads the artifacts to a Github Release +# * on success, uploads the artifacts to a GitHub Release # -# Note that the Github Release will be created with a generated +# Note that the GitHub Release will be created with a generated # title/body based on your changelogs. name: Release @@ -31,7 +31,7 @@ permissions: # packages versioned/released in lockstep). # # If you push multiple tags at once, separate instances of this workflow will -# spin up, creating an independent announcement for each one. However Github +# spin up, creating an independent announcement for each one. However, GitHub # will hard limit this to 3 tags per commit, as it will assume more tags is a # mistake. # @@ -62,7 +62,7 @@ jobs: # we specify bash to get pipefail; it guards against the `curl` command # failing. otherwise `sh` won't catch that `curl` returned non-0 shell: bash - run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.12.0/cargo-dist-installer.sh | sh" + run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.13.3/cargo-dist-installer.sh | sh" # sure would be cool if github gave us proper conditionals... # so here's a doubly-nested ternary-via-truthiness to try to provide the best possible # functionality based on whether this is a pull_request, and whether it's from a fork. @@ -105,10 +105,15 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json steps: + - name: enable windows longpaths + run: | + git config --global core.longpaths true - uses: actions/checkout@v4 with: submodules: recursive - uses: swatinem/rust-cache@v2 + with: + key: ${{ join(matrix.targets, '-') }} - name: Install cargo-dist run: ${{ matrix.install_dist }} # Get the dist-manifest @@ -135,7 +140,7 @@ jobs: run: | # Parse out what we just built and upload it to scratch storage echo "paths<> "$GITHUB_OUTPUT" - jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json >> "$GITHUB_OUTPUT" + jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" echo "EOF" >> "$GITHUB_OUTPUT" cp dist-manifest.json "$BUILD_MANIFEST_NAME" @@ -162,7 +167,7 @@ jobs: submodules: recursive - name: Install cargo-dist shell: bash - run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.12.0/cargo-dist-installer.sh | sh" + run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.13.3/cargo-dist-installer.sh | sh" # Get all the local artifacts for the global tasks to use (for e.g. checksums) - name: Fetch local artifacts uses: actions/download-artifact@v4 @@ -178,7 +183,7 @@ jobs: # Parse out what we just built and upload it to scratch storage echo "paths<> "$GITHUB_OUTPUT" - jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json >> "$GITHUB_OUTPUT" + jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" echo "EOF" >> "$GITHUB_OUTPUT" cp dist-manifest.json "$BUILD_MANIFEST_NAME" @@ -207,7 +212,7 @@ jobs: with: submodules: recursive - name: Install cargo-dist - run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.12.0/cargo-dist-installer.sh | sh" + run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.13.3/cargo-dist-installer.sh | sh" # Fetch artifacts from scratch-storage - name: Fetch artifacts uses: actions/download-artifact@v4 @@ -215,7 +220,7 @@ jobs: pattern: artifacts-* path: target/distrib/ merge-multiple: true - # This is a harmless no-op for Github Releases, hosting for that happens in "announce" + # This is a harmless no-op for GitHub Releases, hosting for that happens in "announce" - id: host shell: bash run: | @@ -230,7 +235,7 @@ jobs: name: artifacts-dist-manifest path: dist-manifest.json - # Create a Github Release while uploading all files to it + # Create a GitHub Release while uploading all files to it announce: needs: - plan @@ -246,7 +251,7 @@ jobs: - uses: actions/checkout@v4 with: submodules: recursive - - name: "Download Github Artifacts" + - name: "Download GitHub Artifacts" uses: actions/download-artifact@v4 with: pattern: artifacts-* @@ -256,7 +261,7 @@ jobs: run: | # Remove the granular manifests rm -f artifacts/*-dist-manifest.json - - name: Create Github Release + - name: Create GitHub Release uses: ncipollo/release-action@v1 with: tag: ${{ needs.plan.outputs.tag }} diff --git a/Cargo.toml b/Cargo.toml index 94a585e..b5c638a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,7 @@ lto = "thin" # Config for 'cargo dist' [workspace.metadata.dist] # The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax) -cargo-dist-version = "0.12.0" +cargo-dist-version = "0.13.3" # CI backends to support ci = ["github"] # The installers to generate for each app From 767c6f6c4a712f25df3f14ba5538755a8fc81254 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 1 May 2024 09:56:50 +0000 Subject: [PATCH 102/194] Update Rust crate libfuzzer-sys to 0.4.7 --- fuzz/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 650e1d4..5debf47 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -9,7 +9,7 @@ edition = "2018" cargo-fuzz = true [dependencies] -libfuzzer-sys = "0.4" +libfuzzer-sys = "0.4.7" diffutils = { path = "../" } # Prevent this from interfering with workspaces From 4b70969ff18ad1cb2c290ecdfa977e0937618d54 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 1 May 2024 09:56:55 +0000 Subject: [PATCH 103/194] Update Rust crate pretty_assertions to 1.4.0 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index b5c638a..4963f29 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ same-file = "1.0.6" unicode-width = "0.1.12" [dev-dependencies] -pretty_assertions = "1" +pretty_assertions = "1.4.0" assert_cmd = "2.0.14" predicates = "3.1.0" tempfile = "3.10.1" From 80b993141b377c8651bbb71c8ceee93518855d45 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 19 May 2024 11:59:26 +0200 Subject: [PATCH 104/194] cargo-dist: generate more targets --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 4963f29..7a412e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,6 @@ ci = ["github"] # The installers to generate for each app installers = [] # Target platforms to build apps for (Rust target-triple syntax) -targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"] +targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "aarch64-unknown-linux-musl", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl", "x86_64-pc-windows-msvc"] # Publish jobs to run in CI pr-run-mode = "plan" From d362046ae5f54d78e4a6812d6acae5099f2f99b2 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 19 May 2024 19:09:05 +0200 Subject: [PATCH 105/194] release v0.4.2 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 44d6b6c..64de5f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -122,7 +122,7 @@ checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" [[package]] name = "diffutils" -version = "0.4.1" +version = "0.4.2" dependencies = [ "assert_cmd", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 7a412e6..fa80fc6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "diffutils" -version = "0.4.1" +version = "0.4.2" edition = "2021" description = "A CLI app for generating diff files" license = "MIT OR Apache-2.0" From fa4e0c6097a9ddc1a63f35865781b9c25ad20e1b Mon Sep 17 00:00:00 2001 From: Olivier Tilloy Date: Tue, 23 Apr 2024 20:02:34 +0200 Subject: [PATCH 106/194] Make error message consistent with GNU diff's implementation when failing to read input file(s) --- src/main.rs | 33 ++++++++++++++++++++++---- src/params.rs | 55 +++++++++++++++++++++++++++++++++++++++----- tests/integration.rs | 21 ++++++++++++----- 3 files changed, 93 insertions(+), 16 deletions(-) diff --git a/src/main.rs b/src/main.rs index f1bd1e2..7e221ea 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ // files that was distributed with this source code. use crate::params::{parse_params, Format}; +use regex::Regex; use std::env; use std::ffi::OsString; use std::fs; @@ -18,6 +19,22 @@ mod params; mod unified_diff; mod utils; +fn report_failure_to_read_input_file( + executable: &OsString, + filepath: &OsString, + error: &std::io::Error, +) { + // std::io::Error's display trait outputs "{detail} (os error {code})" + // but we want only the {detail} (error string) part + let error_code_re = Regex::new(r"\ \(os\ error\ \d+\)$").unwrap(); + eprintln!( + "{}: {}: {}", + executable.to_string_lossy(), + filepath.to_string_lossy(), + error_code_re.replace(error.to_string().as_str(), ""), + ); +} + // Exit codes are documented at // https://www.gnu.org/software/diffutils/manual/html_node/Invoking-diff.html. // An exit status of 0 means no differences were found, @@ -45,6 +62,7 @@ fn main() -> ExitCode { maybe_report_identical_files(); return ExitCode::SUCCESS; } + // read files fn read_file_contents(filepath: &OsString) -> io::Result> { if filepath == "-" { @@ -54,20 +72,27 @@ fn main() -> ExitCode { fs::read(filepath) } } + let mut io_error = false; let from_content = match read_file_contents(¶ms.from) { Ok(from_content) => from_content, Err(e) => { - eprintln!("Failed to read from-file: {e}"); - return ExitCode::from(2); + report_failure_to_read_input_file(¶ms.executable, ¶ms.from, &e); + io_error = true; + vec![] } }; let to_content = match read_file_contents(¶ms.to) { Ok(to_content) => to_content, Err(e) => { - eprintln!("Failed to read to-file: {e}"); - return ExitCode::from(2); + report_failure_to_read_input_file(¶ms.executable, ¶ms.to, &e); + io_error = true; + vec![] } }; + if io_error { + return ExitCode::from(2); + } + // run diff let result: Vec = match params.format { Format::Normal => normal_diff::diff(&from_content, &to_content, ¶ms), diff --git a/src/params.rs b/src/params.rs index 62ee518..c671180 100644 --- a/src/params.rs +++ b/src/params.rs @@ -14,6 +14,7 @@ pub enum Format { #[derive(Clone, Debug, Eq, PartialEq)] pub struct Params { + pub executable: OsString, pub from: OsString, pub to: OsString, pub format: Format, @@ -27,6 +28,7 @@ pub struct Params { impl Default for Params { fn default() -> Self { Self { + executable: OsString::default(), from: OsString::default(), to: OsString::default(), format: Format::default(), @@ -43,10 +45,13 @@ pub fn parse_params>(opts: I) -> Result ".to_string()); }; - let mut params = Params::default(); + let mut params = Params { + executable, + ..Default::default() + }; let mut from = None; let mut to = None; let mut format = None; @@ -63,7 +68,10 @@ pub fn parse_params>(opts: I) -> Result ", exe.to_string_lossy())); + return Err(format!( + "Usage: {} ", + params.executable.to_string_lossy() + )); } continue; } @@ -155,7 +163,10 @@ pub fn parse_params>(opts: I) -> Result ", exe.to_string_lossy())); + return Err(format!( + "Usage: {} ", + params.executable.to_string_lossy() + )); } } params.from = if let Some(from) = from { @@ -163,14 +174,20 @@ pub fn parse_params>(opts: I) -> Result ", exe.to_string_lossy())); + return Err(format!( + "Usage: {} ", + params.executable.to_string_lossy() + )); }; params.to = if let Some(to) = to { to } else if let Some(param) = opts.next() { param } else { - return Err(format!("Usage: {} ", exe.to_string_lossy())); + return Err(format!( + "Usage: {} ", + params.executable.to_string_lossy() + )); }; // diff DIRECTORY FILE => diff DIRECTORY/FILE FILE @@ -301,6 +318,7 @@ mod tests { fn basics() { assert_eq!( Ok(Params { + executable: os("diff"), from: os("foo"), to: os("bar"), ..Default::default() @@ -309,6 +327,7 @@ mod tests { ); assert_eq!( Ok(Params { + executable: os("diff"), from: os("foo"), to: os("bar"), ..Default::default() @@ -325,6 +344,7 @@ mod tests { for arg in ["-e", "--ed"] { assert_eq!( Ok(Params { + executable: os("diff"), from: os("foo"), to: os("bar"), format: Format::Ed, @@ -342,6 +362,7 @@ mod tests { params.extend(["foo", "bar"]); assert_eq!( Ok(Params { + executable: os("diff"), from: os("foo"), to: os("bar"), format: Format::Context, @@ -362,6 +383,7 @@ mod tests { params.extend(["foo", "bar"]); assert_eq!( Ok(Params { + executable: os("diff"), from: os("foo"), to: os("bar"), format: Format::Context, @@ -399,6 +421,7 @@ mod tests { params.extend(["foo", "bar"]); assert_eq!( Ok(Params { + executable: os("diff"), from: os("foo"), to: os("bar"), format: Format::Unified, @@ -419,6 +442,7 @@ mod tests { params.extend(["foo", "bar"]); assert_eq!( Ok(Params { + executable: os("diff"), from: os("foo"), to: os("bar"), format: Format::Unified, @@ -452,6 +476,7 @@ mod tests { fn context_count() { assert_eq!( Ok(Params { + executable: os("diff"), from: os("foo"), to: os("bar"), format: Format::Unified, @@ -466,6 +491,7 @@ mod tests { ); assert_eq!( Ok(Params { + executable: os("diff"), from: os("foo"), to: os("bar"), format: Format::Unified, @@ -480,6 +506,7 @@ mod tests { ); assert_eq!( Ok(Params { + executable: os("diff"), from: os("foo"), to: os("bar"), format: Format::Unified, @@ -494,6 +521,7 @@ mod tests { ); assert_eq!( Ok(Params { + executable: os("diff"), from: os("foo"), to: os("bar"), format: Format::Context, @@ -511,6 +539,7 @@ mod tests { fn report_identical_files() { assert_eq!( Ok(Params { + executable: os("diff"), from: os("foo"), to: os("bar"), ..Default::default() @@ -519,6 +548,7 @@ mod tests { ); assert_eq!( Ok(Params { + executable: os("diff"), from: os("foo"), to: os("bar"), report_identical_files: true, @@ -528,6 +558,7 @@ mod tests { ); assert_eq!( Ok(Params { + executable: os("diff"), from: os("foo"), to: os("bar"), report_identical_files: true, @@ -549,6 +580,7 @@ mod tests { fn brief() { assert_eq!( Ok(Params { + executable: os("diff"), from: os("foo"), to: os("bar"), ..Default::default() @@ -557,6 +589,7 @@ mod tests { ); assert_eq!( Ok(Params { + executable: os("diff"), from: os("foo"), to: os("bar"), brief: true, @@ -566,6 +599,7 @@ mod tests { ); assert_eq!( Ok(Params { + executable: os("diff"), from: os("foo"), to: os("bar"), brief: true, @@ -582,6 +616,7 @@ mod tests { fn expand_tabs() { assert_eq!( Ok(Params { + executable: os("diff"), from: os("foo"), to: os("bar"), ..Default::default() @@ -591,6 +626,7 @@ mod tests { for option in ["-t", "--expand-tabs"] { assert_eq!( Ok(Params { + executable: os("diff"), from: os("foo"), to: os("bar"), expand_tabs: true, @@ -608,6 +644,7 @@ mod tests { fn tabsize() { assert_eq!( Ok(Params { + executable: os("diff"), from: os("foo"), to: os("bar"), ..Default::default() @@ -616,6 +653,7 @@ mod tests { ); assert_eq!( Ok(Params { + executable: os("diff"), from: os("foo"), to: os("bar"), tabsize: 0, @@ -629,6 +667,7 @@ mod tests { ); assert_eq!( Ok(Params { + executable: os("diff"), from: os("foo"), to: os("bar"), tabsize: 42, @@ -686,6 +725,7 @@ mod tests { fn double_dash() { assert_eq!( Ok(Params { + executable: os("diff"), from: os("-g"), to: os("-h"), ..Default::default() @@ -697,6 +737,7 @@ mod tests { fn default_to_stdin() { assert_eq!( Ok(Params { + executable: os("diff"), from: os("foo"), to: os("-"), ..Default::default() @@ -705,6 +746,7 @@ mod tests { ); assert_eq!( Ok(Params { + executable: os("diff"), from: os("-"), to: os("bar"), ..Default::default() @@ -713,6 +755,7 @@ mod tests { ); assert_eq!( Ok(Params { + executable: os("diff"), from: os("-"), to: os("-"), ..Default::default() diff --git a/tests/integration.rs b/tests/integration.rs index 9d0cfaf..ee8f15c 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -36,21 +36,30 @@ fn cannot_read_files() -> Result<(), Box> { cmd.assert() .code(predicate::eq(2)) .failure() - .stderr(predicate::str::starts_with("Failed to read from-file")); + .stderr(predicate::str::ends_with(format!( + ": {}: No such file or directory\n", + &nopath.as_os_str().to_string_lossy() + ))); let mut cmd = Command::cargo_bin("diffutils")?; cmd.arg(file.path()).arg(&nopath); cmd.assert() .code(predicate::eq(2)) .failure() - .stderr(predicate::str::starts_with("Failed to read to-file")); + .stderr(predicate::str::ends_with(format!( + ": {}: No such file or directory\n", + &nopath.as_os_str().to_string_lossy() + ))); let mut cmd = Command::cargo_bin("diffutils")?; cmd.arg(&nopath).arg(&nopath); - cmd.assert() - .code(predicate::eq(2)) - .failure() - .stderr(predicate::str::starts_with("Failed to read from-file")); + cmd.assert().code(predicate::eq(2)).failure().stderr( + predicate::str::contains(format!( + ": {}: No such file or directory\n", + &nopath.as_os_str().to_string_lossy() + )) + .count(2), + ); Ok(()) } From 8a3a977d2caae6002f945915d48165ec3534184a Mon Sep 17 00:00:00 2001 From: Olivier Tilloy Date: Tue, 23 Apr 2024 22:29:02 +0200 Subject: [PATCH 107/194] Update the expected error message for Windows --- tests/integration.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/integration.rs b/tests/integration.rs index ee8f15c..f8ad515 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -31,13 +31,18 @@ fn cannot_read_files() -> Result<(), Box> { let nopath = nofile.into_temp_path(); std::fs::remove_file(&nopath)?; + #[cfg(not(windows))] + let error_message = "No such file or directory"; + #[cfg(windows)] + let error_message = "The system cannot find the file specified."; + let mut cmd = Command::cargo_bin("diffutils")?; cmd.arg(&nopath).arg(file.path()); cmd.assert() .code(predicate::eq(2)) .failure() .stderr(predicate::str::ends_with(format!( - ": {}: No such file or directory\n", + ": {}: {error_message}\n", &nopath.as_os_str().to_string_lossy() ))); @@ -47,7 +52,7 @@ fn cannot_read_files() -> Result<(), Box> { .code(predicate::eq(2)) .failure() .stderr(predicate::str::ends_with(format!( - ": {}: No such file or directory\n", + ": {}: {error_message}\n", &nopath.as_os_str().to_string_lossy() ))); @@ -55,7 +60,7 @@ fn cannot_read_files() -> Result<(), Box> { cmd.arg(&nopath).arg(&nopath); cmd.assert().code(predicate::eq(2)).failure().stderr( predicate::str::contains(format!( - ": {}: No such file or directory\n", + ": {}: {error_message}\n", &nopath.as_os_str().to_string_lossy() )) .count(2), From eee6f499209cba06255bcb589638dd553dd00ac9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 4 Jun 2024 17:06:50 +0000 Subject: [PATCH 108/194] Update Rust crate unicode-width to v0.1.13 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 64de5f1..fe430aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -409,9 +409,9 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-width" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" [[package]] name = "wait-timeout" From e98b5e179ed40c08f0228813d74562406cc55d6b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 9 Jun 2024 13:30:51 +0000 Subject: [PATCH 109/194] Update Rust crate regex to v1.10.5 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fe430aa..e0cc642 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -303,9 +303,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.4" +version = "1.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" dependencies = [ "aho-corasick", "memchr", From c9a756eb43c08f2e4f5f6aa297a0da3e6a26f75c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 25 Jul 2024 14:28:54 +0000 Subject: [PATCH 110/194] Update Rust crate assert_cmd to v2.0.15 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e0cc642..d8abd7b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -34,9 +34,9 @@ checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" [[package]] name = "assert_cmd" -version = "2.0.14" +version = "2.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed72493ac66d5804837f480ab3766c72bdfab91a65e565fc54fa9e42db0073a8" +checksum = "bc65048dd435533bb1baf2ed9956b9a278fbfdcf90301b39ee117f06c0199d37" dependencies = [ "anstyle", "bstr", From e9a814161884b987bd69ccea3f4e4769bd5fe8e8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 25 Jul 2024 17:59:30 +0000 Subject: [PATCH 111/194] Update Rust crate predicates to v3.1.2 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e0cc642..3763a14 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -245,9 +245,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "predicates" -version = "3.1.0" +version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b87bfd4605926cdfefc1c3b5f8fe560e3feca9d5552cf68c466d3d8236c7e8" +checksum = "7e9086cc7640c29a356d1a29fd134380bee9d8f79a17410aa76e7ad295f42c97" dependencies = [ "anstyle", "difflib", From e55ee893ddb86a6085ce0c5c8a258c31d493c1f3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 2 Aug 2024 16:27:14 +0000 Subject: [PATCH 112/194] Update Rust crate regex to v1.10.6 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ba88a17..4875ac4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -303,9 +303,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.5" +version = "1.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" dependencies = [ "aho-corasick", "memchr", From eea6b62b20573d6b32c14498c43f062cb7a66cdd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 2 Aug 2024 22:18:35 +0000 Subject: [PATCH 113/194] Update Rust crate tempfile to v3.11.0 --- Cargo.lock | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4875ac4..2f3062a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -385,12 +385,13 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.10.1" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +checksum = "b8fcd239983515c23a32fb82099f97d0b11b8c72f654ed659363a95c3dad7a53" dependencies = [ "cfg-if", "fastrand", + "once_cell", "rustix", "windows-sys", ] From 67ef43083aa12a0978017dc810674a1220e4170a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 6 Aug 2024 23:23:06 +0000 Subject: [PATCH 114/194] Update Rust crate tempfile to v3.12.0 --- Cargo.lock | 58 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2f3062a..0fda722 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -148,7 +148,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -340,7 +340,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -385,15 +385,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.11.0" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fcd239983515c23a32fb82099f97d0b11b8c72f654ed659363a95c3dad7a53" +checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" dependencies = [ "cfg-if", "fastrand", "once_cell", "rustix", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -526,15 +526,25 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-targets" -version = "0.52.2" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98532992affa02e52709d5b4d145a3668ae10d9081eea4a7f26f719a8476f71" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", + "windows_i686_gnullvm", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", @@ -543,45 +553,51 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.1" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7269c1442e75af9fa59290383f7665b828efc76c429cc0b7f2ecb33cf51ebae" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" -version = "0.52.1" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f70ab2cebf332b7ecbdd98900c2da5298a8c862472fb35c75fc297eabb9d89b8" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" -version = "0.52.1" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "679f235acf6b1639408c0f6db295697a19d103b0cdc88146aa1b992c580c647d" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" -version = "0.52.1" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3480ac194b55ae274a7e135c21645656825da4a7f5b6e9286291b2113c94a78b" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" -version = "0.52.1" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42c46bab241c121402d1cb47d028ea3680ee2f359dcc287482dcf7fdddc73363" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.1" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc885a4332ee1afb9a1bacf11514801011725570d35675abc229ce7e3afe4d20" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" -version = "0.52.1" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e440c60457f84b0bee09208e62acc7ade264b38c4453f6312b8c9ab1613e73c" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "yansi" From 12b205e655f47b766e9c89ed17a7afc78566f2e4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 9 Aug 2024 03:18:54 +0000 Subject: [PATCH 115/194] Update Rust crate assert_cmd to v2.0.16 --- Cargo.lock | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0fda722..974fba9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -34,13 +34,14 @@ checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" [[package]] name = "assert_cmd" -version = "2.0.15" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc65048dd435533bb1baf2ed9956b9a278fbfdcf90301b39ee117f06c0199d37" +checksum = "dc1835b7f27878de8525dc71410b5a31cdcc5f230aed5ba5df968e09c201b23d" dependencies = [ "anstyle", "bstr", "doc-comment", + "libc", "predicates", "predicates-core", "predicates-tree", From dbabf399d5632a9a34463b8a10bedf5c6cb14a39 Mon Sep 17 00:00:00 2001 From: Olivier Tilloy Date: Sat, 6 Apr 2024 23:10:42 +0200 Subject: [PATCH 116/194] Use the instrumentation-based code coverage implementation --- .github/workflows/ci.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ddf2ea1..6f0e5ca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -110,6 +110,7 @@ jobs: - name: rust toolchain ~ install uses: dtolnay/rust-toolchain@nightly + - run: rustup component add llvm-tools-preview - name: install GNU patch on MacOS if: runner.os == 'macOS' run: brew install gpatch @@ -122,8 +123,9 @@ jobs: env: CARGO_INCREMENTAL: "0" RUSTC_WRAPPER: "" - RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort" + RUSTFLAGS: "-Cinstrument-coverage -Zcoverage-options=branch -Ccodegen-units=1 -Copt-level=0 -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort" RUSTDOCFLAGS: "-Cpanic=abort" + LLVM_PROFILE_FILE: "diffutils-%p-%m.profraw" - name: "`grcov` ~ install" id: build_grcov shell: bash @@ -151,9 +153,9 @@ jobs: COVERAGE_REPORT_FILE="${COVERAGE_REPORT_DIR}/lcov.info" mkdir -p "${COVERAGE_REPORT_DIR}" # display coverage files - grcov . --output-type files --ignore build.rs --ignore "vendor/*" --ignore "/*" --ignore "[a-zA-Z]:/*" --excl-br-line "^\s*((debug_)?assert(_eq|_ne)?!|#\[derive\()" | sort --unique + grcov . --output-type files --binary-path "${COVERAGE_REPORT_DIR}" | sort --unique # generate coverage report - grcov . --output-type lcov --output-path "${COVERAGE_REPORT_FILE}" --branch --ignore build.rs --ignore "vendor/*" --ignore "/*" --ignore "[a-zA-Z]:/*" --excl-br-line "^\s*((debug_)?assert(_eq|_ne)?!|#\[derive\()" + grcov . --output-type lcov --output-path "${COVERAGE_REPORT_FILE}" --binary-path "${COVERAGE_REPORT_DIR}" --branch echo "report=${COVERAGE_REPORT_FILE}" >> $GITHUB_OUTPUT - name: Upload coverage results (to Codecov.io) uses: codecov/codecov-action@v4 From 2a899a9fc70dec10a703e9001a2370319f3b82e2 Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Fri, 6 Sep 2024 09:27:53 +0200 Subject: [PATCH 117/194] Fix clippy warnings in tests from needless_borrows_for_generic_args lint --- src/context_diff.rs | 40 ++++++++++++++++++------------------ src/ed_diff.rs | 36 ++++++++++++++++---------------- src/normal_diff.rs | 48 +++++++++++++++++++++---------------------- src/unified_diff.rs | 50 ++++++++++++++++++++++----------------------- 4 files changed, 87 insertions(+), 87 deletions(-) diff --git a/src/context_diff.rs b/src/context_diff.rs index e276ce5..873fc3d 100644 --- a/src/context_diff.rs +++ b/src/context_diff.rs @@ -439,26 +439,26 @@ mod tests { ..Default::default() }, ); - File::create(&format!("{target}/ab.diff")) + File::create(format!("{target}/ab.diff")) .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create(&format!("{target}/alef")).unwrap(); + let mut fa = File::create(format!("{target}/alef")).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{target}/bet")).unwrap(); + let mut fb = File::create(format!("{target}/bet")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") .arg("--context") - .stdin(File::open(&format!("{target}/ab.diff")).unwrap()) + .stdin(File::open(format!("{target}/ab.diff")).unwrap()) .output() .unwrap(); assert!(output.status.success(), "{output:?}"); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read(&format!("{target}/alef")).unwrap(); + let alef = fs::read(format!("{target}/alef")).unwrap(); assert_eq!(alef, bet); } } @@ -520,26 +520,26 @@ mod tests { ..Default::default() }, ); - File::create(&format!("{target}/ab_.diff")) + File::create(format!("{target}/ab_.diff")) .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create(&format!("{target}/alef_")).unwrap(); + let mut fa = File::create(format!("{target}/alef_")).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{target}/bet_")).unwrap(); + let mut fb = File::create(format!("{target}/bet_")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") .arg("--context") - .stdin(File::open(&format!("{target}/ab_.diff")).unwrap()) + .stdin(File::open(format!("{target}/ab_.diff")).unwrap()) .output() .unwrap(); assert!(output.status.success(), "{output:?}"); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read(&format!("{target}/alef_")).unwrap(); + let alef = fs::read(format!("{target}/alef_")).unwrap(); assert_eq!(alef, bet); } } @@ -604,26 +604,26 @@ mod tests { ..Default::default() }, ); - File::create(&format!("{target}/abx.diff")) + File::create(format!("{target}/abx.diff")) .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create(&format!("{target}/alefx")).unwrap(); + let mut fa = File::create(format!("{target}/alefx")).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{target}/betx")).unwrap(); + let mut fb = File::create(format!("{target}/betx")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") .arg("--context") - .stdin(File::open(&format!("{target}/abx.diff")).unwrap()) + .stdin(File::open(format!("{target}/abx.diff")).unwrap()) .output() .unwrap(); assert!(output.status.success(), "{output:?}"); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read(&format!("{target}/alefx")).unwrap(); + let alef = fs::read(format!("{target}/alefx")).unwrap(); assert_eq!(alef, bet); } } @@ -691,26 +691,26 @@ mod tests { ..Default::default() }, ); - File::create(&format!("{target}/abr.diff")) + File::create(format!("{target}/abr.diff")) .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create(&format!("{target}/alefr")).unwrap(); + let mut fa = File::create(format!("{target}/alefr")).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{target}/betr")).unwrap(); + let mut fb = File::create(format!("{target}/betr")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") .arg("--context") - .stdin(File::open(&format!("{target}/abr.diff")).unwrap()) + .stdin(File::open(format!("{target}/abr.diff")).unwrap()) .output() .unwrap(); assert!(output.status.success(), "{output:?}"); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read(&format!("{target}/alefr")).unwrap(); + let alef = fs::read(format!("{target}/alefr")).unwrap(); assert_eq!(alef, bet); } } diff --git a/src/ed_diff.rs b/src/ed_diff.rs index 304a26f..b8cdbc5 100644 --- a/src/ed_diff.rs +++ b/src/ed_diff.rs @@ -225,13 +225,13 @@ mod tests { // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff_w(&alef, &bet, &format!("{target}/alef")).unwrap(); - File::create(&format!("{target}/ab.ed")) + File::create(format!("{target}/ab.ed")) .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create(&format!("{target}/alef")).unwrap(); + let mut fa = File::create(format!("{target}/alef")).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{target}/bet")).unwrap(); + let mut fb = File::create(format!("{target}/bet")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; @@ -239,14 +239,14 @@ mod tests { { use std::process::Command; let output = Command::new("ed") - .arg(&format!("{target}/alef")) - .stdin(File::open(&format!("{target}/ab.ed")).unwrap()) + .arg(format!("{target}/alef")) + .stdin(File::open(format!("{target}/ab.ed")).unwrap()) .output() .unwrap(); assert!(output.status.success(), "{output:?}"); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = std::fs::read(&format!("{target}/alef")).unwrap(); + let alef = std::fs::read(format!("{target}/alef")).unwrap(); assert_eq!(alef, bet); } } @@ -299,13 +299,13 @@ mod tests { // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff_w(&alef, &bet, &format!("{target}/alef_")).unwrap(); - File::create(&format!("{target}/ab_.ed")) + File::create(format!("{target}/ab_.ed")) .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create(&format!("{target}/alef_")).unwrap(); + let mut fa = File::create(format!("{target}/alef_")).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{target}/bet_")).unwrap(); + let mut fb = File::create(format!("{target}/bet_")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; @@ -313,14 +313,14 @@ mod tests { { use std::process::Command; let output = Command::new("ed") - .arg(&format!("{target}/alef_")) - .stdin(File::open(&format!("{target}/ab_.ed")).unwrap()) + .arg(format!("{target}/alef_")) + .stdin(File::open(format!("{target}/ab_.ed")).unwrap()) .output() .unwrap(); assert!(output.status.success(), "{output:?}"); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = std::fs::read(&format!("{target}/alef_")).unwrap(); + let alef = std::fs::read(format!("{target}/alef_")).unwrap(); assert_eq!(alef, bet); } } @@ -379,13 +379,13 @@ mod tests { // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff_w(&alef, &bet, &format!("{target}/alefr")).unwrap(); - File::create(&format!("{target}/abr.ed")) + File::create(format!("{target}/abr.ed")) .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create(&format!("{target}/alefr")).unwrap(); + let mut fa = File::create(format!("{target}/alefr")).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{target}/betr")).unwrap(); + let mut fb = File::create(format!("{target}/betr")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; @@ -393,14 +393,14 @@ mod tests { { use std::process::Command; let output = Command::new("ed") - .arg(&format!("{target}/alefr")) - .stdin(File::open(&format!("{target}/abr.ed")).unwrap()) + .arg(format!("{target}/alefr")) + .stdin(File::open(format!("{target}/abr.ed")).unwrap()) .output() .unwrap(); assert!(output.status.success(), "{output:?}"); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = std::fs::read(&format!("{target}/alefr")).unwrap(); + let alef = std::fs::read(format!("{target}/alefr")).unwrap(); assert_eq!(alef, bet); } } diff --git a/src/normal_diff.rs b/src/normal_diff.rs index d6f8297..002cd01 100644 --- a/src/normal_diff.rs +++ b/src/normal_diff.rs @@ -275,26 +275,26 @@ mod tests { // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff(&alef, &bet, &Params::default()); - File::create(&format!("{target}/ab.diff")) + File::create(format!("{target}/ab.diff")) .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create(&format!("{target}/alef")).unwrap(); + let mut fa = File::create(format!("{target}/alef")).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{target}/bet")).unwrap(); + let mut fb = File::create(format!("{target}/bet")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") - .arg(&format!("{target}/alef")) - .stdin(File::open(&format!("{target}/ab.diff")).unwrap()) + .arg(format!("{target}/alef")) + .stdin(File::open(format!("{target}/ab.diff")).unwrap()) .output() .unwrap(); assert!(output.status.success(), "{output:?}"); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read(&format!("{target}/alef")).unwrap(); + let alef = fs::read(format!("{target}/alef")).unwrap(); assert_eq!(alef, bet); } } @@ -367,27 +367,27 @@ mod tests { // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff(&alef, &bet, &Params::default()); - File::create(&format!("{target}/abn.diff")) + File::create(format!("{target}/abn.diff")) .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create(&format!("{target}/alefn")).unwrap(); + let mut fa = File::create(format!("{target}/alefn")).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{target}/betn")).unwrap(); + let mut fb = File::create(format!("{target}/betn")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") .arg("--normal") - .arg(&format!("{target}/alefn")) - .stdin(File::open(&format!("{target}/abn.diff")).unwrap()) + .arg(format!("{target}/alefn")) + .stdin(File::open(format!("{target}/abn.diff")).unwrap()) .output() .unwrap(); assert!(output.status.success(), "{output:?}"); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read(&format!("{target}/alefn")).unwrap(); + let alef = fs::read(format!("{target}/alefn")).unwrap(); assert_eq!(alef, bet); } } @@ -441,26 +441,26 @@ mod tests { // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff(&alef, &bet, &Params::default()); - File::create(&format!("{target}/ab_.diff")) + File::create(format!("{target}/ab_.diff")) .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create(&format!("{target}/alef_")).unwrap(); + let mut fa = File::create(format!("{target}/alef_")).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{target}/bet_")).unwrap(); + let mut fb = File::create(format!("{target}/bet_")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") - .arg(&format!("{target}/alef_")) - .stdin(File::open(&format!("{target}/ab_.diff")).unwrap()) + .arg(format!("{target}/alef_")) + .stdin(File::open(format!("{target}/ab_.diff")).unwrap()) .output() .unwrap(); assert!(output.status.success(), "{output:?}"); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read(&format!("{target}/alef_")).unwrap(); + let alef = fs::read(format!("{target}/alef_")).unwrap(); assert_eq!(alef, bet); } } @@ -519,26 +519,26 @@ mod tests { // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff(&alef, &bet, &Params::default()); - File::create(&format!("{target}/abr.diff")) + File::create(format!("{target}/abr.diff")) .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create(&format!("{target}/alefr")).unwrap(); + let mut fa = File::create(format!("{target}/alefr")).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{target}/betr")).unwrap(); + let mut fb = File::create(format!("{target}/betr")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") - .arg(&format!("{target}/alefr")) - .stdin(File::open(&format!("{target}/abr.diff")).unwrap()) + .arg(format!("{target}/alefr")) + .stdin(File::open(format!("{target}/abr.diff")).unwrap()) .output() .unwrap(); assert!(output.status.success(), "{output:?}"); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read(&format!("{target}/alefr")).unwrap(); + let alef = fs::read(format!("{target}/alefr")).unwrap(); assert_eq!(alef, bet); } } diff --git a/src/unified_diff.rs b/src/unified_diff.rs index 11299d7..0f504a8 100644 --- a/src/unified_diff.rs +++ b/src/unified_diff.rs @@ -466,13 +466,13 @@ mod tests { ..Default::default() }, ); - File::create(&format!("{target}/ab.diff")) + File::create(format!("{target}/ab.diff")) .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create(&format!("{target}/alef")).unwrap(); + let mut fa = File::create(format!("{target}/alef")).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{target}/bet")).unwrap(); + let mut fb = File::create(format!("{target}/bet")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; @@ -494,13 +494,13 @@ mod tests { let output = Command::new("patch") .arg("-p0") - .stdin(File::open(&format!("{target}/ab.diff")).unwrap()) + .stdin(File::open(format!("{target}/ab.diff")).unwrap()) .output() .unwrap(); println!("{}", String::from_utf8_lossy(&output.stdout)); println!("{}", String::from_utf8_lossy(&output.stderr)); assert!(output.status.success(), "{output:?}"); - let alef = fs::read(&format!("{target}/alef")).unwrap(); + let alef = fs::read(format!("{target}/alef")).unwrap(); assert_eq!(alef, bet); } } @@ -582,25 +582,25 @@ mod tests { ..Default::default() }, ); - File::create(&format!("{target}/abn.diff")) + File::create(format!("{target}/abn.diff")) .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create(&format!("{target}/alefn")).unwrap(); + let mut fa = File::create(format!("{target}/alefn")).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{target}/betn")).unwrap(); + let mut fb = File::create(format!("{target}/betn")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") - .stdin(File::open(&format!("{target}/abn.diff")).unwrap()) + .stdin(File::open(format!("{target}/abn.diff")).unwrap()) .output() .unwrap(); assert!(output.status.success(), "{output:?}"); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read(&format!("{target}/alefn")).unwrap(); + let alef = fs::read(format!("{target}/alefn")).unwrap(); assert_eq!(alef, bet); } } @@ -678,25 +678,25 @@ mod tests { ..Default::default() }, ); - File::create(&format!("{target}/ab_.diff")) + File::create(format!("{target}/ab_.diff")) .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create(&format!("{target}/alef_")).unwrap(); + let mut fa = File::create(format!("{target}/alef_")).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{target}/bet_")).unwrap(); + let mut fb = File::create(format!("{target}/bet_")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") - .stdin(File::open(&format!("{target}/ab_.diff")).unwrap()) + .stdin(File::open(format!("{target}/ab_.diff")).unwrap()) .output() .unwrap(); assert!(output.status.success(), "{output:?}"); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read(&format!("{target}/alef_")).unwrap(); + let alef = fs::read(format!("{target}/alef_")).unwrap(); assert_eq!(alef, bet); } } @@ -759,25 +759,25 @@ mod tests { ..Default::default() }, ); - File::create(&format!("{target}/abx.diff")) + File::create(format!("{target}/abx.diff")) .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create(&format!("{target}/alefx")).unwrap(); + let mut fa = File::create(format!("{target}/alefx")).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{target}/betx")).unwrap(); + let mut fb = File::create(format!("{target}/betx")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") - .stdin(File::open(&format!("{target}/abx.diff")).unwrap()) + .stdin(File::open(format!("{target}/abx.diff")).unwrap()) .output() .unwrap(); assert!(output.status.success(), "{output:?}"); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read(&format!("{target}/alefx")).unwrap(); + let alef = fs::read(format!("{target}/alefx")).unwrap(); assert_eq!(alef, bet); } } @@ -845,25 +845,25 @@ mod tests { ..Default::default() }, ); - File::create(&format!("{target}/abr.diff")) + File::create(format!("{target}/abr.diff")) .unwrap() .write_all(&diff) .unwrap(); - let mut fa = File::create(&format!("{target}/alefr")).unwrap(); + let mut fa = File::create(format!("{target}/alefr")).unwrap(); fa.write_all(&alef[..]).unwrap(); - let mut fb = File::create(&format!("{target}/betr")).unwrap(); + let mut fb = File::create(format!("{target}/betr")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") - .stdin(File::open(&format!("{target}/abr.diff")).unwrap()) + .stdin(File::open(format!("{target}/abr.diff")).unwrap()) .output() .unwrap(); assert!(output.status.success(), "{output:?}"); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); - let alef = fs::read(&format!("{target}/alefr")).unwrap(); + let alef = fs::read(format!("{target}/alefr")).unwrap(); assert_eq!(alef, bet); } } From 9db1eab1d03c8941db94913e53c087de031793fd Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Thu, 12 Sep 2024 10:10:47 +0200 Subject: [PATCH 118/194] Revert "cargo-dist: generate more targets" This reverts commit 80b993141b377c8651bbb71c8ceee93518855d45. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index fa80fc6..761e703 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,6 @@ ci = ["github"] # The installers to generate for each app installers = [] # Target platforms to build apps for (Rust target-triple syntax) -targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "aarch64-unknown-linux-musl", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl", "x86_64-pc-windows-msvc"] +targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"] # Publish jobs to run in CI pr-run-mode = "plan" From d5bce65a29fbf36e12c0ed36d65c2c7225f2ff76 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 16 Sep 2024 10:41:54 +0000 Subject: [PATCH 119/194] Update Rust crate pretty_assertions to v1.4.1 --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 974fba9..7fdb99d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -276,9 +276,9 @@ dependencies = [ [[package]] name = "pretty_assertions" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" dependencies = [ "diff", "yansi", @@ -602,6 +602,6 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "yansi" -version = "0.5.1" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" From 7c9c2a1ab280d0cde2de6f37837bbb8fd88a1f49 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 19 Sep 2024 20:09:56 +0000 Subject: [PATCH 120/194] Update Rust crate unicode-width to v0.1.14 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7fdb99d..f5589ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -411,9 +411,9 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-width" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "wait-timeout" From d8b91fd60eabc584ebdbad86d6ace6bbfea9355a Mon Sep 17 00:00:00 2001 From: Olivier Tilloy Date: Thu, 19 Sep 2024 22:33:33 +0200 Subject: [PATCH 121/194] Update unit test expectation --- src/utils.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index 561f2b9..df1390d 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -101,10 +101,11 @@ mod tests { // Note: The Woman Scientist emoji (👩‍🔬) is a ZWJ sequence combining // the Woman emoji (👩) and the Microscope emoji (🔬). On supported platforms - // it is displayed as a single emoji and should have a print size of 2 columns, - // but terminal emulators tend to not support this, and display the two emojis - // side by side, thus accounting for a print size of 4 columns. - assert_tab_expansion("foo\t👩‍🔬\tbaz", 6, "foo 👩‍🔬 baz"); + // it is displayed as a single emoji and has a print size of 2 columns. + // Terminal emulators tend to not support this, and display the two emojis + // side by side, thus accounting for a print size of 4 columns, but the + // unicode_width crate reports a correct size of 2. + assert_tab_expansion("foo\t👩‍🔬\tbaz", 6, "foo 👩‍🔬 baz"); } #[test] From 7574243de14cc33ba2222ebdafba5dcc3192ec36 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 20 Sep 2024 05:14:22 +0000 Subject: [PATCH 122/194] Update Rust crate unicode-width to 0.2.0 --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f5589ee..24fc712 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -411,9 +411,9 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-width" -version = "0.1.14" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" [[package]] name = "wait-timeout" diff --git a/Cargo.toml b/Cargo.toml index 761e703..477467c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ chrono = "0.4.38" diff = "0.1.13" regex = "1.10.4" same-file = "1.0.6" -unicode-width = "0.1.12" +unicode-width = "0.2.0" [dev-dependencies] pretty_assertions = "1.4.0" From c1b66e4a47ab398e441e9414dfb89ada5017114d Mon Sep 17 00:00:00 2001 From: Olivier Tilloy Date: Thu, 26 Sep 2024 22:44:56 +0200 Subject: [PATCH 123/194] When running the upstream test suite, fetch missing tests/init.sh (fixes #90) --- tests/run-upstream-testsuite.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/run-upstream-testsuite.sh b/tests/run-upstream-testsuite.sh index cb59834..cfc20a9 100755 --- a/tests/run-upstream-testsuite.sh +++ b/tests/run-upstream-testsuite.sh @@ -59,6 +59,10 @@ cd src ln -s "$binary" diff cd ../tests +# Fetch tests/init.sh from the gnulib repository (needed since +# https://git.savannah.gnu.org/cgit/diffutils.git/commit/tests?id=1d2456f539) +curl -s "$gitserver/gitweb/?p=gnulib.git;a=blob_plain;f=tests/init.sh;hb=HEAD" -o init.sh + if [[ -n "$TESTS" ]] then tests="$TESTS" From 72c7802f0694e44b19832db7f87d9da0c32db5c6 Mon Sep 17 00:00:00 2001 From: Gustavo Noronha Silva Date: Sun, 22 Sep 2024 22:48:36 -0300 Subject: [PATCH 124/194] Take utility name as first parameter on diffutils This is in preparation for adding the other diffutils commands, cmp, diff3, sdiff. We use a similar strategy to uutils/coreutils, with the single binary acting as one of the supported tools if called through a symlink with the appropriate name. When using the multi-tool binary directly, the utility needds to be the first parameter. --- src/diff.rs | 98 +++++++++++++++++++++++++++ src/main.rs | 154 ++++++++++++++++--------------------------- src/params.rs | 136 ++++++++++++++++++++++++++++++-------- src/utils.rs | 19 +++++- tests/integration.rs | 16 +++++ 5 files changed, 296 insertions(+), 127 deletions(-) create mode 100644 src/diff.rs diff --git a/src/diff.rs b/src/diff.rs new file mode 100644 index 0000000..6998e2b --- /dev/null +++ b/src/diff.rs @@ -0,0 +1,98 @@ +// This file is part of the uutils diffutils package. +// +// For the full copyright and license information, please view the LICENSE-* +// files that was distributed with this source code. + +use crate::params::{parse_params, Format}; +use crate::utils::report_failure_to_read_input_file; +use crate::{context_diff, ed_diff, normal_diff, unified_diff}; +use std::env::ArgsOs; +use std::ffi::OsString; +use std::fs; +use std::io::{self, Read, Write}; +use std::iter::Peekable; +use std::process::{exit, ExitCode}; + +// Exit codes are documented at +// https://www.gnu.org/software/diffutils/manual/html_node/Invoking-diff.html. +// An exit status of 0 means no differences were found, +// 1 means some differences were found, +// and 2 means trouble. +pub(crate) fn main(opts: Peekable) -> ExitCode { + let params = parse_params(opts).unwrap_or_else(|error| { + eprintln!("{error}"); + exit(2); + }); + // if from and to are the same file, no need to perform any comparison + let maybe_report_identical_files = || { + if params.report_identical_files { + println!( + "Files {} and {} are identical", + params.from.to_string_lossy(), + params.to.to_string_lossy(), + ); + } + }; + if params.from == "-" && params.to == "-" + || same_file::is_same_file(¶ms.from, ¶ms.to).unwrap_or(false) + { + maybe_report_identical_files(); + return ExitCode::SUCCESS; + } + + // read files + fn read_file_contents(filepath: &OsString) -> io::Result> { + if filepath == "-" { + let mut content = Vec::new(); + io::stdin().read_to_end(&mut content).and(Ok(content)) + } else { + fs::read(filepath) + } + } + let mut io_error = false; + let from_content = match read_file_contents(¶ms.from) { + Ok(from_content) => from_content, + Err(e) => { + report_failure_to_read_input_file(¶ms.executable, ¶ms.from, &e); + io_error = true; + vec![] + } + }; + let to_content = match read_file_contents(¶ms.to) { + Ok(to_content) => to_content, + Err(e) => { + report_failure_to_read_input_file(¶ms.executable, ¶ms.to, &e); + io_error = true; + vec![] + } + }; + if io_error { + return ExitCode::from(2); + } + + // run diff + let result: Vec = match params.format { + Format::Normal => normal_diff::diff(&from_content, &to_content, ¶ms), + Format::Unified => unified_diff::diff(&from_content, &to_content, ¶ms), + Format::Context => context_diff::diff(&from_content, &to_content, ¶ms), + Format::Ed => ed_diff::diff(&from_content, &to_content, ¶ms).unwrap_or_else(|error| { + eprintln!("{error}"); + exit(2); + }), + }; + if params.brief && !result.is_empty() { + println!( + "Files {} and {} differ", + params.from.to_string_lossy(), + params.to.to_string_lossy() + ); + } else { + io::stdout().write_all(&result).unwrap(); + } + if result.is_empty() { + maybe_report_identical_files(); + ExitCode::SUCCESS + } else { + ExitCode::from(1) + } +} diff --git a/src/main.rs b/src/main.rs index 7e221ea..824b45c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,15 +3,16 @@ // For the full copyright and license information, please view the LICENSE-* // files that was distributed with this source code. -use crate::params::{parse_params, Format}; -use regex::Regex; -use std::env; -use std::ffi::OsString; -use std::fs; -use std::io::{self, Read, Write}; -use std::process::{exit, ExitCode}; +use std::{ + env::ArgsOs, + ffi::OsString, + iter::Peekable, + path::{Path, PathBuf}, + process::ExitCode, +}; mod context_diff; +mod diff; mod ed_diff; mod macros; mod normal_diff; @@ -19,103 +20,60 @@ mod params; mod unified_diff; mod utils; -fn report_failure_to_read_input_file( - executable: &OsString, - filepath: &OsString, - error: &std::io::Error, -) { - // std::io::Error's display trait outputs "{detail} (os error {code})" - // but we want only the {detail} (error string) part - let error_code_re = Regex::new(r"\ \(os\ error\ \d+\)$").unwrap(); - eprintln!( - "{}: {}: {}", - executable.to_string_lossy(), - filepath.to_string_lossy(), - error_code_re.replace(error.to_string().as_str(), ""), - ); +/// # Panics +/// Panics if the binary path cannot be determined +fn binary_path(args: &mut Peekable) -> PathBuf { + match args.peek() { + Some(ref s) if !s.is_empty() => PathBuf::from(s), + _ => std::env::current_exe().unwrap(), + } +} + +fn name(binary_path: &Path) -> Option<&str> { + binary_path.file_stem()?.to_str() +} + +const VERSION: &str = env!("CARGO_PKG_VERSION"); + +fn usage(name: &str) { + println!("{name} {VERSION} (multi-call binary)\n"); + println!("Usage: {name} [function [arguments...]]\n"); + println!("Currently defined functions:\n"); + println!(" diff\n"); +} + +fn second_arg_error(name: &str) -> ! { + println!("Expected utility name as second argument, got nothing."); + usage(name); + std::process::exit(0); } -// Exit codes are documented at -// https://www.gnu.org/software/diffutils/manual/html_node/Invoking-diff.html. -// An exit status of 0 means no differences were found, -// 1 means some differences were found, -// and 2 means trouble. fn main() -> ExitCode { - let opts = env::args_os(); - let params = parse_params(opts).unwrap_or_else(|error| { - eprintln!("{error}"); - exit(2); + let mut args = std::env::args_os().peekable(); + + let exe_path = binary_path(&mut args); + let exe_name = name(&exe_path).unwrap_or_else(|| { + usage(""); + std::process::exit(1); }); - // if from and to are the same file, no need to perform any comparison - let maybe_report_identical_files = || { - if params.report_identical_files { - println!( - "Files {} and {} are identical", - params.from.to_string_lossy(), - params.to.to_string_lossy(), - ); - } - }; - if params.from == "-" && params.to == "-" - || same_file::is_same_file(¶ms.from, ¶ms.to).unwrap_or(false) - { - maybe_report_identical_files(); - return ExitCode::SUCCESS; - } - // read files - fn read_file_contents(filepath: &OsString) -> io::Result> { - if filepath == "-" { - let mut content = Vec::new(); - io::stdin().read_to_end(&mut content).and(Ok(content)) - } else { - fs::read(filepath) - } - } - let mut io_error = false; - let from_content = match read_file_contents(¶ms.from) { - Ok(from_content) => from_content, - Err(e) => { - report_failure_to_read_input_file(¶ms.executable, ¶ms.from, &e); - io_error = true; - vec![] - } - }; - let to_content = match read_file_contents(¶ms.to) { - Ok(to_content) => to_content, - Err(e) => { - report_failure_to_read_input_file(¶ms.executable, ¶ms.to, &e); - io_error = true; - vec![] - } - }; - if io_error { - return ExitCode::from(2); - } + let util_name = if exe_name == "diffutils" { + // Discard the item we peeked. + let _ = args.next(); - // run diff - let result: Vec = match params.format { - Format::Normal => normal_diff::diff(&from_content, &to_content, ¶ms), - Format::Unified => unified_diff::diff(&from_content, &to_content, ¶ms), - Format::Context => context_diff::diff(&from_content, &to_content, ¶ms), - Format::Ed => ed_diff::diff(&from_content, &to_content, ¶ms).unwrap_or_else(|error| { - eprintln!("{error}"); - exit(2); - }), - }; - if params.brief && !result.is_empty() { - println!( - "Files {} and {} differ", - params.from.to_string_lossy(), - params.to.to_string_lossy() - ); - } else { - io::stdout().write_all(&result).unwrap(); - } - if result.is_empty() { - maybe_report_identical_files(); - ExitCode::SUCCESS + args.peek() + .cloned() + .unwrap_or_else(|| second_arg_error(exe_name)) } else { - ExitCode::from(1) + OsString::from(exe_name) + }; + + match util_name.to_str() { + Some("diff") => diff::main(args), + Some(name) => { + usage(&format!("{}: utility not supported", name)); + ExitCode::from(1) + } + None => second_arg_error(exe_name), } } diff --git a/src/params.rs b/src/params.rs index c671180..9b3abc4 100644 --- a/src/params.rs +++ b/src/params.rs @@ -1,4 +1,5 @@ use std::ffi::OsString; +use std::iter::Peekable; use std::path::PathBuf; use regex::Regex; @@ -41,8 +42,7 @@ impl Default for Params { } } -pub fn parse_params>(opts: I) -> Result { - let mut opts = opts.into_iter().peekable(); +pub fn parse_params>(mut opts: Peekable) -> Result { // parse CLI let Some(executable) = opts.next() else { @@ -323,7 +323,12 @@ mod tests { to: os("bar"), ..Default::default() }), - parse_params([os("diff"), os("foo"), os("bar")].iter().cloned()) + parse_params( + [os("diff"), os("foo"), os("bar")] + .iter() + .cloned() + .peekable() + ) ); assert_eq!( Ok(Params { @@ -336,6 +341,7 @@ mod tests { [os("diff"), os("--normal"), os("foo"), os("bar")] .iter() .cloned() + .peekable() ) ); } @@ -350,7 +356,12 @@ mod tests { format: Format::Ed, ..Default::default() }), - parse_params([os("diff"), os(arg), os("foo"), os("bar")].iter().cloned()) + parse_params( + [os("diff"), os(arg), os("foo"), os("bar")] + .iter() + .cloned() + .peekable() + ) ); } } @@ -368,7 +379,7 @@ mod tests { format: Format::Context, ..Default::default() }), - parse_params(params.iter().map(|x| os(x))) + parse_params(params.iter().map(|x| os(x)).peekable()) ); } for args in [ @@ -390,7 +401,7 @@ mod tests { context_count: 42, ..Default::default() }), - parse_params(params.iter().map(|x| os(x))) + parse_params(params.iter().map(|x| os(x)).peekable()) ); } } @@ -410,7 +421,7 @@ mod tests { let mut params = vec!["diff"]; params.extend(args); params.extend(["foo", "bar"]); - assert!(parse_params(params.iter().map(|x| os(x))).is_err()); + assert!(parse_params(params.iter().map(|x| os(x)).peekable()).is_err()); } } #[test] @@ -427,7 +438,7 @@ mod tests { format: Format::Unified, ..Default::default() }), - parse_params(params.iter().map(|x| os(x))) + parse_params(params.iter().map(|x| os(x)).peekable()) ); } for args in [ @@ -449,7 +460,7 @@ mod tests { context_count: 42, ..Default::default() }), - parse_params(params.iter().map(|x| os(x))) + parse_params(params.iter().map(|x| os(x)).peekable()) ); } } @@ -469,7 +480,7 @@ mod tests { let mut params = vec!["diff"]; params.extend(args); params.extend(["foo", "bar"]); - assert!(parse_params(params.iter().map(|x| os(x))).is_err()); + assert!(parse_params(params.iter().map(|x| os(x)).peekable()).is_err()); } } #[test] @@ -487,6 +498,7 @@ mod tests { [os("diff"), os("-u54"), os("foo"), os("bar")] .iter() .cloned() + .peekable() ) ); assert_eq!( @@ -502,6 +514,7 @@ mod tests { [os("diff"), os("-U54"), os("foo"), os("bar")] .iter() .cloned() + .peekable() ) ); assert_eq!( @@ -517,6 +530,7 @@ mod tests { [os("diff"), os("-U"), os("54"), os("foo"), os("bar")] .iter() .cloned() + .peekable() ) ); assert_eq!( @@ -532,6 +546,7 @@ mod tests { [os("diff"), os("-c54"), os("foo"), os("bar")] .iter() .cloned() + .peekable() ) ); } @@ -544,7 +559,12 @@ mod tests { to: os("bar"), ..Default::default() }), - parse_params([os("diff"), os("foo"), os("bar")].iter().cloned()) + parse_params( + [os("diff"), os("foo"), os("bar")] + .iter() + .cloned() + .peekable() + ) ); assert_eq!( Ok(Params { @@ -554,7 +574,12 @@ mod tests { report_identical_files: true, ..Default::default() }), - parse_params([os("diff"), os("-s"), os("foo"), os("bar")].iter().cloned()) + parse_params( + [os("diff"), os("-s"), os("foo"), os("bar")] + .iter() + .cloned() + .peekable() + ) ); assert_eq!( Ok(Params { @@ -573,6 +598,7 @@ mod tests { ] .iter() .cloned() + .peekable() ) ); } @@ -585,7 +611,12 @@ mod tests { to: os("bar"), ..Default::default() }), - parse_params([os("diff"), os("foo"), os("bar")].iter().cloned()) + parse_params( + [os("diff"), os("foo"), os("bar")] + .iter() + .cloned() + .peekable() + ) ); assert_eq!( Ok(Params { @@ -595,7 +626,12 @@ mod tests { brief: true, ..Default::default() }), - parse_params([os("diff"), os("-q"), os("foo"), os("bar")].iter().cloned()) + parse_params( + [os("diff"), os("-q"), os("foo"), os("bar")] + .iter() + .cloned() + .peekable() + ) ); assert_eq!( Ok(Params { @@ -609,6 +645,7 @@ mod tests { [os("diff"), os("--brief"), os("foo"), os("bar"),] .iter() .cloned() + .peekable() ) ); } @@ -621,7 +658,12 @@ mod tests { to: os("bar"), ..Default::default() }), - parse_params([os("diff"), os("foo"), os("bar")].iter().cloned()) + parse_params( + [os("diff"), os("foo"), os("bar")] + .iter() + .cloned() + .peekable() + ) ); for option in ["-t", "--expand-tabs"] { assert_eq!( @@ -636,6 +678,7 @@ mod tests { [os("diff"), os(option), os("foo"), os("bar")] .iter() .cloned() + .peekable() ) ); } @@ -649,7 +692,12 @@ mod tests { to: os("bar"), ..Default::default() }), - parse_params([os("diff"), os("foo"), os("bar")].iter().cloned()) + parse_params( + [os("diff"), os("foo"), os("bar")] + .iter() + .cloned() + .peekable() + ) ); assert_eq!( Ok(Params { @@ -663,6 +711,7 @@ mod tests { [os("diff"), os("--tabsize=0"), os("foo"), os("bar")] .iter() .cloned() + .peekable() ) ); assert_eq!( @@ -677,36 +726,42 @@ mod tests { [os("diff"), os("--tabsize=42"), os("foo"), os("bar")] .iter() .cloned() + .peekable() ) ); assert!(parse_params( [os("diff"), os("--tabsize"), os("foo"), os("bar")] .iter() .cloned() + .peekable() ) .is_err()); assert!(parse_params( [os("diff"), os("--tabsize="), os("foo"), os("bar")] .iter() .cloned() + .peekable() ) .is_err()); assert!(parse_params( [os("diff"), os("--tabsize=r2"), os("foo"), os("bar")] .iter() .cloned() + .peekable() ) .is_err()); assert!(parse_params( [os("diff"), os("--tabsize=-1"), os("foo"), os("bar")] .iter() .cloned() + .peekable() ) .is_err()); assert!(parse_params( [os("diff"), os("--tabsize=r2"), os("foo"), os("bar")] .iter() .cloned() + .peekable() ) .is_err()); assert!(parse_params( @@ -718,6 +773,7 @@ mod tests { ] .iter() .cloned() + .peekable() ) .is_err()); } @@ -730,7 +786,12 @@ mod tests { to: os("-h"), ..Default::default() }), - parse_params([os("diff"), os("--"), os("-g"), os("-h")].iter().cloned()) + parse_params( + [os("diff"), os("--"), os("-g"), os("-h")] + .iter() + .cloned() + .peekable() + ) ); } #[test] @@ -742,7 +803,7 @@ mod tests { to: os("-"), ..Default::default() }), - parse_params([os("diff"), os("foo"), os("-")].iter().cloned()) + parse_params([os("diff"), os("foo"), os("-")].iter().cloned().peekable()) ); assert_eq!( Ok(Params { @@ -751,7 +812,7 @@ mod tests { to: os("bar"), ..Default::default() }), - parse_params([os("diff"), os("-"), os("bar")].iter().cloned()) + parse_params([os("diff"), os("-"), os("bar")].iter().cloned().peekable()) ); assert_eq!( Ok(Params { @@ -760,27 +821,45 @@ mod tests { to: os("-"), ..Default::default() }), - parse_params([os("diff"), os("-"), os("-")].iter().cloned()) + parse_params([os("diff"), os("-"), os("-")].iter().cloned().peekable()) ); - assert!(parse_params([os("diff"), os("foo"), os("bar"), os("-")].iter().cloned()).is_err()); - assert!(parse_params([os("diff"), os("-"), os("-"), os("-")].iter().cloned()).is_err()); + assert!(parse_params( + [os("diff"), os("foo"), os("bar"), os("-")] + .iter() + .cloned() + .peekable() + ) + .is_err()); + assert!(parse_params( + [os("diff"), os("-"), os("-"), os("-")] + .iter() + .cloned() + .peekable() + ) + .is_err()); } #[test] fn missing_arguments() { - assert!(parse_params([os("diff")].iter().cloned()).is_err()); - assert!(parse_params([os("diff"), os("foo")].iter().cloned()).is_err()); + assert!(parse_params([os("diff")].iter().cloned().peekable()).is_err()); + assert!(parse_params([os("diff"), os("foo")].iter().cloned().peekable()).is_err()); } #[test] fn unknown_argument() { + assert!(parse_params( + [os("diff"), os("-g"), os("foo"), os("bar")] + .iter() + .cloned() + .peekable() + ) + .is_err()); assert!( - parse_params([os("diff"), os("-g"), os("foo"), os("bar")].iter().cloned()).is_err() + parse_params([os("diff"), os("-g"), os("bar")].iter().cloned().peekable()).is_err() ); - assert!(parse_params([os("diff"), os("-g"), os("bar")].iter().cloned()).is_err()); - assert!(parse_params([os("diff"), os("-g")].iter().cloned()).is_err()); + assert!(parse_params([os("diff"), os("-g")].iter().cloned().peekable()).is_err()); } #[test] fn empty() { - assert!(parse_params([].iter().cloned()).is_err()); + assert!(parse_params([].iter().cloned().peekable()).is_err()); } #[test] fn conflicting_output_styles() { @@ -797,6 +876,7 @@ mod tests { [os("diff"), os(arg1), os(arg2), os("foo"), os("bar")] .iter() .cloned() + .peekable() ) .is_err()); } diff --git a/src/utils.rs b/src/utils.rs index df1390d..a216784 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -3,8 +3,9 @@ // For the full copyright and license information, please view the LICENSE-* // files that was distributed with this source code. -use std::io::Write; +use std::{ffi::OsString, io::Write}; +use regex::Regex; use unicode_width::UnicodeWidthStr; /// Replace tabs by spaces in the input line. @@ -71,6 +72,22 @@ pub fn get_modification_time(file_path: &str) -> String { modification_time } +pub fn report_failure_to_read_input_file( + executable: &OsString, + filepath: &OsString, + error: &std::io::Error, +) { + // std::io::Error's display trait outputs "{detail} (os error {code})" + // but we want only the {detail} (error string) part + let error_code_re = Regex::new(r"\ \(os\ error\ \d+\)$").unwrap(); + eprintln!( + "{}: {}: {}", + executable.to_string_lossy(), + filepath.to_string_lossy(), + error_code_re.replace(error.to_string().as_str(), ""), + ); +} + #[cfg(test)] mod tests { use super::*; diff --git a/tests/integration.rs b/tests/integration.rs index f8ad515..2b3fd4f 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -15,6 +15,7 @@ use tempfile::{tempdir, NamedTempFile}; #[test] fn unknown_param() -> Result<(), Box> { let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("diff"); cmd.arg("--foobar"); cmd.assert() .code(predicate::eq(2)) @@ -37,6 +38,7 @@ fn cannot_read_files() -> Result<(), Box> { let error_message = "The system cannot find the file specified."; let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("diff"); cmd.arg(&nopath).arg(file.path()); cmd.assert() .code(predicate::eq(2)) @@ -47,6 +49,7 @@ fn cannot_read_files() -> Result<(), Box> { ))); let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("diff"); cmd.arg(file.path()).arg(&nopath); cmd.assert() .code(predicate::eq(2)) @@ -57,6 +60,7 @@ fn cannot_read_files() -> Result<(), Box> { ))); let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("diff"); cmd.arg(&nopath).arg(&nopath); cmd.assert().code(predicate::eq(2)).failure().stderr( predicate::str::contains(format!( @@ -74,6 +78,7 @@ fn no_differences() -> Result<(), Box> { let file = NamedTempFile::new()?; for option in ["", "-u", "-c", "-e"] { let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("diff"); if !option.is_empty() { cmd.arg(option); } @@ -93,6 +98,7 @@ fn no_differences_report_identical_files() -> Result<(), Box Result<(), Box Result<(), Box> { file2.write_all("bar\n".as_bytes())?; for option in ["", "-u", "-c", "-e"] { let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("diff"); if !option.is_empty() { cmd.arg(option); } @@ -155,6 +163,7 @@ fn differences_brief() -> Result<(), Box> { file2.write_all("bar\n".as_bytes())?; for option in ["", "-u", "-c", "-e"] { let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("diff"); if !option.is_empty() { cmd.arg(option); } @@ -178,6 +187,7 @@ fn missing_newline() -> Result<(), Box> { let mut file2 = NamedTempFile::new()?; file2.write_all("bar".as_bytes())?; let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("diff"); cmd.arg("-e").arg(file1.path()).arg(file2.path()); cmd.assert() .code(predicate::eq(2)) @@ -194,6 +204,7 @@ fn read_from_stdin() -> Result<(), Box> { file2.write_all("bar\n".as_bytes())?; let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("diff"); cmd.arg("-u") .arg(file1.path()) .arg("-") @@ -210,6 +221,7 @@ fn read_from_stdin() -> Result<(), Box> { ); let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("diff"); cmd.arg("-u") .arg("-") .arg(file2.path()) @@ -226,6 +238,7 @@ fn read_from_stdin() -> Result<(), Box> { ); let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("diff"); cmd.arg("-u").arg("-").arg("-"); cmd.assert() .code(predicate::eq(0)) @@ -235,6 +248,7 @@ fn read_from_stdin() -> Result<(), Box> { #[cfg(unix)] { let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("diff"); cmd.arg("-u") .arg(file1.path()) .arg("/dev/stdin") @@ -270,6 +284,7 @@ fn compare_file_to_directory() -> Result<(), Box> { da.write_all(b"da\n").unwrap(); let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("diff"); cmd.arg("-u").arg(&directory).arg(&a_path); cmd.assert().code(predicate::eq(1)).failure(); @@ -284,6 +299,7 @@ fn compare_file_to_directory() -> Result<(), Box> { ); let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("diff"); cmd.arg("-u").arg(&a_path).arg(&directory); cmd.assert().code(predicate::eq(1)).failure(); From f75c1879711f8ec05922f5be00be7447e1f735af Mon Sep 17 00:00:00 2001 From: Olivier Tilloy Date: Fri, 27 Sep 2024 19:45:34 +0200 Subject: [PATCH 125/194] Upstream test suite: correctly handle tests that are skipped (fixes #92) --- tests/run-upstream-testsuite.sh | 52 +++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/tests/run-upstream-testsuite.sh b/tests/run-upstream-testsuite.sh index cfc20a9..44c56f3 100755 --- a/tests/run-upstream-testsuite.sh +++ b/tests/run-upstream-testsuite.sh @@ -75,7 +75,6 @@ total=$(echo "$tests" | wc -w) echo "Running $total tests" export LC_ALL=C export KEEP=yes -exitcode=0 timestamp=$(date -Iseconds) urlroot="$gitserver/cgit/diffutils.git/tree/tests/" passed=0 @@ -90,31 +89,39 @@ do # because other binaries aren't implemented yet if ! grep -E -s -q "(cmp|diff3|sdiff)" "$test" then - sh "$test" 1> stdout.txt 2> stderr.txt && result="PASS" || exitcode=1 - json+="{\"test\":\"$test\",\"result\":\"$result\"," - json+="\"url\":\"$url\"," - json+="\"stdout\":\"$(base64 -w0 < stdout.txt)\"," - json+="\"stderr\":\"$(base64 -w0 < stderr.txt)\"," - json+="\"files\":{" - cd gt-$test.* - # Note: this doesn't include the contents of subdirectories, - # but there isn't much value added in doing so - for file in * - do - [[ -f "$file" ]] && json+="\"$file\":\"$(base64 -w0 < "$file")\"," - done - json="${json%,}}}," - cd - > /dev/null - [[ "$result" = "PASS" ]] && (( passed++ )) - [[ "$result" = "FAIL" ]] && (( failed++ )) + sh "$test" 1> stdout.txt 2> stderr.txt && result="PASS" + if [[ $? = 77 ]] + then + result="SKIP" + else + json+="{\"test\":\"$test\",\"result\":\"$result\"," + json+="\"url\":\"$url\"," + json+="\"stdout\":\"$(base64 -w0 < stdout.txt)\"," + json+="\"stderr\":\"$(base64 -w0 < stderr.txt)\"," + json+="\"files\":{" + cd gt-$test.* + # Note: this doesn't include the contents of subdirectories, + # but there isn't much value added in doing so + for file in * + do + [[ -f "$file" ]] && json+="\"$file\":\"$(base64 -w0 < "$file")\"," + done + json="${json%,}}}," + cd - > /dev/null + [[ "$result" = "PASS" ]] && (( passed++ )) + [[ "$result" = "FAIL" ]] && (( failed++ )) + fi else result="SKIP" - (( skipped++ )) - json+="{\"test\":\"$test\",\"url\":\"$url\",\"result\":\"$result\"}," fi color=2 # green [[ "$result" = "FAIL" ]] && color=1 # red - [[ "$result" = "SKIP" ]] && color=3 # yellow + if [[ $result = "SKIP" ]] + then + (( skipped++ )) + json+="{\"test\":\"$test\",\"url\":\"$url\",\"result\":\"$result\"}," + color=3 # yellow + fi printf " %-40s $(tput setaf $color)$result$(tput sgr0)\n" "$test" done echo "" @@ -142,4 +149,5 @@ resultsfile="test-results.json" echo "$json" | jq > "$resultsfile" echo "Results written to $scriptpath/$resultsfile" -exit $exitcode +(( failed > 0 )) && exit 1 +exit 0 From bfdbf6d7b227207e9fc0ff2db16b1f8a0112b3f7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 28 Sep 2024 19:37:34 +0000 Subject: [PATCH 126/194] Update Rust crate tempfile to v3.13.0 --- Cargo.lock | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 24fc712..5afcb6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -154,9 +154,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" [[package]] name = "float-cmp" @@ -201,15 +201,15 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.153" +version = "0.2.159" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" [[package]] name = "linux-raw-sys" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "log" @@ -333,9 +333,9 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "rustix" -version = "0.38.31" +version = "0.38.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" dependencies = [ "bitflags", "errno", @@ -386,9 +386,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.12.0" +version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" dependencies = [ "cfg-if", "fastrand", From 26bcc102c0c71b747e1102b7de7fef1adae5ed19 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 29 Sep 2024 15:37:34 +0000 Subject: [PATCH 127/194] Update Rust crate regex to v1.11.0 --- Cargo.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5afcb6d..3330d21 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -304,9 +304,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.6" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" dependencies = [ "aho-corasick", "memchr", @@ -316,9 +316,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.5" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" dependencies = [ "aho-corasick", "memchr", @@ -327,9 +327,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rustix" From 50057412bdf43159abb2cd0c412969f5a505be1b Mon Sep 17 00:00:00 2001 From: Gustavo Noronha Silva Date: Mon, 23 Sep 2024 16:27:40 -0300 Subject: [PATCH 128/194] Add cmp utility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The utility should support all the arguments supported by GNU cmp and perform slightly better. On a "bad" scenario, ~36M files which are completely different, our version runs in ~72% of the time of the original on my M1 Max: > hyperfine --warmup 1 -i --output=pipe \ 'cmp -l huge huge.3' Benchmark 1: cmp -l huge huge.3 Time (mean ± σ): 3.237 s ± 0.014 s [User: 2.891 s, System: 0.341 s] Range (min … max): 3.221 s … 3.271 s 10 runs Warning: Ignoring non-zero exit code. > hyperfine --warmup 1 -i --output=pipe \ '../target/release/diffutils cmp -l huge huge.3' Benchmark 1: ../target/release/diffutils cmp -l huge huge.3 Time (mean ± σ): 2.392 s ± 0.009 s [User: 1.978 s, System: 0.406 s] Range (min … max): 2.378 s … 2.406 s 10 runs Warning: Ignoring non-zero exit code. Our cmp runs in ~116% of the time when comparing libxul.so to the chromium-browser binary with -l and -b. In a best case scenario of comparing 2 files which are the same except for the last byte, our tool is slightly faster. --- .github/workflows/fuzzing.yml | 2 + fuzz/Cargo.toml | 12 + fuzz/dictionaries/cmp.txt | 36 + fuzz/fuzz_targets/fuzz_cmp.rs | 51 ++ fuzz/fuzz_targets/fuzz_cmp_args.rs | 23 + src/cmp.rs | 1115 ++++++++++++++++++++++++++++ src/diff.rs | 2 +- src/lib.rs | 1 + src/main.rs | 27 +- src/utils.rs | 17 +- tests/integration.rs | 1077 ++++++++++++++++++++------- tests/run-upstream-testsuite.sh | 7 +- 12 files changed, 2089 insertions(+), 281 deletions(-) create mode 100644 fuzz/dictionaries/cmp.txt create mode 100644 fuzz/fuzz_targets/fuzz_cmp.rs create mode 100644 fuzz/fuzz_targets/fuzz_cmp_args.rs create mode 100644 src/cmp.rs diff --git a/.github/workflows/fuzzing.yml b/.github/workflows/fuzzing.yml index 589b952..9ad1c17 100644 --- a/.github/workflows/fuzzing.yml +++ b/.github/workflows/fuzzing.yml @@ -41,6 +41,8 @@ jobs: strategy: matrix: test-target: + - { name: fuzz_cmp, should_pass: true } + - { name: fuzz_cmp_args, should_pass: true } - { name: fuzz_ed, should_pass: true } - { name: fuzz_normal, should_pass: true } - { name: fuzz_patch, should_pass: true } diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 5debf47..8b0b521 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -16,6 +16,18 @@ diffutils = { path = "../" } [workspace] members = ["."] +[[bin]] +name = "fuzz_cmp" +path = "fuzz_targets/fuzz_cmp.rs" +test = false +doc = false + +[[bin]] +name = "fuzz_cmp_args" +path = "fuzz_targets/fuzz_cmp_args.rs" +test = false +doc = false + [[bin]] name = "fuzz_patch" path = "fuzz_targets/fuzz_patch.rs" diff --git a/fuzz/dictionaries/cmp.txt b/fuzz/dictionaries/cmp.txt new file mode 100644 index 0000000..0365fef --- /dev/null +++ b/fuzz/dictionaries/cmp.txt @@ -0,0 +1,36 @@ +"-l" +"--verbose" +"-b" +"--print-bytes" +"-lb" +"-bl" +"-n" +"--bytes" +"--bytes=" +"--bytes=1024" +"--bytes=99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999" +"-i" +"--ignore-initial" +"--ignore-initial=" +"--ignore-initial=1024" +"--ignore-initial=99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999:9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999" +"-s" +"-q" +"--quiet" +"--silent" +"-" +"--" +"1kB" +"1G" +"1GB" +"1T" +"1TB" +"1P" +"1PB" +"1Z" +"1ZB" +"1Y" +"1YB" +"1Y" +"0" +"1:2" diff --git a/fuzz/fuzz_targets/fuzz_cmp.rs b/fuzz/fuzz_targets/fuzz_cmp.rs new file mode 100644 index 0000000..e9d0e4c --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_cmp.rs @@ -0,0 +1,51 @@ +#![no_main] +#[macro_use] +extern crate libfuzzer_sys; +use diffutilslib::cmp::{self, Cmp}; + +use std::ffi::OsString; +use std::fs::File; +use std::io::Write; + +fn os(s: &str) -> OsString { + OsString::from(s) +} + +fuzz_target!(|x: (Vec, Vec)| { + let args = vec!["cmp", "-l", "-b", "target/fuzz.cmp.a", "target/fuzz.cmp.b"] + .into_iter() + .map(|s| os(s)) + .peekable(); + + let (from, to) = x; + + File::create("target/fuzz.cmp.a") + .unwrap() + .write_all(&from) + .unwrap(); + + File::create("target/fuzz.cmp.b") + .unwrap() + .write_all(&to) + .unwrap(); + + let params = + cmp::parse_params(args).unwrap_or_else(|e| panic!("Failed to parse params: {}", e)); + let ret = cmp::cmp(¶ms); + if from == to && !matches!(ret, Ok(Cmp::Equal)) { + panic!( + "target/fuzz.cmp.a and target/fuzz.cmp.b are equal, but cmp returned {:?}.", + ret + ); + } else if from != to && !matches!(ret, Ok(Cmp::Different)) { + panic!( + "target/fuzz.cmp.a and target/fuzz.cmp.b are different, but cmp returned {:?}.", + ret + ); + } else if ret.is_err() { + panic!( + "target/fuzz.cmp.a and target/fuzz.cmp.b caused cmp to error ({:?}).", + ret + ); + } +}); diff --git a/fuzz/fuzz_targets/fuzz_cmp_args.rs b/fuzz/fuzz_targets/fuzz_cmp_args.rs new file mode 100644 index 0000000..579cf34 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_cmp_args.rs @@ -0,0 +1,23 @@ +#![no_main] +#[macro_use] +extern crate libfuzzer_sys; +use diffutilslib::cmp; + +use libfuzzer_sys::Corpus; +use std::ffi::OsString; + +fn os(s: &str) -> OsString { + OsString::from(s) +} + +fuzz_target!(|x: Vec| -> Corpus { + if x.len() > 6 { + // Make sure we try to parse an option when we get longer args. x[0] will be + // the executable name. + if ![os("-l"), os("-b"), os("-s"), os("-n"), os("-i")].contains(&x[1]) { + return Corpus::Reject; + } + } + let _ = cmp::parse_params(x.into_iter().peekable()); + Corpus::Keep +}); diff --git a/src/cmp.rs b/src/cmp.rs new file mode 100644 index 0000000..29b8775 --- /dev/null +++ b/src/cmp.rs @@ -0,0 +1,1115 @@ +// This file is part of the uutils diffutils package. +// +// For the full copyright and license information, please view the LICENSE-* +// files that was distributed with this source code. + +use crate::utils::format_failure_to_read_input_file; +use std::env::{self, ArgsOs}; +use std::ffi::OsString; +use std::io::{BufRead, BufReader, BufWriter, Read, Write}; +use std::iter::Peekable; +use std::process::ExitCode; +use std::{fs, io}; + +#[cfg(not(target_os = "windows"))] +use std::os::fd::{AsRawFd, FromRawFd}; + +#[cfg(not(target_os = "windows"))] +use std::os::unix::fs::MetadataExt; + +#[cfg(target_os = "windows")] +use std::os::windows::fs::MetadataExt; + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct Params { + executable: OsString, + from: OsString, + to: OsString, + print_bytes: bool, + skip_a: Option, + skip_b: Option, + max_bytes: Option, + verbose: bool, + quiet: bool, +} + +#[inline] +fn usage_string(executable: &str) -> String { + format!("Usage: {} ", executable) +} + +#[cfg(not(target_os = "windows"))] +fn is_stdout_dev_null() -> bool { + let Ok(dev_null) = fs::metadata("/dev/null") else { + return false; + }; + + let stdout_fd = io::stdout().lock().as_raw_fd(); + + // SAFETY: we have exclusive access to stdout right now. + let stdout_file = unsafe { fs::File::from_raw_fd(stdout_fd) }; + let Ok(stdout) = stdout_file.metadata() else { + return false; + }; + + let is_dev_null = stdout.dev() == dev_null.dev() && stdout.ino() == dev_null.ino(); + + // Don't let File close the fd. It's unfortunate that File doesn't have a leak_fd(). + std::mem::forget(stdout_file); + + is_dev_null +} + +pub fn parse_params>(mut opts: Peekable) -> Result { + let Some(executable) = opts.next() else { + return Err("Usage: ".to_string()); + }; + let executable_str = executable.to_string_lossy().to_string(); + + let parse_skip = |param: &str, skip_desc: &str| -> Result { + let suffix_start = param + .find(|b: char| !b.is_ascii_digit()) + .unwrap_or(param.len()); + let mut num = match param[..suffix_start].parse::() { + Ok(num) => num, + Err(e) if *e.kind() == std::num::IntErrorKind::PosOverflow => usize::MAX, + Err(_) => { + return Err(format!( + "{}: invalid --ignore-initial value '{}'", + executable_str, skip_desc + )) + } + }; + + if suffix_start != param.len() { + // Note that GNU cmp advertises supporting up to Y, but fails if you try + // to actually use anything beyond E. + let multiplier: usize = match ¶m[suffix_start..] { + "kB" => 1_000, + "K" => 1_024, + "MB" => 1_000_000, + "M" => 1_048_576, + "GB" => 1_000_000_000, + "G" => 1_073_741_824, + "TB" => 1_000_000_000_000, + "T" => 1_099_511_627_776, + "PB" => 1_000_000_000_000_000, + "P" => 1_125_899_906_842_624, + "EB" => 1_000_000_000_000_000_000, + "E" => 1_152_921_504_606_846_976, + "ZB" => usize::MAX, // 1_000_000_000_000_000_000_000, + "Z" => usize::MAX, // 1_180_591_620_717_411_303_424, + "YB" => usize::MAX, // 1_000_000_000_000_000_000_000_000, + "Y" => usize::MAX, // 1_208_925_819_614_629_174_706_176, + _ => { + return Err(format!( + "{}: invalid --ignore-initial value '{}'", + executable_str, skip_desc + )); + } + }; + + num = match num.overflowing_mul(multiplier) { + (n, false) => n, + _ => usize::MAX, + } + } + + Ok(num) + }; + + let mut params = Params { + executable, + ..Default::default() + }; + let mut from = None; + let mut to = None; + let mut skip_pos1 = None; + let mut skip_pos2 = None; + while let Some(param) = opts.next() { + if param == "--" { + break; + } + if param == "-" { + if from.is_none() { + from = Some(param); + } else if to.is_none() { + to = Some(param); + } else { + return Err(usage_string(&executable_str)); + } + continue; + } + if param == "-b" || param == "--print-bytes" { + params.print_bytes = true; + continue; + } + if param == "-l" || param == "--verbose" { + params.verbose = true; + continue; + } + if param == "-lb" || param == "-bl" { + params.print_bytes = true; + params.verbose = true; + continue; + } + + let param_str = param.to_string_lossy().to_string(); + if param == "-n" || param_str.starts_with("--bytes=") { + let max_bytes = if param == "-n" { + opts.next() + .ok_or_else(|| usage_string(&executable_str))? + .to_string_lossy() + .to_string() + } else { + let (_, arg) = param_str.split_once('=').unwrap(); + arg.to_string() + }; + let max_bytes = match max_bytes.parse::() { + Ok(num) => num, + Err(e) if *e.kind() == std::num::IntErrorKind::PosOverflow => usize::MAX, + Err(_) => { + return Err(format!( + "{}: invalid --bytes value '{}'", + executable_str, max_bytes + )) + } + }; + params.max_bytes = Some(max_bytes); + continue; + } + if param == "-i" || param_str.starts_with("--ignore-initial=") { + let skip_desc = if param == "-i" { + opts.next() + .ok_or_else(|| usage_string(&executable_str))? + .to_string_lossy() + .to_string() + } else { + let (_, arg) = param_str.split_once('=').unwrap(); + arg.to_string() + }; + let (skip_a, skip_b) = if let Some((skip_a, skip_b)) = skip_desc.split_once(':') { + ( + parse_skip(skip_a, &skip_desc)?, + parse_skip(skip_b, &skip_desc)?, + ) + } else { + let skip = parse_skip(&skip_desc, &skip_desc)?; + (skip, skip) + }; + params.skip_a = Some(skip_a); + params.skip_b = Some(skip_b); + continue; + } + if param == "-s" || param == "--quiet" || param == "--silent" { + params.quiet = true; + continue; + } + if param == "--help" { + println!("{}", usage_string(&executable_str)); + std::process::exit(0); + } + if param_str.starts_with('-') { + return Err(format!("Unknown option: {:?}", param)); + } + if from.is_none() { + from = Some(param); + } else if to.is_none() { + to = Some(param); + } else if skip_pos1.is_none() { + skip_pos1 = Some(parse_skip(¶m_str, ¶m_str)?); + } else if skip_pos2.is_none() { + skip_pos2 = Some(parse_skip(¶m_str, ¶m_str)?); + } else { + return Err(usage_string(&executable_str)); + } + } + + // Do as GNU cmp, and completely disable printing if we are + // outputing to /dev/null. + #[cfg(not(target_os = "windows"))] + if is_stdout_dev_null() { + params.quiet = true; + params.verbose = false; + params.print_bytes = false; + } + + if params.quiet && params.verbose { + return Err(format!( + "{}: options -l and -s are incompatible", + executable_str + )); + } + + params.from = if let Some(from) = from { + from + } else if let Some(param) = opts.next() { + param + } else { + return Err(usage_string(&executable_str)); + }; + params.to = if let Some(to) = to { + to + } else if let Some(param) = opts.next() { + param + } else { + OsString::from("-") + }; + + // GNU cmp ignores positional skip arguments if -i is provided. + if params.skip_a.is_none() { + if skip_pos1.is_some() { + params.skip_a = skip_pos1; + } else if let Some(param) = opts.next() { + let param_str = param.to_string_lossy().to_string(); + params.skip_a = Some(parse_skip(¶m_str, ¶m_str)?); + } + }; + if params.skip_b.is_none() { + if skip_pos2.is_some() { + params.skip_b = skip_pos2; + } else if let Some(param) = opts.next() { + let param_str = param.to_string_lossy().to_string(); + params.skip_b = Some(parse_skip(¶m_str, ¶m_str)?); + } + } + + Ok(params) +} + +fn prepare_reader( + path: &OsString, + skip: &Option, + params: &Params, +) -> Result, String> { + let mut reader: Box = if path == "-" { + Box::new(BufReader::new(io::stdin())) + } else { + match fs::File::open(path) { + Ok(file) => Box::new(BufReader::new(file)), + Err(e) => { + return Err(format_failure_to_read_input_file( + ¶ms.executable, + path, + &e, + )); + } + } + }; + + if let Some(skip) = skip { + if let Err(e) = io::copy(&mut reader.by_ref().take(*skip as u64), &mut io::sink()) { + return Err(format_failure_to_read_input_file( + ¶ms.executable, + path, + &e, + )); + } + } + + Ok(reader) +} + +#[derive(Debug)] +pub enum Cmp { + Equal, + Different, +} + +pub fn cmp(params: &Params) -> Result { + let mut from = prepare_reader(¶ms.from, ¶ms.skip_a, params)?; + let mut to = prepare_reader(¶ms.to, ¶ms.skip_b, params)?; + + let mut at_byte = 1; + let mut at_line = 1; + let mut start_of_line = true; + let mut verbose_diffs = vec![]; + loop { + // Fill up our buffers. + let from_buf = match from.fill_buf() { + Ok(buf) => buf, + Err(e) => { + return Err(format_failure_to_read_input_file( + ¶ms.executable, + ¶ms.from, + &e, + )); + } + }; + + let to_buf = match to.fill_buf() { + Ok(buf) => buf, + Err(e) => { + return Err(format_failure_to_read_input_file( + ¶ms.executable, + ¶ms.to, + &e, + )); + } + }; + + // Check for EOF conditions. + if from_buf.is_empty() && to_buf.is_empty() { + break; + } + + if from_buf.is_empty() || to_buf.is_empty() { + let eof_on = if from_buf.is_empty() { + ¶ms.from.to_string_lossy() + } else { + ¶ms.to.to_string_lossy() + }; + + if params.verbose { + report_verbose_diffs(verbose_diffs, params)?; + } + + report_eof(at_byte, at_line, start_of_line, eof_on, params); + return Ok(Cmp::Different); + } + + // Fast path - for long files in which almost all bytes are the same we + // can do a direct comparison to let the compiler optimize. + let consumed = std::cmp::min(from_buf.len(), to_buf.len()); + if from_buf[..consumed] == to_buf[..consumed] { + let last = from_buf[..consumed].last().unwrap(); + + at_byte += consumed; + at_line += from_buf[..consumed].iter().filter(|&c| *c == b'\n').count(); + + start_of_line = *last == b'\n'; + + if let Some(max_bytes) = params.max_bytes { + if at_byte > max_bytes { + break; + } + } + + from.consume(consumed); + to.consume(consumed); + + continue; + } + + // Iterate over the buffers, the zip iterator will stop us as soon as the + // first one runs out. + for (&from_byte, &to_byte) in from_buf.iter().zip(to_buf.iter()) { + if from_byte != to_byte { + if params.verbose { + verbose_diffs.push((at_byte, from_byte, to_byte)); + } else { + report_difference(from_byte, to_byte, at_byte, at_line, params); + return Ok(Cmp::Different); + } + } + + start_of_line = from_byte == b'\n'; + if start_of_line { + at_line += 1; + } + + at_byte += 1; + + if let Some(max_bytes) = params.max_bytes { + if at_byte > max_bytes { + break; + } + } + } + + // Notify our readers about the bytes we went over. + from.consume(consumed); + to.consume(consumed); + } + + if params.verbose && !verbose_diffs.is_empty() { + report_verbose_diffs(verbose_diffs, params)?; + return Ok(Cmp::Different); + } + + Ok(Cmp::Equal) +} + +// Exit codes are documented at +// https://www.gnu.org/software/diffutils/manual/html_node/Invoking-cmp.html +// An exit status of 0 means no differences were found, +// 1 means some differences were found, +// and 2 means trouble. +pub fn main(opts: Peekable) -> ExitCode { + let params = match parse_params(opts) { + Ok(param) => param, + Err(e) => { + eprintln!("{e}"); + return ExitCode::from(2); + } + }; + + if params.from == "-" && params.to == "-" + || same_file::is_same_file(¶ms.from, ¶ms.to).unwrap_or(false) + { + return ExitCode::SUCCESS; + } + + // If the files have different sizes, we already know they are not identical. If we have not + // been asked to show even the first difference, we can quit early. + if params.quiet { + if let (Ok(a_meta), Ok(b_meta)) = (fs::metadata(¶ms.from), fs::metadata(¶ms.to)) { + #[cfg(not(target_os = "windows"))] + if a_meta.size() != b_meta.size() { + return ExitCode::from(1); + } + #[cfg(target_os = "windows")] + if a_meta.file_size() != b_meta.file_size() { + return ExitCode::from(1); + } + } + } + + match cmp(¶ms) { + Ok(Cmp::Equal) => ExitCode::SUCCESS, + Ok(Cmp::Different) => ExitCode::from(1), + Err(e) => { + if !params.quiet { + eprintln!("{e}"); + } + ExitCode::from(2) + } + } +} + +#[inline] +fn is_ascii_printable(byte: u8) -> bool { + let c = byte as char; + c.is_ascii() && !c.is_ascii_control() +} + +#[inline] +fn format_byte(byte: u8) -> String { + let mut byte = byte; + let mut quoted = vec![]; + + if !is_ascii_printable(byte) { + if byte >= 128 { + quoted.push(b'M'); + quoted.push(b'-'); + byte -= 128; + } + + if byte < 32 { + quoted.push(b'^'); + byte += 64; + } else if byte == 127 { + quoted.push(b'^'); + byte = b'?'; + } + assert!((byte as char).is_ascii()); + } + + quoted.push(byte); + + // SAFETY: the checks and shifts we do above match what cat and GNU + // cmp do to ensure characters fall inside the ascii range. + unsafe { String::from_utf8_unchecked(quoted) } +} + +fn report_verbose_diffs(diffs: Vec<(usize, u8, u8)>, params: &Params) -> Result<(), String> { + assert!(!params.quiet); + + let mut stdout = BufWriter::new(io::stdout().lock()); + if let Some((offset, _, _)) = diffs.last() { + // Obtain the width of the first column from the last byte offset. + let width = format!("{}", offset).len(); + + if params.print_bytes { + for (at_byte, from_byte, to_byte) in diffs { + writeln!( + stdout, + "{:>width$} {:>3o} {:4} {:>3o} {}", + at_byte, + from_byte, + format_byte(from_byte), + to_byte, + format_byte(to_byte), + ) + .map_err(|e| { + format!( + "{}: error printing output: {e}", + params.executable.to_string_lossy() + ) + })?; + } + } else { + for (at_byte, from_byte, to_byte) in diffs { + writeln!( + stdout, + "{:>width$} {:>3o} {:>3o}", + at_byte, + from_byte, + to_byte, + width = width + ) + .map_err(|e| { + format!( + "{}: error printing output: {e}", + params.executable.to_string_lossy() + ) + })?; + } + } + } + + Ok(()) +} + +#[inline] +fn report_eof(at_byte: usize, at_line: usize, start_of_line: bool, eof_on: &str, params: &Params) { + if params.quiet { + return; + } + + if at_byte == 1 { + eprintln!( + "{}: EOF on '{}' which is empty", + params.executable.to_string_lossy(), + eof_on + ); + } else if params.verbose { + eprintln!( + "{}: EOF on '{}' after byte {}", + params.executable.to_string_lossy(), + eof_on, + at_byte - 1, + ); + } else if start_of_line { + eprintln!( + "{}: EOF on '{}' after byte {}, line {}", + params.executable.to_string_lossy(), + eof_on, + at_byte - 1, + at_line - 1 + ); + } else { + eprintln!( + "{}: EOF on '{}' after byte {}, in line {}", + params.executable.to_string_lossy(), + eof_on, + at_byte - 1, + at_line + ); + } +} + +fn is_posix_locale() -> bool { + let locale = if let Ok(locale) = env::var("LC_ALL") { + locale + } else if let Ok(locale) = env::var("LC_MESSAGES") { + locale + } else if let Ok(locale) = env::var("LANG") { + locale + } else { + "C".to_string() + }; + + locale == "C" || locale == "POSIX" +} + +#[inline] +fn report_difference(from_byte: u8, to_byte: u8, at_byte: usize, at_line: usize, params: &Params) { + if params.quiet { + return; + } + + let term = if is_posix_locale() && !params.print_bytes { + "char" + } else { + "byte" + }; + print!( + "{} {} differ: {term} {}, line {}", + ¶ms.from.to_string_lossy(), + ¶ms.to.to_string_lossy(), + at_byte, + at_line + ); + if params.print_bytes { + let char_width = if to_byte >= 0x7F { 2 } else { 1 }; + print!( + " is {:>3o} {:char_width$} {:>3o} {:char_width$}", + from_byte, + format_byte(from_byte), + to_byte, + format_byte(to_byte) + ); + } + println!(); +} + +#[cfg(test)] +mod tests { + use super::*; + fn os(s: &str) -> OsString { + OsString::from(s) + } + + #[test] + fn positional() { + assert_eq!( + Ok(Params { + executable: os("cmp"), + from: os("foo"), + to: os("bar"), + ..Default::default() + }), + parse_params([os("cmp"), os("foo"), os("bar")].iter().cloned().peekable()) + ); + + assert_eq!( + Ok(Params { + executable: os("cmp"), + from: os("foo"), + to: os("-"), + ..Default::default() + }), + parse_params([os("cmp"), os("foo")].iter().cloned().peekable()) + ); + + assert_eq!( + Ok(Params { + executable: os("cmp"), + from: os("foo"), + to: os("--help"), + ..Default::default() + }), + parse_params( + [os("cmp"), os("foo"), os("--"), os("--help")] + .iter() + .cloned() + .peekable() + ) + ); + + assert_eq!( + Ok(Params { + executable: os("cmp"), + from: os("foo"), + to: os("bar"), + skip_a: Some(1), + skip_b: None, + ..Default::default() + }), + parse_params( + [os("cmp"), os("foo"), os("bar"), os("1")] + .iter() + .cloned() + .peekable() + ) + ); + + assert_eq!( + Ok(Params { + executable: os("cmp"), + from: os("foo"), + to: os("bar"), + skip_a: Some(1), + skip_b: Some(usize::MAX), + ..Default::default() + }), + parse_params( + [os("cmp"), os("foo"), os("bar"), os("1"), os("2Y")] + .iter() + .cloned() + .peekable() + ) + ); + + // Bad positional arguments. + assert_eq!( + Err("Usage: cmp ".to_string()), + parse_params( + [os("cmp"), os("foo"), os("bar"), os("1"), os("2"), os("3")] + .iter() + .cloned() + .peekable() + ) + ); + assert_eq!( + Err("Usage: cmp ".to_string()), + parse_params([os("cmp")].iter().cloned().peekable()) + ); + } + + #[test] + fn execution_modes() { + let print_bytes = Params { + executable: os("cmp"), + from: os("foo"), + to: os("bar"), + print_bytes: true, + ..Default::default() + }; + assert_eq!( + Ok(print_bytes.clone()), + parse_params( + [os("cmp"), os("-b"), os("foo"), os("bar")] + .iter() + .cloned() + .peekable() + ) + ); + assert_eq!( + Ok(print_bytes), + parse_params( + [os("cmp"), os("--print-bytes"), os("foo"), os("bar")] + .iter() + .cloned() + .peekable() + ) + ); + + let verbose = Params { + executable: os("cmp"), + from: os("foo"), + to: os("bar"), + verbose: true, + ..Default::default() + }; + assert_eq!( + Ok(verbose.clone()), + parse_params( + [os("cmp"), os("-l"), os("foo"), os("bar")] + .iter() + .cloned() + .peekable() + ) + ); + assert_eq!( + Ok(verbose), + parse_params( + [os("cmp"), os("--verbose"), os("foo"), os("bar")] + .iter() + .cloned() + .peekable() + ) + ); + + let verbose_and_print_bytes = Params { + executable: os("cmp"), + from: os("foo"), + to: os("bar"), + print_bytes: true, + verbose: true, + ..Default::default() + }; + assert_eq!( + Ok(verbose_and_print_bytes.clone()), + parse_params( + [os("cmp"), os("-l"), os("-b"), os("foo"), os("bar")] + .iter() + .cloned() + .peekable() + ) + ); + assert_eq!( + Ok(verbose_and_print_bytes.clone()), + parse_params( + [os("cmp"), os("-lb"), os("foo"), os("bar")] + .iter() + .cloned() + .peekable() + ) + ); + assert_eq!( + Ok(verbose_and_print_bytes), + parse_params( + [os("cmp"), os("-bl"), os("foo"), os("bar")] + .iter() + .cloned() + .peekable() + ) + ); + + assert_eq!( + Ok(Params { + executable: os("cmp"), + from: os("foo"), + to: os("bar"), + quiet: true, + ..Default::default() + }), + parse_params( + [os("cmp"), os("-s"), os("foo"), os("bar")] + .iter() + .cloned() + .peekable() + ) + ); + + // Some options do not mix. + assert_eq!( + Err("cmp: options -l and -s are incompatible".to_string()), + parse_params( + [os("cmp"), os("-l"), os("-s"), os("foo"), os("bar")] + .iter() + .cloned() + .peekable() + ) + ); + } + + #[test] + fn max_bytes() { + let max_bytes = Params { + executable: os("cmp"), + from: os("foo"), + to: os("bar"), + max_bytes: Some(1), + ..Default::default() + }; + assert_eq!( + Ok(max_bytes.clone()), + parse_params( + [os("cmp"), os("-n"), os("1"), os("foo"), os("bar")] + .iter() + .cloned() + .peekable() + ) + ); + assert_eq!( + Ok(max_bytes), + parse_params( + [os("cmp"), os("--bytes=1"), os("foo"), os("bar")] + .iter() + .cloned() + .peekable() + ) + ); + + assert_eq!( + Ok(Params { + executable: os("cmp"), + from: os("foo"), + to: os("bar"), + max_bytes: Some(usize::MAX), + ..Default::default() + }), + parse_params( + [ + os("cmp"), + os("--bytes=99999999999999999999999999999999999999999999999999999999999"), + os("foo"), + os("bar") + ] + .iter() + .cloned() + .peekable() + ) + ); + + // Failure case + assert_eq!( + Err("cmp: invalid --bytes value '1K'".to_string()), + parse_params( + [os("cmp"), os("--bytes=1K"), os("foo"), os("bar")] + .iter() + .cloned() + .peekable() + ) + ); + } + + #[test] + fn skips() { + let skips = Params { + executable: os("cmp"), + from: os("foo"), + to: os("bar"), + skip_a: Some(1), + skip_b: Some(1), + ..Default::default() + }; + assert_eq!( + Ok(skips.clone()), + parse_params( + [os("cmp"), os("-i"), os("1"), os("foo"), os("bar")] + .iter() + .cloned() + .peekable() + ) + ); + assert_eq!( + Ok(skips), + parse_params( + [os("cmp"), os("--ignore-initial=1"), os("foo"), os("bar")] + .iter() + .cloned() + .peekable() + ) + ); + + assert_eq!( + Ok(Params { + executable: os("cmp"), + from: os("foo"), + to: os("bar"), + skip_a: Some(usize::MAX), + skip_b: Some(usize::MAX), + ..Default::default() + }), + parse_params( + [ + os("cmp"), + os("-i"), + os("99999999999999999999999999999999999999999999999999999999999"), + os("foo"), + os("bar") + ] + .iter() + .cloned() + .peekable() + ) + ); + + assert_eq!( + Ok(Params { + executable: os("cmp"), + from: os("foo"), + to: os("bar"), + skip_a: Some(1), + skip_b: Some(2), + ..Default::default() + }), + parse_params( + [os("cmp"), os("--ignore-initial=1:2"), os("foo"), os("bar")] + .iter() + .cloned() + .peekable() + ) + ); + + assert_eq!( + Ok(Params { + executable: os("cmp"), + from: os("foo"), + to: os("bar"), + skip_a: Some(1_000_000_000), + skip_b: Some(1_152_921_504_606_846_976 * 2), + ..Default::default() + }), + parse_params( + [ + os("cmp"), + os("--ignore-initial=1GB:2E"), + os("foo"), + os("bar") + ] + .iter() + .cloned() + .peekable() + ) + ); + + // All special suffixes. + for (i, suffixes) in [ + ["kB", "K"], + ["MB", "M"], + ["GB", "G"], + ["TB", "T"], + ["PB", "P"], + ["EB", "E"], + ["ZB", "Z"], + ["YB", "Y"], + ] + .iter() + .enumerate() + { + let values = [ + 1_000usize.checked_pow((i + 1) as u32).unwrap_or(usize::MAX), + 1024usize.checked_pow((i + 1) as u32).unwrap_or(usize::MAX), + ]; + for (j, v) in values.iter().enumerate() { + assert_eq!( + Ok(Params { + executable: os("cmp"), + from: os("foo"), + to: os("bar"), + skip_a: Some(*v), + skip_b: Some(2), + ..Default::default() + }), + parse_params( + [ + os("cmp"), + os("-i"), + os(&format!("1{}:2", suffixes[j])), + os("foo"), + os("bar"), + ] + .iter() + .cloned() + .peekable() + ) + ); + } + } + + // Ignores positional arguments when -i is provided. + assert_eq!( + Ok(Params { + executable: os("cmp"), + from: os("foo"), + to: os("bar"), + skip_a: Some(1), + skip_b: Some(2), + ..Default::default() + }), + parse_params( + [ + os("cmp"), + os("-i"), + os("1:2"), + os("foo"), + os("bar"), + os("3"), + os("4") + ] + .iter() + .cloned() + .peekable() + ) + ); + + // Failure cases + assert_eq!( + Err("cmp: invalid --ignore-initial value '1mb'".to_string()), + parse_params( + [os("cmp"), os("--ignore-initial=1mb"), os("foo"), os("bar")] + .iter() + .cloned() + .peekable() + ) + ); + assert_eq!( + Err("cmp: invalid --ignore-initial value '1:2:3'".to_string()), + parse_params( + [ + os("cmp"), + os("--ignore-initial=1:2:3"), + os("foo"), + os("bar") + ] + .iter() + .cloned() + .peekable() + ) + ); + assert_eq!( + Err("cmp: invalid --ignore-initial value '-1'".to_string()), + parse_params( + [os("cmp"), os("--ignore-initial=-1"), os("foo"), os("bar")] + .iter() + .cloned() + .peekable() + ) + ); + } +} diff --git a/src/diff.rs b/src/diff.rs index 6998e2b..f769a29 100644 --- a/src/diff.rs +++ b/src/diff.rs @@ -18,7 +18,7 @@ use std::process::{exit, ExitCode}; // An exit status of 0 means no differences were found, // 1 means some differences were found, // and 2 means trouble. -pub(crate) fn main(opts: Peekable) -> ExitCode { +pub fn main(opts: Peekable) -> ExitCode { let params = parse_params(opts).unwrap_or_else(|error| { eprintln!("{error}"); exit(2); diff --git a/src/lib.rs b/src/lib.rs index 0bb911b..a20ac56 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +pub mod cmp; pub mod context_diff; pub mod ed_diff; pub mod macros; diff --git a/src/main.rs b/src/main.rs index 824b45c..8194d00 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,12 +5,13 @@ use std::{ env::ArgsOs, - ffi::OsString, + ffi::{OsStr, OsString}, iter::Peekable, path::{Path, PathBuf}, process::ExitCode, }; +mod cmp; mod context_diff; mod diff; mod ed_diff; @@ -29,8 +30,10 @@ fn binary_path(args: &mut Peekable) -> PathBuf { } } -fn name(binary_path: &Path) -> Option<&str> { - binary_path.file_stem()?.to_str() +/// #Panics +/// Panics if path has no UTF-8 valid name +fn name(binary_path: &Path) -> &OsStr { + binary_path.file_stem().unwrap() } const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -39,12 +42,12 @@ fn usage(name: &str) { println!("{name} {VERSION} (multi-call binary)\n"); println!("Usage: {name} [function [arguments...]]\n"); println!("Currently defined functions:\n"); - println!(" diff\n"); + println!(" cmp, diff\n"); } -fn second_arg_error(name: &str) -> ! { - println!("Expected utility name as second argument, got nothing."); - usage(name); +fn second_arg_error(name: &OsStr) -> ! { + eprintln!("Expected utility name as second argument, got nothing."); + usage(&name.to_string_lossy()); std::process::exit(0); } @@ -52,10 +55,7 @@ fn main() -> ExitCode { let mut args = std::env::args_os().peekable(); let exe_path = binary_path(&mut args); - let exe_name = name(&exe_path).unwrap_or_else(|| { - usage(""); - std::process::exit(1); - }); + let exe_name = name(&exe_path); let util_name = if exe_name == "diffutils" { // Discard the item we peeked. @@ -70,9 +70,10 @@ fn main() -> ExitCode { match util_name.to_str() { Some("diff") => diff::main(args), + Some("cmp") => cmp::main(args), Some(name) => { - usage(&format!("{}: utility not supported", name)); - ExitCode::from(1) + eprintln!("{}: utility not supported", name); + ExitCode::from(2) } None => second_arg_error(exe_name), } diff --git a/src/utils.rs b/src/utils.rs index a216784..88b39ff 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -72,19 +72,30 @@ pub fn get_modification_time(file_path: &str) -> String { modification_time } -pub fn report_failure_to_read_input_file( +pub fn format_failure_to_read_input_file( executable: &OsString, filepath: &OsString, error: &std::io::Error, -) { +) -> String { // std::io::Error's display trait outputs "{detail} (os error {code})" // but we want only the {detail} (error string) part let error_code_re = Regex::new(r"\ \(os\ error\ \d+\)$").unwrap(); - eprintln!( + format!( "{}: {}: {}", executable.to_string_lossy(), filepath.to_string_lossy(), error_code_re.replace(error.to_string().as_str(), ""), + ) +} + +pub fn report_failure_to_read_input_file( + executable: &OsString, + filepath: &OsString, + error: &std::io::Error, +) { + eprintln!( + "{}", + format_failure_to_read_input_file(executable, filepath, error) ); } diff --git a/tests/integration.rs b/tests/integration.rs index 2b3fd4f..4cff8ff 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -4,314 +4,869 @@ // files that was distributed with this source code. use assert_cmd::cmd::Command; -use diffutilslib::assert_diff_eq; use predicates::prelude::*; -use std::fs::File; +use std::fs::{File, OpenOptions}; use std::io::Write; use tempfile::{tempdir, NamedTempFile}; // Integration tests for the diffutils command +mod common { + use super::*; -#[test] -fn unknown_param() -> Result<(), Box> { - let mut cmd = Command::cargo_bin("diffutils")?; - cmd.arg("diff"); - cmd.arg("--foobar"); - cmd.assert() - .code(predicate::eq(2)) - .failure() - .stderr(predicate::str::starts_with("Unknown option: \"--foobar\"")); - Ok(()) -} + #[test] + fn unknown_param() -> Result<(), Box> { + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("patch"); + cmd.assert() + .code(predicate::eq(2)) + .failure() + .stderr(predicate::eq("patch: utility not supported\n")); -#[test] -fn cannot_read_files() -> Result<(), Box> { - let file = NamedTempFile::new()?; + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.assert() + .code(predicate::eq(0)) + .success() + .stderr(predicate::str::starts_with( + "Expected utility name as second argument, got nothing.\n", + )); + + for subcmd in ["diff", "cmp"] { + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg(subcmd); + cmd.arg("--foobar"); + cmd.assert() + .code(predicate::eq(2)) + .failure() + .stderr(predicate::str::starts_with("Unknown option: \"--foobar\"")); + } + Ok(()) + } - let nofile = NamedTempFile::new()?; - let nopath = nofile.into_temp_path(); - std::fs::remove_file(&nopath)?; + #[test] + fn cannot_read_files() -> Result<(), Box> { + let file = NamedTempFile::new()?; + + let nofile = NamedTempFile::new()?; + let nopath = nofile.into_temp_path(); + std::fs::remove_file(&nopath)?; + + #[cfg(not(windows))] + let error_message = "No such file or directory"; + #[cfg(windows)] + let error_message = "The system cannot find the file specified."; + + for subcmd in ["diff", "cmp"] { + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg(subcmd); + cmd.arg(&nopath).arg(file.path()); + cmd.assert() + .code(predicate::eq(2)) + .failure() + .stderr(predicate::str::ends_with(format!( + ": {}: {error_message}\n", + &nopath.as_os_str().to_string_lossy() + ))); + + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg(subcmd); + cmd.arg(file.path()).arg(&nopath); + cmd.assert() + .code(predicate::eq(2)) + .failure() + .stderr(predicate::str::ends_with(format!( + ": {}: {error_message}\n", + &nopath.as_os_str().to_string_lossy() + ))); + } - #[cfg(not(windows))] - let error_message = "No such file or directory"; - #[cfg(windows)] - let error_message = "The system cannot find the file specified."; - - let mut cmd = Command::cargo_bin("diffutils")?; - cmd.arg("diff"); - cmd.arg(&nopath).arg(file.path()); - cmd.assert() - .code(predicate::eq(2)) - .failure() - .stderr(predicate::str::ends_with(format!( - ": {}: {error_message}\n", - &nopath.as_os_str().to_string_lossy() - ))); - - let mut cmd = Command::cargo_bin("diffutils")?; - cmd.arg("diff"); - cmd.arg(file.path()).arg(&nopath); - cmd.assert() - .code(predicate::eq(2)) - .failure() - .stderr(predicate::str::ends_with(format!( - ": {}: {error_message}\n", - &nopath.as_os_str().to_string_lossy() - ))); - - let mut cmd = Command::cargo_bin("diffutils")?; - cmd.arg("diff"); - cmd.arg(&nopath).arg(&nopath); - cmd.assert().code(predicate::eq(2)).failure().stderr( - predicate::str::contains(format!( - ": {}: {error_message}\n", - &nopath.as_os_str().to_string_lossy() - )) - .count(2), - ); - - Ok(()) + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("diff"); + cmd.arg(&nopath).arg(&nopath); + cmd.assert().code(predicate::eq(2)).failure().stderr( + predicate::str::contains(format!( + ": {}: {error_message}\n", + &nopath.as_os_str().to_string_lossy() + )) + .count(2), + ); + + Ok(()) + } } -#[test] -fn no_differences() -> Result<(), Box> { - let file = NamedTempFile::new()?; - for option in ["", "-u", "-c", "-e"] { +mod diff { + use diffutilslib::assert_diff_eq; + + use super::*; + + #[test] + fn no_differences() -> Result<(), Box> { + let file = NamedTempFile::new()?; + for option in ["", "-u", "-c", "-e"] { + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("diff"); + if !option.is_empty() { + cmd.arg(option); + } + cmd.arg(file.path()).arg(file.path()); + cmd.assert() + .code(predicate::eq(0)) + .success() + .stdout(predicate::str::is_empty()); + } + Ok(()) + } + + #[test] + fn no_differences_report_identical_files() -> Result<(), Box> { + // same file + let mut file1 = NamedTempFile::new()?; + file1.write_all("foo\n".as_bytes())?; + for option in ["", "-u", "-c", "-e"] { + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("diff"); + if !option.is_empty() { + cmd.arg(option); + } + cmd.arg("-s").arg(file1.path()).arg(file1.path()); + cmd.assert() + .code(predicate::eq(0)) + .success() + .stdout(predicate::eq(format!( + "Files {} and {} are identical\n", + file1.path().to_string_lossy(), + file1.path().to_string_lossy(), + ))); + } + // two files with the same content + let mut file2 = NamedTempFile::new()?; + file2.write_all("foo\n".as_bytes())?; + for option in ["", "-u", "-c", "-e"] { + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("diff"); + if !option.is_empty() { + cmd.arg(option); + } + cmd.arg("-s").arg(file1.path()).arg(file2.path()); + cmd.assert() + .code(predicate::eq(0)) + .success() + .stdout(predicate::eq(format!( + "Files {} and {} are identical\n", + file1.path().to_string_lossy(), + file2.path().to_string_lossy(), + ))); + } + Ok(()) + } + + #[test] + fn differences() -> Result<(), Box> { + let mut file1 = NamedTempFile::new()?; + file1.write_all("foo\n".as_bytes())?; + let mut file2 = NamedTempFile::new()?; + file2.write_all("bar\n".as_bytes())?; + for option in ["", "-u", "-c", "-e"] { + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("diff"); + if !option.is_empty() { + cmd.arg(option); + } + cmd.arg(file1.path()).arg(file2.path()); + cmd.assert() + .code(predicate::eq(1)) + .failure() + .stdout(predicate::str::is_empty().not()); + } + Ok(()) + } + + #[test] + fn differences_brief() -> Result<(), Box> { + let mut file1 = NamedTempFile::new()?; + file1.write_all("foo\n".as_bytes())?; + let mut file2 = NamedTempFile::new()?; + file2.write_all("bar\n".as_bytes())?; + for option in ["", "-u", "-c", "-e"] { + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("diff"); + if !option.is_empty() { + cmd.arg(option); + } + cmd.arg("-q").arg(file1.path()).arg(file2.path()); + cmd.assert() + .code(predicate::eq(1)) + .failure() + .stdout(predicate::eq(format!( + "Files {} and {} differ\n", + file1.path().to_string_lossy(), + file2.path().to_string_lossy() + ))); + } + Ok(()) + } + + #[test] + fn missing_newline() -> Result<(), Box> { + let mut file1 = NamedTempFile::new()?; + file1.write_all("foo".as_bytes())?; + let mut file2 = NamedTempFile::new()?; + file2.write_all("bar".as_bytes())?; let mut cmd = Command::cargo_bin("diffutils")?; cmd.arg("diff"); - if !option.is_empty() { - cmd.arg(option); - } - cmd.arg(file.path()).arg(file.path()); + cmd.arg("-e").arg(file1.path()).arg(file2.path()); + cmd.assert() + .code(predicate::eq(2)) + .failure() + .stderr(predicate::str::starts_with("No newline at end of file")); + Ok(()) + } + + #[test] + fn read_from_stdin() -> Result<(), Box> { + let mut file1 = NamedTempFile::new()?; + file1.write_all("foo\n".as_bytes())?; + let mut file2 = NamedTempFile::new()?; + file2.write_all("bar\n".as_bytes())?; + + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("diff"); + cmd.arg("-u") + .arg(file1.path()) + .arg("-") + .write_stdin("bar\n"); + cmd.assert().code(predicate::eq(1)).failure(); + + let output = cmd.output().unwrap().stdout; + assert_diff_eq!( + output, + format!( + "--- {}\tTIMESTAMP\n+++ -\tTIMESTAMP\n@@ -1 +1 @@\n-foo\n+bar\n", + file1.path().to_string_lossy() + ) + ); + + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("diff"); + cmd.arg("-u") + .arg("-") + .arg(file2.path()) + .write_stdin("foo\n"); + cmd.assert().code(predicate::eq(1)).failure(); + + let output = cmd.output().unwrap().stdout; + assert_diff_eq!( + output, + format!( + "--- -\tTIMESTAMP\n+++ {}\tTIMESTAMP\n@@ -1 +1 @@\n-foo\n+bar\n", + file2.path().to_string_lossy() + ) + ); + + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("diff"); + cmd.arg("-u").arg("-").arg("-"); cmd.assert() .code(predicate::eq(0)) .success() .stdout(predicate::str::is_empty()); + + #[cfg(unix)] + { + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("diff"); + cmd.arg("-u") + .arg(file1.path()) + .arg("/dev/stdin") + .write_stdin("bar\n"); + cmd.assert().code(predicate::eq(1)).failure(); + + let output = cmd.output().unwrap().stdout; + assert_diff_eq!( + output, + format!( + "--- {}\tTIMESTAMP\n+++ /dev/stdin\tTIMESTAMP\n@@ -1 +1 @@\n-foo\n+bar\n", + file1.path().to_string_lossy() + ) + ); + } + + Ok(()) } - Ok(()) -} -#[test] -fn no_differences_report_identical_files() -> Result<(), Box> { - // same file - let mut file1 = NamedTempFile::new()?; - file1.write_all("foo\n".as_bytes())?; - for option in ["", "-u", "-c", "-e"] { + #[test] + fn compare_file_to_directory() -> Result<(), Box> { + let tmp_dir = tempdir()?; + + let directory = tmp_dir.path().join("d"); + let _ = std::fs::create_dir(&directory); + + let a_path = tmp_dir.path().join("a"); + let mut a = File::create(&a_path).unwrap(); + a.write_all(b"a\n").unwrap(); + + let da_path = directory.join("a"); + let mut da = File::create(&da_path).unwrap(); + da.write_all(b"da\n").unwrap(); + let mut cmd = Command::cargo_bin("diffutils")?; cmd.arg("diff"); - if !option.is_empty() { - cmd.arg(option); - } - cmd.arg("-s").arg(file1.path()).arg(file1.path()); + cmd.arg("-u").arg(&directory).arg(&a_path); + cmd.assert().code(predicate::eq(1)).failure(); + + let output = cmd.output().unwrap().stdout; + assert_diff_eq!( + output, + format!( + "--- {}\tTIMESTAMP\n+++ {}\tTIMESTAMP\n@@ -1 +1 @@\n-da\n+a\n", + da_path.display(), + a_path.display() + ) + ); + + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("diff"); + cmd.arg("-u").arg(&a_path).arg(&directory); + cmd.assert().code(predicate::eq(1)).failure(); + + let output = cmd.output().unwrap().stdout; + assert_diff_eq!( + output, + format!( + "--- {}\tTIMESTAMP\n+++ {}\tTIMESTAMP\n@@ -1 +1 @@\n-a\n+da\n", + a_path.display(), + da_path.display() + ) + ); + + Ok(()) + } +} + +mod cmp { + use super::*; + + #[test] + fn cmp_incompatible_params() -> Result<(), Box> { + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("cmp"); + cmd.arg("-l"); + cmd.arg("-s"); + cmd.arg("/etc/passwd").arg("/etc/group"); + cmd.assert() + .code(predicate::eq(2)) + .failure() + .stderr(predicate::str::ends_with( + ": options -l and -s are incompatible\n", + )); + + Ok(()) + } + + #[test] + fn cmp_stdin() -> Result<(), Box> { + let tmp_dir = tempdir()?; + + let a_path = tmp_dir.path().join("a"); + let mut a = File::create(&a_path).unwrap(); + a.write_all(b"a\n").unwrap(); + + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("cmp"); + cmd.arg(&a_path); + cmd.write_stdin("a\n"); cmd.assert() .code(predicate::eq(0)) .success() - .stdout(predicate::eq(format!( - "Files {} and {} are identical\n", - file1.path().to_string_lossy(), - file1.path().to_string_lossy(), - ))); + .stderr(predicate::str::is_empty()) + .stdout(predicate::str::is_empty()); + + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.env("LC_ALL", "C"); + cmd.arg("cmp"); + cmd.arg(&a_path); + cmd.write_stdin("b\n"); + cmd.assert() + .code(predicate::eq(1)) + .failure() + .stderr(predicate::str::is_empty()) + .stdout(predicate::str::ends_with(" - differ: char 1, line 1\n")); + + Ok(()) } - // two files with the same content - let mut file2 = NamedTempFile::new()?; - file2.write_all("foo\n".as_bytes())?; - for option in ["", "-u", "-c", "-e"] { + + #[test] + fn cmp_equal_files() -> Result<(), Box> { + let tmp_dir = tempdir()?; + + let a_path = tmp_dir.path().join("a"); + let mut a = File::create(&a_path).unwrap(); + a.write_all(b"a\n").unwrap(); + + let b_path = tmp_dir.path().join("b"); + let mut b = File::create(&b_path).unwrap(); + b.write_all(b"a\n").unwrap(); + let mut cmd = Command::cargo_bin("diffutils")?; - cmd.arg("diff"); - if !option.is_empty() { - cmd.arg(option); - } - cmd.arg("-s").arg(file1.path()).arg(file2.path()); + cmd.arg("cmp"); + cmd.arg(&a_path).arg(&b_path); cmd.assert() .code(predicate::eq(0)) .success() - .stdout(predicate::eq(format!( - "Files {} and {} are identical\n", - file1.path().to_string_lossy(), - file2.path().to_string_lossy(), - ))); + .stderr(predicate::str::is_empty()) + .stdout(predicate::str::is_empty()); + + Ok(()) } - Ok(()) -} -#[test] -fn differences() -> Result<(), Box> { - let mut file1 = NamedTempFile::new()?; - file1.write_all("foo\n".as_bytes())?; - let mut file2 = NamedTempFile::new()?; - file2.write_all("bar\n".as_bytes())?; - for option in ["", "-u", "-c", "-e"] { + #[test] + fn cmp_one_file_empty() -> Result<(), Box> { + let tmp_dir = tempdir()?; + + let a_path = tmp_dir.path().join("a"); + let mut a = File::create(&a_path).unwrap(); + a.write_all(b"a\n").unwrap(); + + let b_path = tmp_dir.path().join("b"); + let _ = File::create(&b_path).unwrap(); + let mut cmd = Command::cargo_bin("diffutils")?; - cmd.arg("diff"); - if !option.is_empty() { - cmd.arg(option); - } - cmd.arg(file1.path()).arg(file2.path()); + cmd.arg("cmp"); + cmd.arg(&a_path).arg(&b_path); cmd.assert() .code(predicate::eq(1)) .failure() - .stdout(predicate::str::is_empty().not()); + .stderr(predicate::str::contains(" EOF on ")) + .stderr(predicate::str::ends_with(" which is empty\n")); + + Ok(()) } - Ok(()) -} -#[test] -fn differences_brief() -> Result<(), Box> { - let mut file1 = NamedTempFile::new()?; - file1.write_all("foo\n".as_bytes())?; - let mut file2 = NamedTempFile::new()?; - file2.write_all("bar\n".as_bytes())?; - for option in ["", "-u", "-c", "-e"] { + #[test] + fn cmp_immediate_difference() -> Result<(), Box> { + let tmp_dir = tempdir()?; + + let a_path = tmp_dir.path().join("a"); + let mut a = File::create(&a_path).unwrap(); + a.write_all(b"abc\n").unwrap(); + + let b_path = tmp_dir.path().join("b"); + let mut b = File::create(&b_path).unwrap(); + b.write_all(b"bcd\n").unwrap(); + let mut cmd = Command::cargo_bin("diffutils")?; - cmd.arg("diff"); - if !option.is_empty() { - cmd.arg(option); - } - cmd.arg("-q").arg(file1.path()).arg(file2.path()); + cmd.env("LC_ALL", "C"); + cmd.arg("cmp"); + cmd.arg(&a_path).arg(&b_path); cmd.assert() .code(predicate::eq(1)) .failure() - .stdout(predicate::eq(format!( - "Files {} and {} differ\n", - file1.path().to_string_lossy(), - file2.path().to_string_lossy() - ))); + .stdout(predicate::str::ends_with(" differ: char 1, line 1\n")); + + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.env("LC_ALL", "C"); + cmd.arg("cmp"); + cmd.arg("-b"); + cmd.arg(&a_path).arg(&b_path); + cmd.assert() + .code(predicate::eq(1)) + .failure() + .stderr(predicate::str::is_empty()) + .stdout(predicate::str::ends_with( + " differ: byte 1, line 1 is 141 a 142 b\n", + )); + + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.env("LC_ALL", "C"); + cmd.arg("cmp"); + cmd.arg("-l"); + cmd.arg(&a_path).arg(&b_path); + cmd.assert() + .code(predicate::eq(1)) + .failure() + .stderr(predicate::str::is_empty()) + .stdout(predicate::eq("1 141 142\n2 142 143\n3 143 144\n")); + + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.env("LC_ALL", "C"); + cmd.arg("cmp"); + cmd.arg("-l"); + cmd.arg("-b"); + cmd.arg(&a_path).arg(&b_path); + cmd.assert() + .code(predicate::eq(1)) + .failure() + .stderr(predicate::str::is_empty()) + .stdout(predicate::eq( + "1 141 a 142 b\n2 142 b 143 c\n3 143 c 144 d\n", + )); + + Ok(()) } - Ok(()) -} -#[test] -fn missing_newline() -> Result<(), Box> { - let mut file1 = NamedTempFile::new()?; - file1.write_all("foo".as_bytes())?; - let mut file2 = NamedTempFile::new()?; - file2.write_all("bar".as_bytes())?; - let mut cmd = Command::cargo_bin("diffutils")?; - cmd.arg("diff"); - cmd.arg("-e").arg(file1.path()).arg(file2.path()); - cmd.assert() - .code(predicate::eq(2)) - .failure() - .stderr(predicate::str::starts_with("No newline at end of file")); - Ok(()) -} + #[test] + fn cmp_newline_difference() -> Result<(), Box> { + let tmp_dir = tempdir()?; + + let a_path = tmp_dir.path().join("a"); + let mut a = File::create(&a_path).unwrap(); + a.write_all(b"abc\ndefg").unwrap(); + + let b_path = tmp_dir.path().join("b"); + let mut b = File::create(&b_path).unwrap(); + b.write_all(b"abc\ndef\ng").unwrap(); -#[test] -fn read_from_stdin() -> Result<(), Box> { - let mut file1 = NamedTempFile::new()?; - file1.write_all("foo\n".as_bytes())?; - let mut file2 = NamedTempFile::new()?; - file2.write_all("bar\n".as_bytes())?; - - let mut cmd = Command::cargo_bin("diffutils")?; - cmd.arg("diff"); - cmd.arg("-u") - .arg(file1.path()) - .arg("-") - .write_stdin("bar\n"); - cmd.assert().code(predicate::eq(1)).failure(); - - let output = cmd.output().unwrap().stdout; - assert_diff_eq!( - output, - format!( - "--- {}\tTIMESTAMP\n+++ -\tTIMESTAMP\n@@ -1 +1 @@\n-foo\n+bar\n", - file1.path().to_string_lossy() - ) - ); - - let mut cmd = Command::cargo_bin("diffutils")?; - cmd.arg("diff"); - cmd.arg("-u") - .arg("-") - .arg(file2.path()) - .write_stdin("foo\n"); - cmd.assert().code(predicate::eq(1)).failure(); - - let output = cmd.output().unwrap().stdout; - assert_diff_eq!( - output, - format!( - "--- -\tTIMESTAMP\n+++ {}\tTIMESTAMP\n@@ -1 +1 @@\n-foo\n+bar\n", - file2.path().to_string_lossy() - ) - ); - - let mut cmd = Command::cargo_bin("diffutils")?; - cmd.arg("diff"); - cmd.arg("-u").arg("-").arg("-"); - cmd.assert() - .code(predicate::eq(0)) - .success() - .stdout(predicate::str::is_empty()); - - #[cfg(unix)] - { let mut cmd = Command::cargo_bin("diffutils")?; - cmd.arg("diff"); - cmd.arg("-u") - .arg(file1.path()) - .arg("/dev/stdin") - .write_stdin("bar\n"); - cmd.assert().code(predicate::eq(1)).failure(); + cmd.env("LC_ALL", "C"); + cmd.arg("cmp"); + cmd.arg(&a_path).arg(&b_path); + cmd.assert() + .code(predicate::eq(1)) + .failure() + .stderr(predicate::str::is_empty()) + .stdout(predicate::str::ends_with(" differ: char 8, line 2\n")); - let output = cmd.output().unwrap().stdout; - assert_diff_eq!( - output, - format!( - "--- {}\tTIMESTAMP\n+++ /dev/stdin\tTIMESTAMP\n@@ -1 +1 @@\n-foo\n+bar\n", - file1.path().to_string_lossy() - ) - ); + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.env("LC_ALL", "C"); + cmd.arg("cmp"); + cmd.arg("-b"); + cmd.arg(&a_path).arg(&b_path); + cmd.assert() + .code(predicate::eq(1)) + .failure() + .stderr(predicate::str::is_empty()) + .stdout(predicate::str::ends_with( + " differ: byte 8, line 2 is 147 g 12 ^J\n", + )); + + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.env("LC_ALL", "C"); + cmd.arg("cmp"); + cmd.arg("-l"); + cmd.arg(&a_path).arg(&b_path); + cmd.assert() + .code(predicate::eq(1)) + .failure() + .stdout(predicate::str::starts_with("8 147 12\n")) + .stderr(predicate::str::contains(" EOF on")) + .stderr(predicate::str::ends_with(" after byte 8\n")); + + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.env("LC_ALL", "C"); + cmd.arg("cmp"); + cmd.arg("-b"); + cmd.arg("-l"); + cmd.arg(&a_path).arg(&b_path); + cmd.assert() + .code(predicate::eq(1)) + .failure() + .stdout(predicate::str::starts_with("8 147 g 12 ^J\n")) + .stderr(predicate::str::contains(" EOF on")) + .stderr(predicate::str::ends_with(" after byte 8\n")); + + Ok(()) } - Ok(()) -} + #[test] + fn cmp_max_bytes() -> Result<(), Box> { + let tmp_dir = tempdir()?; + + let a_path = tmp_dir.path().join("a"); + let mut a = File::create(&a_path).unwrap(); + a.write_all(b"abc efg ijkl\n").unwrap(); + + let b_path = tmp_dir.path().join("b"); + let mut b = File::create(&b_path).unwrap(); + b.write_all(b"abcdefghijkl\n").unwrap(); + + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("cmp"); + cmd.arg("-l"); + cmd.arg("-b"); + cmd.arg("-n"); + cmd.arg("3"); + cmd.arg(&a_path).arg(&b_path); + cmd.assert() + .code(predicate::eq(0)) + .success() + .stderr(predicate::str::is_empty()) + .stdout(predicate::str::is_empty()); + + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("cmp"); + cmd.arg("-l"); + cmd.arg("-b"); + cmd.arg("-n"); + cmd.arg("4"); + cmd.arg(&a_path).arg(&b_path); + cmd.assert() + .code(predicate::eq(1)) + .failure() + .stderr(predicate::str::is_empty()) + .stdout(predicate::eq("4 40 144 d\n")); + + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("cmp"); + cmd.arg("-l"); + cmd.arg("-b"); + cmd.arg("-n"); + cmd.arg("13"); + cmd.arg(&a_path).arg(&b_path); + cmd.assert() + .code(predicate::eq(1)) + .failure() + .stderr(predicate::str::is_empty()) + .stdout(predicate::eq("4 40 144 d\n8 40 150 h\n")); + Ok(()) + } + + #[test] + fn cmp_skip_args_parsing() -> Result<(), Box> { + let tmp_dir = tempdir()?; + + let a_path = tmp_dir.path().join("a"); + let mut a = File::create(&a_path).unwrap(); + a.write_all(b"---abc\n").unwrap(); + + let b_path = tmp_dir.path().join("b"); + let mut b = File::create(&b_path).unwrap(); + b.write_all(b"###abc\n").unwrap(); + + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.env("LC_ALL", "C"); + cmd.arg("cmp"); + cmd.arg("-i"); + cmd.arg("3"); + cmd.arg(&a_path).arg(&b_path); + cmd.assert() + .code(predicate::eq(0)) + .success() + .stderr(predicate::str::is_empty()) + .stdout(predicate::str::is_empty()); + + // Positional skips should be ignored + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.env("LC_ALL", "C"); + cmd.arg("cmp"); + cmd.arg("-i"); + cmd.arg("3"); + cmd.arg(&a_path).arg(&b_path); + cmd.arg("1").arg("1"); + cmd.assert() + .code(predicate::eq(0)) + .success() + .stderr(predicate::str::is_empty()) + .stdout(predicate::str::is_empty()); + + // Single positional argument should only affect first file. + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.env("LC_ALL", "C"); + cmd.arg("cmp"); + cmd.arg(&a_path).arg(&b_path); + cmd.arg("3"); + cmd.assert() + .code(predicate::eq(1)) + .failure() + .stderr(predicate::str::is_empty()) + .stdout(predicate::str::ends_with(" differ: char 1, line 1\n")); + + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.env("LC_ALL", "C"); + cmd.arg("cmp"); + cmd.arg(&a_path).arg(&b_path); + cmd.arg("3"); + cmd.arg("3"); + cmd.assert() + .code(predicate::eq(0)) + .success() + .stderr(predicate::str::is_empty()) + .stdout(predicate::str::is_empty()); + + Ok(()) + } + + #[test] + fn cmp_skip_suffix_parsing() -> Result<(), Box> { + let tmp_dir = tempdir()?; + + let a_path = tmp_dir.path().join("a"); + let mut a = File::create(&a_path).unwrap(); + write!(a, "{}c\n", "a".repeat(1024)).unwrap(); + a.flush().unwrap(); + + let b_path = tmp_dir.path().join("b"); + let mut b = File::create(&b_path).unwrap(); + write!(b, "{}c\n", "b".repeat(1024)).unwrap(); + b.flush().unwrap(); + + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("cmp"); + cmd.arg("--ignore-initial=1K"); + cmd.arg(&a_path).arg(&b_path); + cmd.assert() + .code(predicate::eq(0)) + .success() + .stderr(predicate::str::is_empty()) + .stdout(predicate::str::is_empty()); + + Ok(()) + } + + #[test] + fn cmp_skip() -> Result<(), Box> { + let tmp_dir = tempdir()?; + + let a_path = tmp_dir.path().join("a"); + let mut a = File::create(&a_path).unwrap(); + a.write_all(b"abc efg ijkl\n").unwrap(); + + let b_path = tmp_dir.path().join("b"); + let mut b = File::create(&b_path).unwrap(); + b.write_all(b"abcdefghijkl\n").unwrap(); + + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("cmp"); + cmd.arg("-l"); + cmd.arg("-b"); + cmd.arg("-i"); + cmd.arg("8"); + cmd.arg(&a_path).arg(&b_path); + cmd.assert() + .code(predicate::eq(0)) + .success() + .stderr(predicate::str::is_empty()) + .stdout(predicate::str::is_empty()); + + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("cmp"); + cmd.arg("-b"); + cmd.arg("-i"); + cmd.arg("4"); + cmd.arg(&a_path).arg(&b_path); + cmd.assert() + .code(predicate::eq(1)) + .failure() + .stderr(predicate::str::is_empty()) + .stdout(predicate::str::ends_with( + " differ: byte 4, line 1 is 40 150 h\n", + )); + + Ok(()) + } + + #[test] + fn cmp_binary() -> Result<(), Box> { + let tmp_dir = tempdir()?; + + let mut bytes = vec![0, 15, 31, 32, 33, 40, 64, 126, 127, 128, 129, 200, 254, 255]; + + let a_path = tmp_dir.path().join("a"); + let mut a = File::create(&a_path).unwrap(); + a.write_all(&bytes).unwrap(); + + bytes.reverse(); + + let b_path = tmp_dir.path().join("b"); + let mut b = File::create(&b_path).unwrap(); + b.write_all(&bytes).unwrap(); -#[test] -fn compare_file_to_directory() -> Result<(), Box> { - let tmp_dir = tempdir()?; - - let directory = tmp_dir.path().join("d"); - let _ = std::fs::create_dir(&directory); - - let a_path = tmp_dir.path().join("a"); - let mut a = File::create(&a_path).unwrap(); - a.write_all(b"a\n").unwrap(); - - let da_path = directory.join("a"); - let mut da = File::create(&da_path).unwrap(); - da.write_all(b"da\n").unwrap(); - - let mut cmd = Command::cargo_bin("diffutils")?; - cmd.arg("diff"); - cmd.arg("-u").arg(&directory).arg(&a_path); - cmd.assert().code(predicate::eq(1)).failure(); - - let output = cmd.output().unwrap().stdout; - assert_diff_eq!( - output, - format!( - "--- {}\tTIMESTAMP\n+++ {}\tTIMESTAMP\n@@ -1 +1 @@\n-da\n+a\n", - da_path.display(), - a_path.display() - ) - ); - - let mut cmd = Command::cargo_bin("diffutils")?; - cmd.arg("diff"); - cmd.arg("-u").arg(&a_path).arg(&directory); - cmd.assert().code(predicate::eq(1)).failure(); - - let output = cmd.output().unwrap().stdout; - assert_diff_eq!( - output, - format!( - "--- {}\tTIMESTAMP\n+++ {}\tTIMESTAMP\n@@ -1 +1 @@\n-a\n+da\n", - a_path.display(), - da_path.display() - ) - ); - - Ok(()) + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("cmp"); + cmd.arg("-l"); + cmd.arg("-b"); + cmd.arg(&a_path).arg(&b_path); + cmd.assert() + .code(predicate::eq(1)) + .failure() + .stdout(predicate::eq(concat!( + " 1 0 ^@ 377 M-^?\n", + " 2 17 ^O 376 M-~\n", + " 3 37 ^_ 310 M-H\n", + " 4 40 201 M-^A\n", + " 5 41 ! 200 M-^@\n", + " 6 50 ( 177 ^?\n", + " 7 100 @ 176 ~\n", + " 8 176 ~ 100 @\n", + " 9 177 ^? 50 (\n", + "10 200 M-^@ 41 !\n", + "11 201 M-^A 40 \n", + "12 310 M-H 37 ^_\n", + "13 376 M-~ 17 ^O\n", + "14 377 M-^? 0 ^@\n" + ))); + + Ok(()) + } + + #[test] + #[cfg(not(windows))] + fn cmp_fast_paths() -> Result<(), Box> { + let tmp_dir = tempdir()?; + + // This test mimics one found in the GNU cmp test suite. It is used for + // validating the /dev/null optimization. + let a_path = tmp_dir.path().join("a"); + let a = File::create(&a_path).unwrap(); + a.set_len(14 * 1024 * 1024 * 1024 * 1024).unwrap(); + + let b_path = tmp_dir.path().join("b"); + let b = File::create(&b_path).unwrap(); + b.set_len(15 * 1024 * 1024 * 1024 * 1024).unwrap(); + + let dev_null = OpenOptions::new().write(true).open("/dev/null").unwrap(); + + let mut child = std::process::Command::new(assert_cmd::cargo::cargo_bin("diffutils")) + .arg("cmp") + .arg(&a_path) + .arg(&b_path) + .stdout(dev_null) + .spawn() + .unwrap(); + + std::thread::sleep(std::time::Duration::from_millis(100)); + + assert_eq!(child.try_wait().unwrap().unwrap().code(), Some(1)); + + // Two stdins should be equal + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("cmp"); + cmd.arg("-"); + cmd.arg("-"); + cmd.assert() + .code(predicate::eq(0)) + .success() + .stdout(predicate::str::is_empty()) + .stderr(predicate::str::is_empty()); + + // Files with longer than block size equal segments should still report + // the correct line number for the difference. Assumes 8KB block size (see + // https://github.com/rust-lang/rust/blob/master/library/std/src/sys_common/io.rs), + // create a 24KB equality. + let mut bytes = " ".repeat(4095); + bytes.push('\n'); + bytes.push_str(&" ".repeat(4096)); + + let bytes = bytes.repeat(3); + let bytes = bytes.as_bytes(); + + let a_path = tmp_dir.path().join("a"); + let mut a = File::create(&a_path).unwrap(); + a.write_all(&bytes).unwrap(); + a.write_all(b"A").unwrap(); + + let b_path = tmp_dir.path().join("b"); + let mut b = File::create(&b_path).unwrap(); + b.write_all(&bytes).unwrap(); + b.write_all(b"B").unwrap(); + + let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("cmp"); + cmd.arg(&a_path).arg(&b_path); + cmd.assert() + .code(predicate::eq(1)) + .failure() + .stdout(predicate::str::ends_with(" differ: byte 24577, line 4\n")); + + Ok(()) + } } diff --git a/tests/run-upstream-testsuite.sh b/tests/run-upstream-testsuite.sh index cb59834..f75b0b3 100755 --- a/tests/run-upstream-testsuite.sh +++ b/tests/run-upstream-testsuite.sh @@ -21,7 +21,7 @@ # (e.g. 'dev' or 'test'). # Unless overridden by the $TESTS environment variable, all tests in the test # suite will be run. Tests targeting a command that is not yet implemented -# (e.g. cmp, diff3 or sdiff) are skipped. +# (e.g. diff3 or sdiff) are skipped. scriptpath=$(dirname "$(readlink -f "$0")") rev=$(git rev-parse HEAD) @@ -57,6 +57,7 @@ upstreamrev=$(git rev-parse HEAD) mkdir src cd src ln -s "$binary" diff +ln -s "$binary" cmp cd ../tests if [[ -n "$TESTS" ]] @@ -82,9 +83,9 @@ for test in $tests do result="FAIL" url="$urlroot$test?id=$upstreamrev" - # Run only the tests that invoke `diff`, + # Run only the tests that invoke `diff` or `cmp`, # because other binaries aren't implemented yet - if ! grep -E -s -q "(cmp|diff3|sdiff)" "$test" + if ! grep -E -s -q "(diff3|sdiff)" "$test" then sh "$test" 1> stdout.txt 2> stderr.txt && result="PASS" || exitcode=1 json+="{\"test\":\"$test\",\"result\":\"$result\"," From 2e681301b4c652fd8eab304570eaedc3f7ffd663 Mon Sep 17 00:00:00 2001 From: Gustavo Noronha Silva Date: Tue, 24 Sep 2024 22:15:12 -0300 Subject: [PATCH 129/194] cmp: avoid using advanced rust formatting for -l Octal conversion and simple integer to string both show up in profiling. This change improves comparing ~36M completely different files wth both -l and -b by ~11-13%. --- Cargo.lock | 7 +++++++ Cargo.toml | 1 + src/cmp.rs | 40 ++++++++++++++++++++++++++++++++-------- 3 files changed, 40 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 24fc712..3291450 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -128,6 +128,7 @@ dependencies = [ "assert_cmd", "chrono", "diff", + "itoa", "predicates", "pretty_assertions", "regex", @@ -190,6 +191,12 @@ dependencies = [ "cc", ] +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + [[package]] name = "js-sys" version = "0.3.69" diff --git a/Cargo.toml b/Cargo.toml index 477467c..6fa1a3c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ path = "src/main.rs" [dependencies] chrono = "0.4.38" diff = "0.1.13" +itoa = "1.0.11" regex = "1.10.4" same-file = "1.0.6" unicode-width = "0.2.0" diff --git a/src/cmp.rs b/src/cmp.rs index 29b8775..a337c12 100644 --- a/src/cmp.rs +++ b/src/cmp.rs @@ -483,6 +483,24 @@ fn is_ascii_printable(byte: u8) -> bool { c.is_ascii() && !c.is_ascii_control() } +#[inline] +fn format_octal(byte: u8, buf: &mut [u8; 3]) -> &str { + *buf = [b' ', b' ', b'0']; + + let mut num = byte; + let mut idx = 2; // Start at the last position in the buffer + + // Generate octal digits + while num > 0 { + buf[idx] = b'0' + num % 8; + num /= 8; + idx = idx.saturating_sub(1); + } + + // SAFETY: the operations we do above always land within ascii range. + unsafe { std::str::from_utf8_unchecked(&buf[..]) } +} + #[inline] fn format_byte(byte: u8) -> String { let mut byte = byte; @@ -520,15 +538,20 @@ fn report_verbose_diffs(diffs: Vec<(usize, u8, u8)>, params: &Params) -> Result< // Obtain the width of the first column from the last byte offset. let width = format!("{}", offset).len(); + let mut at_byte_buf = itoa::Buffer::new(); + let mut from_oct = [0u8; 3]; // for octal conversions + let mut to_oct = [0u8; 3]; + if params.print_bytes { for (at_byte, from_byte, to_byte) in diffs { + let at_byte_str = at_byte_buf.format(at_byte); writeln!( stdout, - "{:>width$} {:>3o} {:4} {:>3o} {}", - at_byte, - from_byte, + "{:>width$} {} {:4} {} {}", + at_byte_str, + format_octal(from_byte, &mut from_oct), format_byte(from_byte), - to_byte, + format_octal(to_byte, &mut to_oct), format_byte(to_byte), ) .map_err(|e| { @@ -540,12 +563,13 @@ fn report_verbose_diffs(diffs: Vec<(usize, u8, u8)>, params: &Params) -> Result< } } else { for (at_byte, from_byte, to_byte) in diffs { + let at_byte_str = at_byte_buf.format(at_byte); writeln!( stdout, - "{:>width$} {:>3o} {:>3o}", - at_byte, - from_byte, - to_byte, + "{:>width$} {} {}", + at_byte_str, + format_octal(from_byte, &mut from_oct), + format_octal(to_byte, &mut to_oct), width = width ) .map_err(|e| { From fac8dab1823e8b7c7cfe5c468528dc0842a5daab Mon Sep 17 00:00:00 2001 From: Gustavo Noronha Silva Date: Tue, 24 Sep 2024 22:31:35 -0300 Subject: [PATCH 130/194] cmp: completely avoid Rust fmt in verbose mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This makes the code less readable, but gets us a massive improvement to performance. Comparing ~36M completely different files now takes ~40% of the time. Compared to GNU cmp, we now run the same comparison in ~26% of the time. This also improves comparing binary files. A comparison of chromium and libxul now takes ~60% of the time. We also beat GNU cmpi by about the same margin. Before: > hyperfine --warmup 1 -i --output=pipe \ '../target/release/diffutils cmp -l huge huge.3' Benchmark 1: ../target/release/diffutils cmp -l huge huge.3 Time (mean ± σ): 2.000 s ± 0.016 s [User: 1.603 s, System: 0.392 s] Range (min … max): 1.989 s … 2.043 s 10 runs Warning: Ignoring non-zero exit code. > hyperfine --warmup 1 -i --output=pipe \ '../target/release/diffutils cmp -l -b \ /usr/lib64/chromium-browser/chromium-browser \ /usr/lib64/firefox/libxul.so' Benchmark 1: ../target/release/diffutils cmp -l -b /usr/lib64/chromium-browser/chromium-browser /usr/lib64/firefox/libxul.so Time (mean ± σ): 24.704 s ± 0.162 s [User: 21.948 s, System: 2.700 s] Range (min … max): 24.359 s … 24.889 s 10 runs Warning: Ignoring non-zero exit code. After: > hyperfine --warmup 1 -i --output=pipe \ '../target/release/diffutils cmp -l huge huge.3' Benchmark 1: ../target/release/diffutils cmp -l huge huge.3 Time (mean ± σ): 849.5 ms ± 6.2 ms [User: 538.3 ms, System: 306.8 ms] Range (min … max): 839.4 ms … 857.7 ms 10 runs Warning: Ignoring non-zero exit code. > hyperfine --warmup 1 -i --output=pipe \ '../target/release/diffutils cmp -l -b \ /usr/lib64/chromium-browser/chromium-browser \ /usr/lib64/firefox/libxul.so' Benchmark 1: ../target/release/diffutils cmp -l -b /usr/lib64/chromium-browser/chromium-browser /usr/lib64/firefox/libxul.so Time (mean ± σ): 14.646 s ± 0.040 s [User: 12.328 s, System: 2.286 s] Range (min … max): 14.585 s … 14.702 s 10 runs Warning: Ignoring non-zero exit code. --- src/cmp.rs | 84 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 65 insertions(+), 19 deletions(-) diff --git a/src/cmp.rs b/src/cmp.rs index a337c12..1d9ca9e 100644 --- a/src/cmp.rs +++ b/src/cmp.rs @@ -530,6 +530,9 @@ fn format_byte(byte: u8) -> String { unsafe { String::from_utf8_unchecked(quoted) } } +// This function has been optimized to not use the Rust fmt system, which +// leads to a massive speed up when processing large files: cuts the time +// for comparing 2 ~36MB completely different files in half on an M1 Max. fn report_verbose_diffs(diffs: Vec<(usize, u8, u8)>, params: &Params) -> Result<(), String> { assert!(!params.quiet); @@ -542,19 +545,49 @@ fn report_verbose_diffs(diffs: Vec<(usize, u8, u8)>, params: &Params) -> Result< let mut from_oct = [0u8; 3]; // for octal conversions let mut to_oct = [0u8; 3]; + // Capacity calc: at_byte width + 2 x 3-byte octal numbers + 4-byte value + up to 2 byte value + 4 spaces + let mut output = Vec::::with_capacity(width + 3 * 2 + 4 + 2 + 4); + if params.print_bytes { for (at_byte, from_byte, to_byte) in diffs { + output.clear(); + + // "{:>width$} {:>3o} {:4} {:>3o} {}", let at_byte_str = at_byte_buf.format(at_byte); - writeln!( - stdout, - "{:>width$} {} {:4} {} {}", - at_byte_str, - format_octal(from_byte, &mut from_oct), - format_byte(from_byte), - format_octal(to_byte, &mut to_oct), - format_byte(to_byte), - ) - .map_err(|e| { + let at_byte_padding = width - at_byte_str.len(); + + for _ in 0..at_byte_padding { + output.push(b' ') + } + + output.extend_from_slice(at_byte_str.as_bytes()); + + output.push(b' '); + + output.extend_from_slice(format_octal(from_byte, &mut from_oct).as_bytes()); + + output.push(b' '); + + let from_byte_str = format_byte(from_byte); + let from_byte_padding = 4 - from_byte_str.len(); + + output.extend_from_slice(from_byte_str.as_bytes()); + + for _ in 0..from_byte_padding { + output.push(b' ') + } + + output.push(b' '); + + output.extend_from_slice(format_octal(to_byte, &mut to_oct).as_bytes()); + + output.push(b' '); + + output.extend_from_slice(format_byte(to_byte).as_bytes()); + + output.push(b'\n'); + + stdout.write_all(output.as_slice()).map_err(|e| { format!( "{}: error printing output: {e}", params.executable.to_string_lossy() @@ -563,16 +596,29 @@ fn report_verbose_diffs(diffs: Vec<(usize, u8, u8)>, params: &Params) -> Result< } } else { for (at_byte, from_byte, to_byte) in diffs { + output.clear(); + + // "{:>width$} {:>3o} {:>3o}" let at_byte_str = at_byte_buf.format(at_byte); - writeln!( - stdout, - "{:>width$} {} {}", - at_byte_str, - format_octal(from_byte, &mut from_oct), - format_octal(to_byte, &mut to_oct), - width = width - ) - .map_err(|e| { + let at_byte_padding = width - at_byte_str.len(); + + for _ in 0..at_byte_padding { + output.push(b' ') + } + + output.extend_from_slice(at_byte_str.as_bytes()); + + output.push(b' '); + + output.extend_from_slice(format_octal(from_byte, &mut from_oct).as_bytes()); + + output.push(b' '); + + output.extend_from_slice(format_octal(to_byte, &mut to_oct).as_bytes()); + + output.push(b'\n'); + + stdout.write_all(output.as_slice()).map_err(|e| { format!( "{}: error printing output: {e}", params.executable.to_string_lossy() From 0bf04b439536931c4052cc6a48c1cf39e7abcc01 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Wed, 2 Oct 2024 14:00:43 +0200 Subject: [PATCH 131/194] README.md: be explicit with the list of tools (#99) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 552df09..fae06d6 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![CodeCov](https://codecov.io/gh/uutils/diffutils/branch/main/graph/badge.svg)](https://codecov.io/gh/uutils/diffutils) -The goal of this package is to be a drop-in replacement for the [diffutils commands](https://www.gnu.org/software/diffutils/) in Rust. +The goal of this package is to be a drop-in replacement for the [diffutils commands](https://www.gnu.org/software/diffutils/) (diff, cmp, diff3, sdiff) in Rust. Based on the incomplete diff generator in https://github.com/rust-lang/rust/blob/master/src/tools/compiletest/src/runtest.rs, and made to be compatible with GNU's diff and patch tools. From a31626260389160fd3d0e87906af8d1ab2358e31 Mon Sep 17 00:00:00 2001 From: Gustavo Noronha Silva Date: Sat, 5 Oct 2024 08:09:27 -0300 Subject: [PATCH 132/194] cmp: print verbose diffs as we find them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before this change, we would first find all changes so we could obtain the largest offset we will report and use that to set up the padding. Now we use the file sizes to estimate the largest possible offset. Not only does this allow us to print earlier, reduces memory usage, as we do not store diffs to report later, but it also fixes a case in which our output was different to GNU cmp's - because it also seems to estimate based on size. Memory usage drops by a factor of 1000(!), without losing performance while comparing 2 binaries of hundreds of MBs: Before: Maximum resident set size (kbytes): 2489260 Benchmark 1: ../target/release/diffutils \ cmp -l -b /usr/lib64/chromium-browser/chromium-browser /usr/lib64/firefox/libxul.so Time (mean ± σ): 14.466 s ± 0.166 s [User: 12.367 s, System: 2.012 s] Range (min … max): 14.350 s … 14.914 s 10 runs After: Maximum resident set size (kbytes): 2636 Benchmark 1: ../target/release/diffutils \ cmp -l -b /usr/lib64/chromium-browser/chromium-browser /usr/lib64/firefox/libxul.so Time (mean ± σ): 13.724 s ± 0.038 s [User: 12.263 s, System: 1.372 s] Range (min … max): 13.667 s … 13.793 s 10 runs --- src/cmp.rs | 190 +++++++++++++++++++++---------------------- tests/integration.rs | 2 +- 2 files changed, 93 insertions(+), 99 deletions(-) diff --git a/src/cmp.rs b/src/cmp.rs index 1d9ca9e..c0fc397 100644 --- a/src/cmp.rs +++ b/src/cmp.rs @@ -9,7 +9,7 @@ use std::ffi::OsString; use std::io::{BufRead, BufReader, BufWriter, Read, Write}; use std::iter::Peekable; use std::process::ExitCode; -use std::{fs, io}; +use std::{cmp, fs, io}; #[cfg(not(target_os = "windows"))] use std::os::fd::{AsRawFd, FromRawFd}; @@ -320,10 +320,35 @@ pub fn cmp(params: &Params) -> Result { let mut from = prepare_reader(¶ms.from, ¶ms.skip_a, params)?; let mut to = prepare_reader(¶ms.to, ¶ms.skip_b, params)?; + let mut offset_width = params.max_bytes.unwrap_or(usize::MAX); + + if let (Ok(a_meta), Ok(b_meta)) = (fs::metadata(¶ms.from), fs::metadata(¶ms.to)) { + #[cfg(not(target_os = "windows"))] + let (a_size, b_size) = (a_meta.size(), b_meta.size()); + + #[cfg(target_os = "windows")] + let (a_size, b_size) = (a_meta.file_size(), b_meta.file_size()); + + // If the files have different sizes, we already know they are not identical. If we have not + // been asked to show even the first difference, we can quit early. + if params.quiet && a_size != b_size { + return Ok(Cmp::Different); + } + + let smaller = cmp::min(a_size, b_size) as usize; + offset_width = cmp::min(smaller, offset_width); + } + + let offset_width = 1 + offset_width.checked_ilog10().unwrap_or(1) as usize; + + // Capacity calc: at_byte width + 2 x 3-byte octal numbers + 2 x 4-byte value + 4 spaces + let mut output = Vec::::with_capacity(offset_width + 3 * 2 + 4 * 2 + 4); + let mut at_byte = 1; let mut at_line = 1; let mut start_of_line = true; - let mut verbose_diffs = vec![]; + let mut stdout = BufWriter::new(io::stdout().lock()); + let mut compare = Cmp::Equal; loop { // Fill up our buffers. let from_buf = match from.fill_buf() { @@ -360,10 +385,6 @@ pub fn cmp(params: &Params) -> Result { ¶ms.to.to_string_lossy() }; - if params.verbose { - report_verbose_diffs(verbose_diffs, params)?; - } - report_eof(at_byte, at_line, start_of_line, eof_on, params); return Ok(Cmp::Different); } @@ -395,8 +416,24 @@ pub fn cmp(params: &Params) -> Result { // first one runs out. for (&from_byte, &to_byte) in from_buf.iter().zip(to_buf.iter()) { if from_byte != to_byte { + compare = Cmp::Different; + if params.verbose { - verbose_diffs.push((at_byte, from_byte, to_byte)); + format_verbose_difference( + from_byte, + to_byte, + at_byte, + offset_width, + &mut output, + params, + )?; + stdout.write_all(output.as_slice()).map_err(|e| { + format!( + "{}: error printing output: {e}", + params.executable.to_string_lossy() + ) + })?; + output.clear(); } else { report_difference(from_byte, to_byte, at_byte, at_line, params); return Ok(Cmp::Different); @@ -422,12 +459,7 @@ pub fn cmp(params: &Params) -> Result { to.consume(consumed); } - if params.verbose && !verbose_diffs.is_empty() { - report_verbose_diffs(verbose_diffs, params)?; - return Ok(Cmp::Different); - } - - Ok(Cmp::Equal) + Ok(compare) } // Exit codes are documented at @@ -450,21 +482,6 @@ pub fn main(opts: Peekable) -> ExitCode { return ExitCode::SUCCESS; } - // If the files have different sizes, we already know they are not identical. If we have not - // been asked to show even the first difference, we can quit early. - if params.quiet { - if let (Ok(a_meta), Ok(b_meta)) = (fs::metadata(¶ms.from), fs::metadata(¶ms.to)) { - #[cfg(not(target_os = "windows"))] - if a_meta.size() != b_meta.size() { - return ExitCode::from(1); - } - #[cfg(target_os = "windows")] - if a_meta.file_size() != b_meta.file_size() { - return ExitCode::from(1); - } - } - } - match cmp(¶ms) { Ok(Cmp::Equal) => ExitCode::SUCCESS, Ok(Cmp::Different) => ExitCode::from(1), @@ -533,99 +550,76 @@ fn format_byte(byte: u8) -> String { // This function has been optimized to not use the Rust fmt system, which // leads to a massive speed up when processing large files: cuts the time // for comparing 2 ~36MB completely different files in half on an M1 Max. -fn report_verbose_diffs(diffs: Vec<(usize, u8, u8)>, params: &Params) -> Result<(), String> { +#[inline] +fn format_verbose_difference( + from_byte: u8, + to_byte: u8, + at_byte: usize, + offset_width: usize, + output: &mut Vec, + params: &Params, +) -> Result<(), String> { assert!(!params.quiet); - let mut stdout = BufWriter::new(io::stdout().lock()); - if let Some((offset, _, _)) = diffs.last() { - // Obtain the width of the first column from the last byte offset. - let width = format!("{}", offset).len(); - - let mut at_byte_buf = itoa::Buffer::new(); - let mut from_oct = [0u8; 3]; // for octal conversions - let mut to_oct = [0u8; 3]; - - // Capacity calc: at_byte width + 2 x 3-byte octal numbers + 4-byte value + up to 2 byte value + 4 spaces - let mut output = Vec::::with_capacity(width + 3 * 2 + 4 + 2 + 4); - - if params.print_bytes { - for (at_byte, from_byte, to_byte) in diffs { - output.clear(); - - // "{:>width$} {:>3o} {:4} {:>3o} {}", - let at_byte_str = at_byte_buf.format(at_byte); - let at_byte_padding = width - at_byte_str.len(); + let mut at_byte_buf = itoa::Buffer::new(); + let mut from_oct = [0u8; 3]; // for octal conversions + let mut to_oct = [0u8; 3]; - for _ in 0..at_byte_padding { - output.push(b' ') - } - - output.extend_from_slice(at_byte_str.as_bytes()); - - output.push(b' '); + if params.print_bytes { + // "{:>width$} {:>3o} {:4} {:>3o} {}", + let at_byte_str = at_byte_buf.format(at_byte); + let at_byte_padding = offset_width.saturating_sub(at_byte_str.len()); - output.extend_from_slice(format_octal(from_byte, &mut from_oct).as_bytes()); + for _ in 0..at_byte_padding { + output.push(b' ') + } - output.push(b' '); + output.extend_from_slice(at_byte_str.as_bytes()); - let from_byte_str = format_byte(from_byte); - let from_byte_padding = 4 - from_byte_str.len(); + output.push(b' '); - output.extend_from_slice(from_byte_str.as_bytes()); + output.extend_from_slice(format_octal(from_byte, &mut from_oct).as_bytes()); - for _ in 0..from_byte_padding { - output.push(b' ') - } + output.push(b' '); - output.push(b' '); + let from_byte_str = format_byte(from_byte); + let from_byte_padding = 4 - from_byte_str.len(); - output.extend_from_slice(format_octal(to_byte, &mut to_oct).as_bytes()); + output.extend_from_slice(from_byte_str.as_bytes()); - output.push(b' '); + for _ in 0..from_byte_padding { + output.push(b' ') + } - output.extend_from_slice(format_byte(to_byte).as_bytes()); + output.push(b' '); - output.push(b'\n'); + output.extend_from_slice(format_octal(to_byte, &mut to_oct).as_bytes()); - stdout.write_all(output.as_slice()).map_err(|e| { - format!( - "{}: error printing output: {e}", - params.executable.to_string_lossy() - ) - })?; - } - } else { - for (at_byte, from_byte, to_byte) in diffs { - output.clear(); + output.push(b' '); - // "{:>width$} {:>3o} {:>3o}" - let at_byte_str = at_byte_buf.format(at_byte); - let at_byte_padding = width - at_byte_str.len(); + output.extend_from_slice(format_byte(to_byte).as_bytes()); - for _ in 0..at_byte_padding { - output.push(b' ') - } + output.push(b'\n'); + } else { + // "{:>width$} {:>3o} {:>3o}" + let at_byte_str = at_byte_buf.format(at_byte); + let at_byte_padding = offset_width - at_byte_str.len(); - output.extend_from_slice(at_byte_str.as_bytes()); + for _ in 0..at_byte_padding { + output.push(b' ') + } - output.push(b' '); + output.extend_from_slice(at_byte_str.as_bytes()); - output.extend_from_slice(format_octal(from_byte, &mut from_oct).as_bytes()); + output.push(b' '); - output.push(b' '); + output.extend_from_slice(format_octal(from_byte, &mut from_oct).as_bytes()); - output.extend_from_slice(format_octal(to_byte, &mut to_oct).as_bytes()); + output.push(b' '); - output.push(b'\n'); + output.extend_from_slice(format_octal(to_byte, &mut to_oct).as_bytes()); - stdout.write_all(output.as_slice()).map_err(|e| { - format!( - "{}: error printing output: {e}", - params.executable.to_string_lossy() - ) - })?; - } - } + output.push(b'\n'); } Ok(()) diff --git a/tests/integration.rs b/tests/integration.rs index 4cff8ff..5619b1a 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -616,7 +616,7 @@ mod cmp { .code(predicate::eq(1)) .failure() .stderr(predicate::str::is_empty()) - .stdout(predicate::eq("4 40 144 d\n8 40 150 h\n")); + .stdout(predicate::eq(" 4 40 144 d\n 8 40 150 h\n")); Ok(()) } From c70cc1921c7739a406b052b9d2af8643343e7313 Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Fri, 18 Oct 2024 09:08:07 +0200 Subject: [PATCH 133/194] Fix warnings from write_with_newline lint --- tests/integration.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration.rs b/tests/integration.rs index 5619b1a..c2ef299 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -691,12 +691,12 @@ mod cmp { let a_path = tmp_dir.path().join("a"); let mut a = File::create(&a_path).unwrap(); - write!(a, "{}c\n", "a".repeat(1024)).unwrap(); + writeln!(a, "{}c", "a".repeat(1024)).unwrap(); a.flush().unwrap(); let b_path = tmp_dir.path().join("b"); let mut b = File::create(&b_path).unwrap(); - write!(b, "{}c\n", "b".repeat(1024)).unwrap(); + writeln!(b, "{}c", "b".repeat(1024)).unwrap(); b.flush().unwrap(); let mut cmd = Command::cargo_bin("diffutils")?; From 1910cbfe5866649fdf121ae3966db82664e7a2a3 Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Fri, 18 Oct 2024 09:10:03 +0200 Subject: [PATCH 134/194] Fix warnings from needless_borrow lint --- tests/integration.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration.rs b/tests/integration.rs index c2ef299..c11726e 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -851,12 +851,12 @@ mod cmp { let a_path = tmp_dir.path().join("a"); let mut a = File::create(&a_path).unwrap(); - a.write_all(&bytes).unwrap(); + a.write_all(bytes).unwrap(); a.write_all(b"A").unwrap(); let b_path = tmp_dir.path().join("b"); let mut b = File::create(&b_path).unwrap(); - b.write_all(&bytes).unwrap(); + b.write_all(bytes).unwrap(); b.write_all(b"B").unwrap(); let mut cmd = Command::cargo_bin("diffutils")?; From 3de1930bbe6fc705375bc8399855a7bd7c6bc961 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 24 Oct 2024 15:05:12 +0000 Subject: [PATCH 135/194] fix(deps): update rust crate regex to v1.11.1 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fe461de..3b9e4da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -311,9 +311,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", From 14b062251fbfbb6138a84b271ec8a293511cc92a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 8 Nov 2024 03:47:37 +0000 Subject: [PATCH 136/194] chore(deps): update rust crate tempfile to v3.14.0 --- Cargo.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3b9e4da..bebb053 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -208,9 +208,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.159" +version = "0.2.162" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" +checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" [[package]] name = "linux-raw-sys" @@ -340,9 +340,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rustix" -version = "0.38.37" +version = "0.38.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +checksum = "375116bee2be9ed569afe2154ea6a99dfdffd257f533f187498c2a8f5feaf4ee" dependencies = [ "bitflags", "errno", @@ -393,9 +393,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.13.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" +checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" dependencies = [ "cfg-if", "fastrand", From 4f2f86902118f7f74f3212f8ab1d32b85b9ff5f0 Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Fri, 8 Nov 2024 09:06:33 +0100 Subject: [PATCH 137/194] Fix "unused import" warning on Windows --- tests/integration.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/integration.rs b/tests/integration.rs index c11726e..cfbf529 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -5,7 +5,9 @@ use assert_cmd::cmd::Command; use predicates::prelude::*; -use std::fs::{File, OpenOptions}; +use std::fs::File; +#[cfg(not(windows))] +use std::fs::OpenOptions; use std::io::Write; use tempfile::{tempdir, NamedTempFile}; From 90bed40046f6e261532763a3dd4967a1a032789e Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Fri, 8 Nov 2024 09:25:17 +0100 Subject: [PATCH 138/194] ci: remove CARGO_FEATURES_OPTION --- .github/workflows/ci.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6f0e5ca..732fa2d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -102,8 +102,6 @@ jobs: if [ -n "${{ matrix.job.toolchain }}" ]; then TOOLCHAIN="${{ matrix.job.toolchain }}" ; fi outputs TOOLCHAIN # target-specific options - # * CARGO_FEATURES_OPTION - CARGO_FEATURES_OPTION='--all -- --check' ; ## default to '--all-features' for code coverage # * CODECOV_FLAGS CODECOV_FLAGS=$( echo "${{ matrix.job.os }}" | sed 's/[^[:alnum:]]/_/g' ) outputs CODECOV_FLAGS @@ -119,7 +117,7 @@ jobs: if: runner.os == 'Windows' run: echo "C:\Program Files\Git\usr\bin" >> $env:GITHUB_PATH - name: Test - run: cargo test ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} --no-fail-fast + run: cargo test --all-features --no-fail-fast env: CARGO_INCREMENTAL: "0" RUSTC_WRAPPER: "" From 39e092488b8b0e88ac867f8e73d20ac4f5404091 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 16 Nov 2024 07:49:05 +0000 Subject: [PATCH 139/194] chore(deps): update codecov/codecov-action action to v5 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 732fa2d..cb118ac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -156,7 +156,7 @@ jobs: grcov . --output-type lcov --output-path "${COVERAGE_REPORT_FILE}" --binary-path "${COVERAGE_REPORT_DIR}" --branch echo "report=${COVERAGE_REPORT_FILE}" >> $GITHUB_OUTPUT - name: Upload coverage results (to Codecov.io) - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} file: ${{ steps.coverage.outputs.report }} From 4ff2d6b1829a11361945a581e644b8e68230e929 Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Sat, 16 Nov 2024 09:42:50 +0100 Subject: [PATCH 140/194] ci: fix deprecated codecov argument --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb118ac..97434f0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -159,7 +159,7 @@ jobs: uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} - file: ${{ steps.coverage.outputs.report }} + files: ${{ steps.coverage.outputs.report }} ## flags: IntegrationTests, UnitTests, ${{ steps.vars.outputs.CODECOV_FLAGS }} flags: ${{ steps.vars.outputs.CODECOV_FLAGS }} name: codecov-umbrella From 44c195c0b22f653f9f3d0ad89f789df06b35dbb5 Mon Sep 17 00:00:00 2001 From: Olivier Tilloy Date: Tue, 4 Mar 2025 10:51:09 +0100 Subject: [PATCH 141/194] ci: make sure gpatch is actually being used for tests on MacOS --- .github/workflows/ci.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 97434f0..8343add 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,9 @@ jobs: - uses: dtolnay/rust-toolchain@stable - name: install GNU patch on MacOS if: runner.os == 'macOS' - run: brew install gpatch + run: | + brew install gpatch + echo "/opt/homebrew/opt/gpatch/libexec/gnubin" >> "$GITHUB_PATH" - name: set up PATH on Windows # Needed to use GNU's patch.exe instead of Strawberry Perl patch if: runner.os == 'Windows' @@ -111,7 +113,9 @@ jobs: - run: rustup component add llvm-tools-preview - name: install GNU patch on MacOS if: runner.os == 'macOS' - run: brew install gpatch + run: | + brew install gpatch + echo "/opt/homebrew/opt/gpatch/libexec/gnubin" >> "$GITHUB_PATH" - name: set up PATH on Windows # Needed to use GNU's patch.exe instead of Strawberry Perl patch if: runner.os == 'Windows' From f9553984f40853418f4c13d96473553895c18620 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 4 Mar 2025 10:09:06 +0000 Subject: [PATCH 142/194] fix(deps): update rust crate itoa to v1.0.15 --- Cargo.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bebb053..502f487 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "aho-corasick" @@ -193,9 +193,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "js-sys" From bbdfa1b765ded59f20df3b64fea83872fe97c22b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 4 Mar 2025 10:29:12 +0000 Subject: [PATCH 143/194] fix(deps): update rust crate chrono to v0.4.40 --- Cargo.lock | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 502f487..85b6dd6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -91,16 +91,16 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.38" +version = "0.4.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "wasm-bindgen", - "windows-targets", + "windows-link", ] [[package]] @@ -525,6 +525,12 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-link" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" + [[package]] name = "windows-sys" version = "0.52.0" From ba1cac3c20fda9baa86a38d2f638f742b6295001 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 4 Mar 2025 10:40:57 +0000 Subject: [PATCH 144/194] chore(deps): update rust crate predicates to v3.1.3 --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 85b6dd6..744c848 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -161,9 +161,9 @@ checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" [[package]] name = "float-cmp" -version = "0.9.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" dependencies = [ "num-traits", ] @@ -253,9 +253,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "predicates" -version = "3.1.2" +version = "3.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e9086cc7640c29a356d1a29fd134380bee9d8f79a17410aa76e7ad295f42c97" +checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" dependencies = [ "anstyle", "difflib", From d573c3ae1d5ab2d7413b9f4f581ebf8ecc45f29b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 4 Mar 2025 10:51:49 +0000 Subject: [PATCH 145/194] chore(deps): update rust crate tempfile to v3.17.1 --- Cargo.lock | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 744c848..23f7a4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -168,6 +168,18 @@ dependencies = [ "num-traits", ] +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi", + "windows-targets", +] + [[package]] name = "iana-time-zone" version = "0.1.60" @@ -393,12 +405,13 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.14.0" +version = "3.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" +checksum = "22e5a0acb1f3f55f65cc4a866c361b2fb2a0ff6366785ae6fbb5f85df07ba230" dependencies = [ "cfg-if", "fastrand", + "getrandom", "once_cell", "rustix", "windows-sys 0.59.0", @@ -431,6 +444,15 @@ dependencies = [ "libc", ] +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasm-bindgen" version = "0.2.92" @@ -613,6 +635,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags", +] + [[package]] name = "yansi" version = "1.0.1" From b53d4f427cf7a1e4a54d4b790298d1d5c1d9a31c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 6 Mar 2025 23:24:15 +0000 Subject: [PATCH 146/194] chore(deps): update rust crate tempfile to v3.18.0 --- Cargo.lock | 35 +++++++++++++---------------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 23f7a4a..205dcf0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -145,12 +145,12 @@ checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" [[package]] name = "errno" -version = "0.3.8" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys", ] [[package]] @@ -220,15 +220,15 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.162" +version = "0.2.170" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" +checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "6db9c683daf087dc577b7506e9695b3d556a9f3849903fa28186283afd6809e9" [[package]] name = "log" @@ -352,15 +352,15 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rustix" -version = "0.38.39" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "375116bee2be9ed569afe2154ea6a99dfdffd257f533f187498c2a8f5feaf4ee" +checksum = "17f8dcd64f141950290e45c99f7710ede1b600297c91818bb30b3667c0f45dc0" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys", ] [[package]] @@ -405,16 +405,16 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.17.1" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e5a0acb1f3f55f65cc4a866c361b2fb2a0ff6366785ae6fbb5f85df07ba230" +checksum = "2c317e0a526ee6120d8dabad239c8dadca62b24b6f168914bbbc8e2fb1f0e567" dependencies = [ "cfg-if", "fastrand", "getrandom", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys", ] [[package]] @@ -553,15 +553,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets", -] - [[package]] name = "windows-sys" version = "0.59.0" From 26ee98dfaaa852d9c49e7337cf0f511591cd4353 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 14 Mar 2025 02:47:05 +0000 Subject: [PATCH 147/194] chore(deps): update rust crate tempfile to v3.19.0 --- Cargo.lock | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 205dcf0..24ee927 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -405,11 +405,10 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.18.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c317e0a526ee6120d8dabad239c8dadca62b24b6f168914bbbc8e2fb1f0e567" +checksum = "488960f40a3fd53d72c2a29a58722561dee8afdd175bd88e3db4677d7b2ba600" dependencies = [ - "cfg-if", "fastrand", "getrandom", "once_cell", From 0d7e4d82aee107f6a29b0b2e744acceae81138d0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 19 Mar 2025 22:53:36 +0000 Subject: [PATCH 148/194] chore(deps): update rust crate tempfile to v3.19.1 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 24ee927..38b7e90 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -405,9 +405,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.19.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488960f40a3fd53d72c2a29a58722561dee8afdd175bd88e3db4677d7b2ba600" +checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" dependencies = [ "fastrand", "getrandom", From 87ccc8e4c2008e11caa9862c1c0708bf6187d6b0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 16 Apr 2025 19:55:52 +0000 Subject: [PATCH 149/194] chore(deps): update rust crate assert_cmd to v2.0.17 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 38b7e90..29a8623 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -34,9 +34,9 @@ checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" [[package]] name = "assert_cmd" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1835b7f27878de8525dc71410b5a31cdcc5f230aed5ba5df968e09c201b23d" +checksum = "2bd389a4b2970a01282ee455294913c0a43724daedcd1a24c3eb0ec1c1320b66" dependencies = [ "anstyle", "bstr", From 199c7f169c12e3d7715f847350c7e6c429eac918 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 29 Apr 2025 15:45:04 +0000 Subject: [PATCH 150/194] fix(deps): update rust crate chrono to v0.4.41 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 29a8623..2c5103a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -91,9 +91,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.40" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ "android-tzdata", "iana-time-zone", From b31df0b5e88d733d001843d0bb25e959ddad2962 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 11 May 2025 22:12:26 +0000 Subject: [PATCH 151/194] chore(deps): update rust crate tempfile to v3.20.0 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2c5103a..6ff8fb5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -405,9 +405,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.19.1" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ "fastrand", "getrandom", From 8105420bb4b665763cca72ec0b9851906fc85ad1 Mon Sep 17 00:00:00 2001 From: Sami Daniel Date: Sat, 19 Apr 2025 01:01:51 -0300 Subject: [PATCH 152/194] Create the side-by-side option (-y) feature for the diff command (Incomplete). - Create the function, in the utils package, limited_string that allows you to truncate a string based on a delimiter (May break the encoding of the character where it was cut) - Create tests for limited_string function - Add support for -y and --side-by-side flags that enables diff output for side-by-side mode - Create implementation of the diff -y (SideBySide) command, base command for sdiff, using the crate diff as engine. Currently it does not fully represent GNU diff -y, some flags (|, (, ), , /) could not be developed due to the limitation of the engine we currently use (crate diff), which did not allow perform logic around it. Only the use of '<' and '>' were enabled. - Create tests for SideBySide implementation --- src/diff.rs | 3 +- src/lib.rs | 2 ++ src/main.rs | 1 + src/params.rs | 8 +++++ src/side_diff.rs | 86 ++++++++++++++++++++++++++++++++++++++++++++++++ src/utils.rs | 72 ++++++++++++++++++++++++++++++++++++++-- 6 files changed, 169 insertions(+), 3 deletions(-) create mode 100644 src/side_diff.rs diff --git a/src/diff.rs b/src/diff.rs index f769a29..bbb725d 100644 --- a/src/diff.rs +++ b/src/diff.rs @@ -5,7 +5,7 @@ use crate::params::{parse_params, Format}; use crate::utils::report_failure_to_read_input_file; -use crate::{context_diff, ed_diff, normal_diff, unified_diff}; +use crate::{context_diff, ed_diff, normal_diff, side_diff, unified_diff}; use std::env::ArgsOs; use std::ffi::OsString; use std::fs; @@ -79,6 +79,7 @@ pub fn main(opts: Peekable) -> ExitCode { eprintln!("{error}"); exit(2); }), + Format::SideBySide => side_diff::diff(&from_content, &to_content), }; if params.brief && !result.is_empty() { println!( diff --git a/src/lib.rs b/src/lib.rs index a20ac56..342b01c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ pub mod ed_diff; pub mod macros; pub mod normal_diff; pub mod params; +pub mod side_diff; pub mod unified_diff; pub mod utils; @@ -11,4 +12,5 @@ pub mod utils; pub use context_diff::diff as context_diff; pub use ed_diff::diff as ed_diff; pub use normal_diff::diff as normal_diff; +pub use side_diff::diff as side_by_side_diff; pub use unified_diff::diff as unified_diff; diff --git a/src/main.rs b/src/main.rs index 8194d00..badaaa0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,6 +18,7 @@ mod ed_diff; mod macros; mod normal_diff; mod params; +mod side_diff; mod unified_diff; mod utils; diff --git a/src/params.rs b/src/params.rs index 9b3abc4..9f5c07d 100644 --- a/src/params.rs +++ b/src/params.rs @@ -11,6 +11,7 @@ pub enum Format { Unified, Context, Ed, + SideBySide, } #[derive(Clone, Debug, Eq, PartialEq)] @@ -101,6 +102,13 @@ pub fn parse_params>(mut opts: Peekable) -> Resu format = Some(Format::Ed); continue; } + if param == "-y" || param == "--side-by-side" { + if format.is_some() && format != Some(Format::SideBySide) { + return Err("Conflicting output style option".to_string()); + } + format = Some(Format::SideBySide); + 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. diff --git a/src/side_diff.rs b/src/side_diff.rs new file mode 100644 index 0000000..71bf4b7 --- /dev/null +++ b/src/side_diff.rs @@ -0,0 +1,86 @@ +// This file is part of the uutils diffutils package. +// +// For the full copyright and license information, please view the LICENSE-* +// files that was distributed with this source code. + +use crate::utils::limited_string; +use diff::Result; +use std::{ + io::{stdout, StdoutLock, Write}, + vec, +}; + +fn push_output( + output: &mut StdoutLock, + left_ln: &[u8], + right_ln: &[u8], + symbol: &[u8], + tab_size: usize, +) -> std::io::Result<()> { + // The reason why this function exists, is that we cannot + // assume a enconding for our left or right line, and the + // writeln!() macro obligattes us to do it. + + // side-by-side diff usually prints the output like: + // {left_line}{tab}{space_char}{symbol(|, < or >)}{space_char}{right_line}{EOL} + + // recalculate how many spaces are nescessary, cause we need to take into + // consideration the lenght of the word before print it. + let tab_size = (tab_size as isize - left_ln.len() as isize).max(0); + let ident = vec![b' '; tab_size as usize]; + output.write_all(left_ln)?; // {left_line} + output.write_all(&ident)?; // {tab} + output.write_all(b" ")?; // {space_char} + output.write_all(symbol)?; // {symbol} + output.write_all(b" ")?; // {space_char} + output.write_all(right_ln)?; // {right_line} + + writeln!(output)?; // {EOL} + + Ok(()) +} + +pub fn diff(from_file: &[u8], to_file: &[u8]) -> Vec { + // ^ The left file ^ The right file + + let mut output = stdout().lock(); + let left_lines: Vec<&[u8]> = from_file.split(|&c| c == b'\n').collect(); + let right_lines: Vec<&[u8]> = to_file.split(|&c| c == b'\n').collect(); + let tab_size = 61; // for some reason the tab spaces are 61 not 60 + for result in diff::slice(&left_lines, &right_lines) { + match result { + Result::Left(left_ln) => { + push_output( + &mut output, + limited_string(left_ln, tab_size), + &[], + b"<", + tab_size, + ) + .unwrap(); + } + Result::Right(right_ln) => { + push_output( + &mut output, + &[], + limited_string(right_ln, tab_size), + b">", + tab_size, + ) + .unwrap(); + } + Result::Both(left_ln, right_ln) => { + push_output( + &mut output, + limited_string(left_ln, tab_size), + limited_string(right_ln, tab_size), + b" ", + tab_size, + ) + .unwrap(); + } + } + } + + vec![] +} diff --git a/src/utils.rs b/src/utils.rs index 88b39ff..b0d0232 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -3,9 +3,8 @@ // For the full copyright and license information, please view the LICENSE-* // files that was distributed with this source code. -use std::{ffi::OsString, io::Write}; - use regex::Regex; +use std::{ffi::OsString, io::Write}; use unicode_width::UnicodeWidthStr; /// Replace tabs by spaces in the input line. @@ -99,6 +98,15 @@ 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::*; @@ -205,4 +213,64 @@ 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"); + } + } } From dff98a29695ddab3ff7e010e16484c0356bf8021 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 29 Apr 2025 15:45:04 +0000 Subject: [PATCH 153/194] fix(deps): update rust crate chrono to v0.4.41 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 29a8623..2c5103a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -91,9 +91,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.40" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ "android-tzdata", "iana-time-zone", From 1ef6923b7dac69edb3b6044d5842d21ccc78bed0 Mon Sep 17 00:00:00 2001 From: "Sami Daniel (Tsoi)" Date: Mon, 26 May 2025 08:26:10 -0300 Subject: [PATCH 154/194] 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 --- fuzz/Cargo.toml | 6 +- fuzz/fuzz_targets/fuzz_side.rs | 42 ++ src/diff.rs | 7 +- src/params.rs | 37 +- src/side_diff.rs | 1302 ++++++++++++++++++++++++++++++-- src/utils.rs | 69 -- 6 files changed, 1327 insertions(+), 136 deletions(-) create mode 100644 fuzz/fuzz_targets/fuzz_side.rs diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 8b0b521..39efd70 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -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 \ No newline at end of file diff --git a/fuzz/fuzz_targets/fuzz_side.rs b/fuzz/fuzz_targets/fuzz_side.rs new file mode 100644 index 0000000..8a69c07 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_side.rs @@ -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, Vec, /* 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, ¶ms); + 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(); +}); \ No newline at end of file diff --git a/src/diff.rs b/src/diff.rs index bbb725d..f4c0614 100644 --- a/src/diff.rs +++ b/src/diff.rs @@ -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) -> 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, ¶ms) + } }; if params.brief && !result.is_empty() { println!( diff --git a/src/params.rs b/src/params.rs index 9f5c07d..c64b3fc 100644 --- a/src/params.rs +++ b/src/params.rs @@ -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>(mut opts: Peekable) -> Resu let mut format = None; let mut context = None; let tabsize_re = Regex::new(r"^--tabsize=(?\d+)$").unwrap(); + let width_re = Regex::new(r"--width=(?P\d+)$").unwrap(); while let Some(param) = opts.next() { let next_param = opts.peek(); if param == "--" { @@ -109,6 +112,27 @@ pub fn parse_params>(mut opts: Peekable) -> 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::() { + 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>(mut opts: Peekable) -> Resu .unwrap() .as_str(); params.tabsize = match tabsize_str.parse::() { - 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(¶m, 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() diff --git a/src/side_diff.rs b/src/side_diff.rs index 71bf4b7..72673d4 100644 --- a/src/side_diff.rs +++ b/src/side_diff.rs @@ -3,84 +3,1264 @@ // For the full copyright and license information, please view the LICENSE-* // files that was distributed with this source code. -use crate::utils::limited_string; +use core::cmp::{max, min}; use diff::Result; -use std::{ - io::{stdout, StdoutLock, Write}, - vec, -}; +use std::{io::Write, vec}; +use unicode_width::UnicodeWidthStr; -fn push_output( - output: &mut StdoutLock, +use crate::params::Params; + +const GUTTER_WIDTH_MIN: usize = 3; + +struct CharIter<'a> { + current: &'a [u8], +} + +struct Config { + sdiff_half_width: usize, + sdiff_column_two_offset: usize, + tab_size: usize, + expanded: bool, + separator_pos: usize, +} + +impl<'a> From<&'a [u8]> for CharIter<'a> { + fn from(value: &'a [u8]) -> Self { + CharIter { current: value } + } +} + +impl<'a> Iterator for CharIter<'a> { + // (bytes for the next char, visible width) + type Item = (&'a [u8], usize); + + fn next(&mut self) -> Option { + let max = self.current.len().min(4); + + // We reached the end. + if max == 0 { + return None; + } + + // Try to find the next utf-8 character, if present in the next 4 bytes. + let mut index = 1; + let mut view = &self.current[..index]; + let mut char = str::from_utf8(view); + while char.is_err() { + index += 1; + if index > max { + break; + } + view = &self.current[..index]; + char = str::from_utf8(view) + } + + match char { + Ok(c) => { + self.current = self + .current + .get(view.len()..) + .unwrap_or(&self.current[0..0]); + Some((view, UnicodeWidthStr::width(c))) + } + Err(_) => { + // We did not find an utf-8 char within the next 4 bytes, return the single byte. + self.current = &self.current[1..]; + Some((&view[..1], 1)) + } + } + } +} + +impl Config { + pub fn new(full_width: usize, tab_size: usize, expanded: bool) -> Self { + // diff uses this calculation to calculate the size of a half line + // based on the options passed (like -w, -t, etc.). It's actually + // pretty useless, because we (actually) don't have any size modifiers + // that can change this, however I just want to leave the calculate + // here, since it's not very clear and may cause some confusion + + let w = full_width as isize; + let t = tab_size as isize; + let t_plus_g = t + GUTTER_WIDTH_MIN as isize; + let unaligned_off = (w >> 1) + (t_plus_g >> 1) + (w & t_plus_g & 1); + let off = unaligned_off - unaligned_off % t; + let hw = max(0, min(off - GUTTER_WIDTH_MIN as isize, w - off)) as usize; + let c2o = if hw != 0 { off as usize } else { w as usize }; + + Self { + expanded, + sdiff_column_two_offset: c2o, + tab_size, + sdiff_half_width: hw, + separator_pos: ((hw + c2o - 1) >> 1), + } + } +} + +fn format_tabs_and_spaces( + from: usize, + to: usize, + config: &Config, + buf: &mut T, +) -> std::io::Result<()> { + let expanded = config.expanded; + let tab_size = config.tab_size; + let mut current = from; + + if current > to { + return Ok(()); + } + + if expanded { + while current < to { + buf.write_all(b" ")?; + current += 1; + } + return Ok(()); + } + + while current + (tab_size - current % tab_size) <= to { + let next_tab = current + (tab_size - current % tab_size); + buf.write_all(b"\t")?; + current = next_tab; + } + + while current < to { + buf.write_all(b" ")?; + current += 1; + } + + Ok(()) +} + +fn process_half_line( + s: &[u8], + max_width: usize, + is_right: bool, + white_space_gutter: bool, + config: &Config, + buf: &mut T, +) -> std::io::Result<()> { + if s.is_empty() { + if !is_right { + format_tabs_and_spaces( + 0, + max_width + + if white_space_gutter { + GUTTER_WIDTH_MIN + } else { + 1 + }, + config, + buf, + )?; + } + + return Ok(()); + } + + if max_width > config.sdiff_half_width { + return Ok(()); + } + + if max_width > config.sdiff_column_two_offset && !is_right { + return Ok(()); + } + + let expanded = config.expanded; + let tab_size = config.tab_size; + let sdiff_column_two_offset = config.sdiff_column_two_offset; + let mut current_width = 0; + let iter = CharIter::from(s); + + // the encoding will probably be compatible with utf8, so we can take advantage + // of that to get the size of the columns and iterate without breaking the encoding of anything. + // It seems like a good trade, since there is still a fallback in case it is not utf8. + // But I think it would be better if we used some lib that would allow us to handle this + // in the best way possible, in order to avoid overhead (currently 2 for loops are needed). + // There is a library called mcel (mcel.h) that is used in GNU diff, but the documentation + // about it is very scarce, nor is its use documented on the internet. In fact, from my + // research I didn't even find any information about it in the GNU lib's own documentation. + + for c in iter { + let (char, c_width) = c; + + if current_width + c_width > max_width { + break; + } + + match char { + b"\t" => { + if expanded && (current_width + tab_size - (current_width % tab_size)) <= max_width + { + let mut spaces = tab_size - (current_width % tab_size); + while spaces > 0 { + buf.write_all(b" ")?; + current_width += 1; + spaces -= 1; + } + } else if current_width + tab_size - (current_width % tab_size) <= max_width { + buf.write_all(b"\t")?; + current_width += tab_size - (current_width % tab_size); + } + } + b"\n" => { + break; + } + b"\r" => { + buf.write_all(b"\r")?; + format_tabs_and_spaces(0, sdiff_column_two_offset, config, buf)?; + current_width = 0; + } + b"\0" | b"\x07" | b"\x0C" | b"\x0B" => { + buf.write_all(char)?; + } + _ => { + buf.write_all(char)?; + current_width += c_width; + } + } + } + + // gnu sdiff do not tabulate the hole empty right line, instead, just keep the line empty + if !is_right { + // we always sum + 1 or + GUTTER_WIDTH_MIN cause we want to expand + // up to the third column of the gutter column if the gutter is gutter white space, + // otherwise we can expand to only the first column of the gutter middle column, cause + // the next is the sep char + format_tabs_and_spaces( + current_width, + max_width + + if white_space_gutter { + GUTTER_WIDTH_MIN + } else { + 1 + }, + config, + buf, + )?; + } + + Ok(()) +} + +fn push_output( left_ln: &[u8], right_ln: &[u8], - symbol: &[u8], - tab_size: usize, + symbol: u8, + output: &mut T, + config: &Config, ) -> std::io::Result<()> { - // The reason why this function exists, is that we cannot - // assume a enconding for our left or right line, and the - // writeln!() macro obligattes us to do it. - - // side-by-side diff usually prints the output like: - // {left_line}{tab}{space_char}{symbol(|, < or >)}{space_char}{right_line}{EOL} - - // recalculate how many spaces are nescessary, cause we need to take into - // consideration the lenght of the word before print it. - let tab_size = (tab_size as isize - left_ln.len() as isize).max(0); - let ident = vec![b' '; tab_size as usize]; - output.write_all(left_ln)?; // {left_line} - output.write_all(&ident)?; // {tab} - output.write_all(b" ")?; // {space_char} - output.write_all(symbol)?; // {symbol} - output.write_all(b" ")?; // {space_char} - output.write_all(right_ln)?; // {right_line} - - writeln!(output)?; // {EOL} + if left_ln.is_empty() && right_ln.is_empty() { + writeln!(output)?; + return Ok(()); + } + + let white_space_gutter = symbol == b' '; + let half_width = config.sdiff_half_width; + let column_two_offset = config.sdiff_column_two_offset; + let separator_pos = config.separator_pos; + let put_new_line = true; // should be false when | is allowed + + // this involves a lot of the '|' mark, however, as it is not active, + // it is better to deactivate it as it introduces visual bug if + // the line is empty. + // if !left_ln.is_empty() { + // put_new_line = put_new_line || (left_ln.last() == Some(&b'\n')); + // } + // if !right_ln.is_empty() { + // put_new_line = put_new_line || (right_ln.last() == Some(&b'\n')); + // } + + process_half_line( + left_ln, + half_width, + false, + white_space_gutter, + config, + output, + )?; + if symbol != b' ' { + // the diff always want to put all tabs possible in the usable are, + // even in the middle space between the gutters if possible. + + output.write_all(&[symbol])?; + if !right_ln.is_empty() { + format_tabs_and_spaces(separator_pos + 1, column_two_offset, config, output)?; + } + } + process_half_line( + right_ln, + half_width, + true, + white_space_gutter, + config, + output, + )?; + + if put_new_line { + writeln!(output)?; + } Ok(()) } -pub fn diff(from_file: &[u8], to_file: &[u8]) -> Vec { +pub fn diff( + from_file: &[u8], + to_file: &[u8], + output: &mut T, + params: &Params, +) -> Vec { // ^ The left file ^ The right file - let mut output = stdout().lock(); - let left_lines: Vec<&[u8]> = from_file.split(|&c| c == b'\n').collect(); - let right_lines: Vec<&[u8]> = to_file.split(|&c| c == b'\n').collect(); - let tab_size = 61; // for some reason the tab spaces are 61 not 60 + let mut left_lines: Vec<&[u8]> = from_file.split_inclusive(|&c| c == b'\n').collect(); + let mut right_lines: Vec<&[u8]> = to_file.split_inclusive(|&c| c == b'\n').collect(); + let config = Config::new(params.width, params.tabsize, params.expand_tabs); + + if left_lines.last() == Some(&&b""[..]) { + left_lines.pop(); + } + + if right_lines.last() == Some(&&b""[..]) { + right_lines.pop(); + } + + /* + DISCLAIMER: + Currently the diff engine does not produce results like the diff engine used in GNU diff, + so some results may be inaccurate. For example, the line difference marker "|", according + to the GNU documentation, appears when the same lines (only the actual line, although the + relative line may change the result, so occasionally '|' markers appear with the same lines) + are different but exist in both files. In the current solution the same result cannot be + obtained because the diff engine does not return Both if both exist but are different, + but instead returns a Left and a Right for each one, implying that two lines were added + and deleted. Furthermore, the GNU diff program apparently stores some internal state + (this internal state is just a note about how the diff engine works) about the lines. + For example, an added or removed line directly counts in the line query of the original + lines to be printed in the output. Because of this imbalance caused by additions and + deletions, the characters ( and ) are introduced. They basically represent lines without + context, which have lost their pair in the other file due to additions or deletions. Anyway, + my goal with this disclaimer is to warn that for some reason, whether it's the diff engine's + inability to determine and predict/precalculate the result of GNU's sdiff, with this software it's + not possible to reproduce results that are 100% faithful to GNU's, however, the basic premise + e of side diff of showing added and removed lines and creating edit scripts is totally possible. + More studies are needed to cover GNU diff side by side with 100% accuracy, which is one of + the goals of this project : ) + */ for result in diff::slice(&left_lines, &right_lines) { match result { - Result::Left(left_ln) => { - push_output( - &mut output, - limited_string(left_ln, tab_size), - &[], - b"<", - tab_size, - ) - .unwrap(); - } - Result::Right(right_ln) => { - push_output( - &mut output, - &[], - limited_string(right_ln, tab_size), - b">", - tab_size, - ) - .unwrap(); - } + Result::Left(left_ln) => push_output(left_ln, b"", b'<', output, &config).unwrap(), + Result::Right(right_ln) => push_output(b"", right_ln, b'>', output, &config).unwrap(), Result::Both(left_ln, right_ln) => { - push_output( - &mut output, - limited_string(left_ln, tab_size), - limited_string(right_ln, tab_size), - b" ", - tab_size, - ) - .unwrap(); + push_output(left_ln, right_ln, b' ', output, &config).unwrap() } } } vec![] } + +#[cfg(test)] +mod tests { + const DEF_TAB_SIZE: usize = 4; + + use super::*; + + mod format_tabs_and_spaces { + use super::*; + + const CONFIG_E_T: Config = Config { + sdiff_half_width: 60, + tab_size: DEF_TAB_SIZE, + expanded: true, + sdiff_column_two_offset: 0, + separator_pos: 0, + }; + + const CONFIG_E_F: Config = Config { + sdiff_half_width: 60, + tab_size: DEF_TAB_SIZE, + expanded: false, + sdiff_column_two_offset: 0, + separator_pos: 0, + }; + + #[test] + fn test_format_tabs_and_spaces_expanded_false() { + let mut buf = vec![]; + format_tabs_and_spaces(0, 5, &CONFIG_E_F, &mut buf).unwrap(); + assert_eq!(buf, vec![b'\t', b' ']); + } + + #[test] + fn test_format_tabs_and_spaces_expanded_true() { + let mut buf = vec![]; + format_tabs_and_spaces(0, 5, &CONFIG_E_T, &mut buf).unwrap(); + assert_eq!(buf, vec![b' '; 5]); + } + + #[test] + fn test_format_tabs_and_spaces_from_greater_than_to() { + let mut buf = vec![]; + format_tabs_and_spaces(6, 5, &CONFIG_E_F, &mut buf).unwrap(); + assert!(buf.is_empty()); + } + + #[test] + fn test_format_from_non_zero_position() { + let mut buf = vec![]; + format_tabs_and_spaces(2, 7, &CONFIG_E_F, &mut buf).unwrap(); + assert_eq!(buf, vec![b'\t', b' ', b' ', b' ']); + } + + #[test] + fn test_multiple_full_tabs_needed() { + let mut buf = vec![]; + format_tabs_and_spaces(0, 12, &CONFIG_E_F, &mut buf).unwrap(); + assert_eq!(buf, vec![b'\t', b'\t', b'\t']); + } + + #[test] + fn test_uneven_tab_boundary_with_spaces() { + let mut buf = vec![]; + format_tabs_and_spaces(3, 10, &CONFIG_E_F, &mut buf).unwrap(); + assert_eq!(buf, vec![b'\t', b'\t', b' ', b' ']); + } + + #[test] + fn test_expanded_true_with_offset() { + let mut buf = vec![]; + format_tabs_and_spaces(3, 9, &CONFIG_E_T, &mut buf).unwrap(); + assert_eq!(buf, vec![b' '; 6]); + } + + #[test] + fn test_exact_tab_boundary_from_midpoint() { + let mut buf = vec![]; + format_tabs_and_spaces(4, 8, &CONFIG_E_F, &mut buf).unwrap(); + assert_eq!(buf, vec![b'\t']); + } + + #[test] + fn test_mixed_tabs_and_spaces_edge_case() { + let mut buf = vec![]; + format_tabs_and_spaces(5, 9, &CONFIG_E_F, &mut buf).unwrap(); + assert_eq!(buf, vec![b'\t', b' ']); + } + + #[test] + fn test_minimal_gap_with_tab() { + let mut buf = vec![]; + format_tabs_and_spaces(7, 8, &CONFIG_E_F, &mut buf).unwrap(); + assert_eq!(buf, vec![b'\t']); + } + + #[test] + fn test_expanded_false_with_tab_at_end() { + let mut buf = vec![]; + format_tabs_and_spaces(6, 8, &CONFIG_E_F, &mut buf).unwrap(); + assert_eq!(buf, vec![b'\t']); + } + } + + mod process_half_line { + use super::*; + + fn create_test_config(expanded: bool, tab_size: usize) -> Config { + Config { + sdiff_half_width: 30, + sdiff_column_two_offset: 60, + tab_size, + expanded, + separator_pos: 15, + } + } + + #[test] + fn test_empty_line_left_expanded_false() { + let config = create_test_config(false, DEF_TAB_SIZE); + let mut buf = vec![]; + process_half_line(b"", 10, false, false, &config, &mut buf).unwrap(); + assert_eq!(buf.len(), 5); + assert_eq!(buf, vec![b'\t', b'\t', b' ', b' ', b' ']); + } + + #[test] + fn test_tabs_unexpanded() { + let config = create_test_config(false, DEF_TAB_SIZE); + let mut buf = vec![]; + process_half_line(b"\tabc", 8, false, false, &config, &mut buf).unwrap(); + assert_eq!(buf, vec![b'\t', b'a', b'b', b'c', b'\t', b' ']); + } + + #[test] + fn test_utf8_multibyte() { + let config = create_test_config(false, DEF_TAB_SIZE); + let mut buf = vec![]; + let s = "😉😉😉".as_bytes(); + process_half_line(s, 3, false, false, &config, &mut buf).unwrap(); + let mut r = vec![]; + r.write_all("😉\t".as_bytes()).unwrap(); + assert_eq!(buf, r) + } + + #[test] + fn test_newline_handling() { + let config = create_test_config(false, DEF_TAB_SIZE); + let mut buf = vec![]; + process_half_line(b"abc\ndef", 5, false, false, &config, &mut buf).unwrap(); + assert_eq!(buf, vec![b'a', b'b', b'c', b'\t', b' ', b' ']); + } + + #[test] + fn test_carriage_return() { + let config = create_test_config(false, DEF_TAB_SIZE); + let mut buf = vec![]; + process_half_line(b"\rxyz", 5, true, false, &config, &mut buf).unwrap(); + let mut r = vec![b'\r']; + r.extend(vec![b'\t'; 15]); + r.extend(vec![b'x', b'y', b'z']); + assert_eq!(buf, r); + } + + #[test] + fn test_exact_width_fit() { + let config = create_test_config(true, DEF_TAB_SIZE); + let mut buf = vec![]; + process_half_line(b"abcd", 4, false, false, &config, &mut buf).unwrap(); + assert_eq!(buf.len(), 5); + assert_eq!(buf, b"abcd ".to_vec()); + } + + #[test] + fn test_non_utf8_bytes() { + let config = create_test_config(false, DEF_TAB_SIZE); + let mut buf = vec![]; + // ISO-8859-1 + process_half_line( + &[0x63, 0x61, 0x66, 0xE9], + 5, + false, + false, + &config, + &mut buf, + ) + .unwrap(); + assert_eq!(&buf, &[0x63, 0x61, 0x66, 0xE9, b' ', b' ']); + assert!(String::from_utf8(buf).is_err()); + } + + #[test] + fn test_non_utf8_bytes_ignore_padding_bytes() { + let config = create_test_config(false, DEF_TAB_SIZE); + let mut buf = vec![]; + + let utf32le_bytes = [ + 0x63, 0x00, 0x00, 0x00, // 'c' + 0x61, 0x00, 0x00, 0x00, // 'a' + 0x66, 0x00, 0x00, 0x00, // 'f' + 0xE9, 0x00, 0x00, 0x00, // 'é' + ]; + // utf8 little endiand 32 bits (or 4 bytes per char) + process_half_line(&utf32le_bytes, 6, false, false, &config, &mut buf).unwrap(); + let mut r = utf32le_bytes.to_vec(); + r.extend(vec![b' '; 3]); + assert_eq!(buf, r); + } + + #[test] + fn test_non_utf8_non_preserve_ascii_bytes_cut() { + let config = create_test_config(false, DEF_TAB_SIZE); + let mut buf = vec![]; + + let gb18030 = b"\x63\x61\x66\xA8\x80"; // some random chinese encoding + // ^ é char, start multi byte + process_half_line(gb18030, 4, false, false, &config, &mut buf).unwrap(); + assert_eq!(buf, b"\x63\x61\x66\xA8 "); // break the encoding of 'é' letter + } + + #[test] + fn test_right_line_padding() { + let config = create_test_config(false, DEF_TAB_SIZE); + let mut buf = vec![]; + process_half_line(b"xyz", 5, true, true, &config, &mut buf).unwrap(); + assert_eq!(buf.len(), 3); + } + + #[test] + fn test_mixed_tabs_spaces() { + let config = create_test_config(false, DEF_TAB_SIZE); + let mut buf = vec![]; + process_half_line(b"\t \t", 10, false, false, &config, &mut buf).unwrap(); + assert_eq!(buf, vec![b'\t', b' ', b' ', b'\t', b' ', b' ', b' ']); + } + + #[test] + fn test_overflow_multibyte() { + let config = create_test_config(false, DEF_TAB_SIZE); + let mut buf = vec![]; + let s = "日本語".as_bytes(); + process_half_line(s, 5, false, false, &config, &mut buf).unwrap(); + assert_eq!(buf, "日本 ".as_bytes()); + } + + #[test] + fn test_white_space_gutter() { + let config = create_test_config(false, DEF_TAB_SIZE); + let mut buf = vec![]; + let s = b"abc"; + process_half_line(s, 3, false, true, &config, &mut buf).unwrap(); + assert_eq!(buf, b"abc\t "); + } + + #[test] + fn test_expanded_true() { + let config = create_test_config(true, DEF_TAB_SIZE); + let mut buf = vec![]; + let s = b"abc"; + process_half_line(s, 10, false, false, &config, &mut buf).unwrap(); + assert_eq!(buf, b"abc ") + } + + #[test] + fn test_expanded_true_with_gutter() { + let config = create_test_config(true, DEF_TAB_SIZE); + let mut buf = vec![]; + let s = b"abc"; + process_half_line(s, 10, false, true, &config, &mut buf).unwrap(); + assert_eq!(buf, b"abc ") + } + + #[test] + fn test_width0_chars() { + let config = create_test_config(false, DEF_TAB_SIZE); + let mut buf = vec![]; + let s = b"abc\0\x0B\x07\x0C"; + process_half_line(s, 4, false, false, &config, &mut buf).unwrap(); + assert_eq!(buf, b"abc\0\x0B\x07\x0C\t ") + } + + #[test] + fn test_left_empty_white_space_gutter() { + let config = create_test_config(false, DEF_TAB_SIZE); + let mut buf = vec![]; + let s = b""; + process_half_line(s, 9, false, true, &config, &mut buf).unwrap(); + assert_eq!(buf, b"\t\t\t"); + } + + #[test] + fn test_s_size_eq_max_width_p1() { + let config = create_test_config(false, DEF_TAB_SIZE); + let mut buf = vec![]; + let s = b"abcdefghij"; + process_half_line(s, 10, false, false, &config, &mut buf).unwrap(); + assert_eq!(buf, b"abcdefghij "); + } + + #[test] + fn test_mixed_tabs_and_spaces_inversion() { + let config = create_test_config(false, DEF_TAB_SIZE); + let mut buf = vec![]; + let s = b" \t \t "; + process_half_line(s, 10, false, false, &config, &mut buf).unwrap(); + assert_eq!(buf, b" \t \t "); + } + + #[test] + fn test_expanded_with_tabs() { + let config = create_test_config(true, DEF_TAB_SIZE); + let mut buf = vec![]; + let s = b" \t \t "; + process_half_line(s, 10, false, false, &config, &mut buf).unwrap(); + assert_eq!(buf, b" "); + } + + #[test] + fn test_expanded_with_tabs_and_space_gutter() { + let config = create_test_config(true, DEF_TAB_SIZE); + let mut buf = vec![]; + let s = b" \t \t "; + process_half_line(s, 10, false, true, &config, &mut buf).unwrap(); + assert_eq!(buf, b" "); + } + + #[test] + fn test_zero_width_unicode_chars() { + let config = create_test_config(false, DEF_TAB_SIZE); + let mut buf = vec![]; + let s = "\u{200B}".as_bytes(); + process_half_line(s, 10, false, false, &config, &mut buf).unwrap(); + assert_eq!(buf, "\u{200B}\t\t ".as_bytes()); + } + + #[test] + fn test_multiple_carriage_returns() { + let config = create_test_config(false, DEF_TAB_SIZE); + let mut buf = vec![]; + let s = b"\r\r"; + process_half_line(s, 10, false, false, &config, &mut buf).unwrap(); + let mut r = vec![b'\r']; + r.extend(vec![b'\t'; 15]); + r.push(b'\r'); + r.extend(vec![b'\t'; 15]); + r.extend(vec![b'\t'; 2]); + r.extend(vec![b' '; 3]); + assert_eq!(buf, r); + } + + #[test] + fn test_multiple_carriage_returns_is_right_true() { + let config = create_test_config(false, DEF_TAB_SIZE); + let mut buf = vec![]; + let s = b"\r\r"; + process_half_line(s, 10, true, false, &config, &mut buf).unwrap(); + let mut r = vec![b'\r']; + r.extend(vec![b'\t'; 15]); + r.push(b'\r'); + r.extend(vec![b'\t'; 15]); + assert_eq!(buf, r); + } + + #[test] + fn test_mixed_invalid_utf8_with_valid() { + let config = create_test_config(false, DEF_TAB_SIZE); + let mut buf = vec![]; + let s = b"abc\xFF\xFEdef"; + process_half_line(s, 10, false, false, &config, &mut buf).unwrap(); + assert!(String::from_utf8(s.to_vec()).is_err()); + assert_eq!(buf, b"abc\xFF\xFEdef "); + } + + #[test] + fn test_max_width_zero() { + let config = create_test_config(false, DEF_TAB_SIZE); + let mut buf = vec![]; + let s = b"foo bar"; + process_half_line(s, 0, false, false, &config, &mut buf).unwrap(); + assert_eq!(buf, vec![b' ']); + } + + #[test] + fn test_line_only_with_tabs() { + let config = create_test_config(false, DEF_TAB_SIZE); + let mut buf = vec![]; + let s = b"\t\t\t"; + process_half_line(s, 10, false, false, &config, &mut buf).unwrap(); + assert_eq!(buf, vec![b'\t', b'\t', b' ', b' ', b' ']) + } + + #[test] + fn test_tabs_expanded() { + let config = create_test_config(true, DEF_TAB_SIZE); + let mut buf = vec![]; + let s = b"\t\t\t"; + process_half_line(s, 12, false, false, &config, &mut buf).unwrap(); + assert_eq!(buf, b" ".repeat(13)); + } + + #[test] + fn test_mixed_tabs() { + let config = create_test_config(false, DEF_TAB_SIZE); + let mut buf = vec![]; + let s = b"a\tb\tc\t"; + process_half_line(s, 10, false, false, &config, &mut buf).unwrap(); + assert_eq!(buf, b"a\tb\tc "); + } + + #[test] + fn test_mixed_tabs_with_gutter() { + let config = create_test_config(false, DEF_TAB_SIZE); + let mut buf = vec![]; + let s = b"a\tb\tc\t"; + process_half_line(s, 10, false, true, &config, &mut buf).unwrap(); + assert_eq!(buf, b"a\tb\tc\t "); + } + + #[test] + fn test_mixed_tabs_expanded() { + let config = create_test_config(true, DEF_TAB_SIZE); + let mut buf = vec![]; + let s = b"a\tb\tc\t"; + process_half_line(s, 10, false, false, &config, &mut buf).unwrap(); + assert_eq!(buf, b"a b c "); + } + + #[test] + fn test_mixed_tabs_expanded_with_gutter() { + let config = create_test_config(true, DEF_TAB_SIZE); + let mut buf = vec![]; + let s = b"a\tb\tc\t"; + process_half_line(s, 10, false, true, &config, &mut buf).unwrap(); + assert_eq!(buf, b"a b c "); + } + + #[test] + fn test_break_if_invalid_max_width() { + let config = create_test_config(true, DEF_TAB_SIZE); + let mut buf = vec![]; + let s = b"a\tb\tc\t"; + process_half_line(s, 61, false, true, &config, &mut buf).unwrap(); + assert_eq!(buf, b""); + assert_eq!(buf.len(), 0); + } + + #[test] + fn test_new_line() { + let config = create_test_config(false, DEF_TAB_SIZE); + let mut buf = vec![]; + let s = b"abc"; + process_half_line(s, 10, false, false, &config, &mut buf).unwrap(); + assert_eq!(buf, b"abc\t\t "); + } + } + + mod push_output { + // almost all behavior of the push_output was tested with tests on process_half_line + + use super::*; + + impl Default for Config { + fn default() -> Self { + Config::new(130, 8, false) + } + } + + fn create_test_config_def() -> Config { + Config::default() + } + + #[test] + fn test_left_empty_right_not_added() { + let config = create_test_config_def(); + let left_ln = b""; + let right_ln = b"bar"; + let symbol = b'>'; + let mut buf = vec![]; + push_output(&left_ln[..], &right_ln[..], symbol, &mut buf, &config).unwrap(); + assert_eq!(buf, b"\t\t\t\t\t\t\t >\tbar\n"); + } + + #[test] + fn test_right_empty_left_not_del() { + let config = create_test_config_def(); + let left_ln = b"bar"; + let right_ln = b""; + let symbol = b'>'; + let mut buf = vec![]; + push_output(&left_ln[..], &right_ln[..], symbol, &mut buf, &config).unwrap(); + assert_eq!(buf, b"bar\t\t\t\t\t\t\t >\n"); + } + + #[test] + fn test_both_empty() { + let config = create_test_config_def(); + let left_ln = b""; + let right_ln = b""; + let symbol = b' '; + let mut buf = vec![]; + push_output(&left_ln[..], &right_ln[..], symbol, &mut buf, &config).unwrap(); + assert_eq!(buf, b"\n"); + } + + #[test] + fn test_output_cut_with_maximization() { + let config = create_test_config_def(); + let left_ln = b"a".repeat(62); + let right_ln = b"a".repeat(62); + let symbol = b' '; + let mut buf = vec![]; + push_output(&left_ln[..], &right_ln[..], symbol, &mut buf, &config).unwrap(); + assert_eq!(buf.len(), 61 * 2 + 2); + assert_eq!(&buf[0..61], vec![b'a'; 61]); + assert_eq!(&buf[61..62], b"\t"); + let mut end = b"a".repeat(61); + end.push(b'\n'); + assert_eq!(&buf[62..], end); + } + + #[test] + fn test_both_lines_non_empty_with_space_symbol_max_tabs() { + let config = create_test_config_def(); + let left_ln = b"left"; + let right_ln = b"right"; + let symbol = b' '; + let mut buf = vec![]; + push_output(left_ln, right_ln, symbol, &mut buf, &config).unwrap(); + let expected_left = "left\t\t\t\t\t\t\t\t"; + let expected_right = "right"; + assert_eq!(buf, format!("{expected_left}{expected_right}\n").as_bytes()); + } + + #[test] + fn test_non_space_symbol_with_padding() { + let config = create_test_config_def(); + let left_ln = b"data"; + let right_ln = b""; + let symbol = b'<'; // impossible case, just to use different symbol + let mut buf = vec![]; + push_output(left_ln, right_ln, symbol, &mut buf, &config).unwrap(); + assert_eq!(buf, format!("data\t\t\t\t\t\t\t <\n").as_bytes()); + } + + #[test] + fn test_lines_exceeding_half_width() { + let config = create_test_config_def(); + let left_ln = vec![b'a'; 100]; + let left_ln = left_ln.as_slice(); + let right_ln = vec![b'b'; 100]; + let right_ln = right_ln.as_slice(); + let symbol = b' '; + let mut buf = vec![]; + push_output(left_ln, right_ln, symbol, &mut buf, &config).unwrap(); + let expected_left = "a".repeat(61); + let expected_right = "b".repeat(61); + assert_eq!(buf.len(), 61 + 1 + 61 + 1); + assert_eq!(&buf[0..61], expected_left.as_bytes()); + assert_eq!(buf[61], b'\t'); + assert_eq!(&buf[62..123], expected_right.as_bytes()); + assert_eq!(&buf[123..], b"\n"); + } + + #[test] + fn test_tabs_in_lines_expanded() { + let mut config = create_test_config_def(); + config.expanded = true; + let left_ln = b"\tleft"; + let right_ln = b"\tright"; + let symbol = b' '; + let mut buf = vec![]; + push_output(left_ln, right_ln, symbol, &mut buf, &config).unwrap(); + let expected_left = " left".to_string() + &" ".repeat(61 - 12); + let expected_right = " right"; + assert_eq!( + buf, + format!("{}{}{}\n", expected_left, " ", expected_right).as_bytes() + ); + } + + #[test] + fn test_unicode_characters() { + let config = create_test_config_def(); + let left_ln = "áéíóú".as_bytes(); + let right_ln = "😀😃😄".as_bytes(); + let symbol = b' '; + let mut buf = vec![]; + push_output(left_ln, right_ln, symbol, &mut buf, &config).unwrap(); + let expected_left = format!("áéíóú\t\t\t\t\t\t\t\t"); + let expected_right = "😀😃😄"; + assert_eq!( + buf, + format!("{}{}\n", expected_left, expected_right).as_bytes() + ); + } + } + + mod diff { + /* + Probably this hole section should be refactored when complete sdiff + arrives. I would say that these tests are more to document the + behavior of the engine than to actually test whether it is right, + because it is right, but right up to its limitations. + */ + + use super::*; + + fn generate_params() -> Params { + Params { + tabsize: 8, + expand_tabs: false, + width: 130, + ..Default::default() + } + } + + fn contains_string(vec: &Vec, s: &str) -> usize { + let pattern = s.as_bytes(); + vec.windows(pattern.len()).filter(|s| s == &pattern).count() + } + + fn calc_lines(input: &Vec) -> usize { + let mut lines_counter = 0; + + for c in input { + if c == &b'\n' { + lines_counter += 1; + } + } + + lines_counter + } + + #[test] + fn test_equal_lines() { + let params = generate_params(); + let from_file = b"equal"; + let to_file = b"equal"; + let mut output = vec![]; + diff(from_file, to_file, &mut output, ¶ms); + assert_eq!(calc_lines(&output), 1); + assert!(!output.contains(&b'<')); + assert!(!output.contains(&b'>')); + assert_eq!(contains_string(&output, "equal"), 2) + } + + #[test] + fn test_different_lines() { + let params = generate_params(); + let from_file = b"eq"; + let to_file = b"ne"; + let mut output = vec![]; + diff(from_file, to_file, &mut output, ¶ms); + assert_eq!(calc_lines(&output), 2); + assert!(output.contains(&b'>')); + assert!(output.contains(&b'<')); + assert_eq!(contains_string(&output, "eq"), 1); + assert_eq!(contains_string(&output, "ne"), 1); + } + + #[test] + fn test_added_line() { + let params = generate_params(); + let from_file = b""; + let to_file = b"new line"; + let mut output = vec![]; + diff(from_file, to_file, &mut output, ¶ms); + + assert_eq!(calc_lines(&output), 1); + assert_eq!(contains_string(&output, ">"), 1); + assert_eq!(contains_string(&output, "new line"), 1); + } + + #[test] + fn test_removed_line() { + let params = generate_params(); + let from_file = b"old line"; + let to_file = b""; + let mut output = vec![]; + diff(from_file, to_file, &mut output, ¶ms); + + assert_eq!(calc_lines(&output), 1); + assert_eq!(contains_string(&output, "<"), 1); + assert_eq!(contains_string(&output, "old line"), 1); + } + + #[test] + fn test_multiple_changes() { + let params = generate_params(); + let from_file = b"line1\nline2\nline3"; + let to_file = b"line1\nmodified\nline4"; + let mut output = vec![]; + diff(from_file, to_file, &mut output, ¶ms); + + assert_eq!(calc_lines(&output), 5); + assert_eq!(contains_string(&output, "<"), 2); + assert_eq!(contains_string(&output, ">"), 2); + } + + #[test] + fn test_unicode_and_special_chars() { + let params = generate_params(); + let from_file = "á\t€".as_bytes(); + let to_file = "€\t😊".as_bytes(); + let mut output = vec![]; + diff(from_file, to_file, &mut output, ¶ms); + + assert!(String::from_utf8_lossy(&output).contains("á")); + assert!(String::from_utf8_lossy(&output).contains("€")); + assert!(String::from_utf8_lossy(&output).contains("😊")); + assert_eq!(contains_string(&output, "<"), 1); + assert_eq!(contains_string(&output, ">"), 1); + } + + #[test] + fn test_mixed_whitespace() { + let params = generate_params(); + let from_file = b" \tspaces"; + let to_file = b"\t\t tabs"; + let mut output = vec![]; + diff(from_file, to_file, &mut output, ¶ms); + + assert!(output.contains(&b'<')); + assert!(output.contains(&b'>')); + assert!(String::from_utf8_lossy(&output).contains("spaces")); + assert!(String::from_utf8_lossy(&output).contains("tabs")); + } + + #[test] + fn test_empty_files() { + let params = generate_params(); + let from_file = b""; + let to_file = b""; + let mut output = vec![]; + diff(from_file, to_file, &mut output, ¶ms); + + assert_eq!(output, vec![]); + } + + #[test] + fn test_partially_matching_lines() { + let params = generate_params(); + let from_file = b"match\nchange"; + let to_file = b"match\nupdated"; + let mut output = vec![]; + diff(from_file, to_file, &mut output, ¶ms); + + assert_eq!(calc_lines(&output), 3); + assert_eq!(contains_string(&output, "match"), 2); + assert_eq!(contains_string(&output, "<"), 1); + assert_eq!(contains_string(&output, ">"), 1); + } + + #[test] + fn test_interleaved_add_remove() { + let params = generate_params(); + let from_file = b"A\nB\nC\nD"; + let to_file = b"B\nX\nD\nY"; + let mut output = vec![]; + diff(from_file, to_file, &mut output, ¶ms); + + assert_eq!(calc_lines(&output), 7); + assert_eq!(contains_string(&output, "A"), 1); + assert_eq!(contains_string(&output, "X"), 1); + assert_eq!(contains_string(&output, "Y"), 1); + assert_eq!(contains_string(&output, "<"), 3); + assert_eq!(contains_string(&output, ">"), 3); + } + + #[test] + fn test_swapped_lines() { + let params = generate_params(); + let from_file = b"1\n2\n3\n4"; + let to_file = b"4\n3\n2\n1"; + let mut output = vec![]; + diff(from_file, to_file, &mut output, ¶ms); + + assert_eq!(calc_lines(&output), 7); + assert_eq!(contains_string(&output, "<"), 3); + assert_eq!(contains_string(&output, ">"), 3); + } + + #[test] + fn test_gap_between_changes() { + let params = generate_params(); + let from_file = b"Start\nKeep1\nRemove\nKeep2\nEnd"; + let to_file = b"Start\nNew1\nKeep1\nKeep2\nNew2\nEnd"; + let mut output = vec![]; + diff(from_file, to_file, &mut output, ¶ms); + + assert_eq!(calc_lines(&output), 7); + assert_eq!(contains_string(&output, "Remove"), 1); + assert_eq!(contains_string(&output, "New1"), 1); + assert_eq!(contains_string(&output, "New2"), 1); + assert_eq!(contains_string(&output, "<"), 1); + assert_eq!(contains_string(&output, ">"), 2); + } + + #[test] + fn test_mixed_operations_complex() { + let params = generate_params(); + let from_file = b"Same\nOld1\nSameMid\nOld2\nSameEnd"; + let to_file = b"Same\nNew1\nSameMid\nNew2\nNew3\nSameEnd"; + let mut output = vec![]; + diff(from_file, to_file, &mut output, ¶ms); + + assert_eq!(calc_lines(&output), 8); + assert_eq!(contains_string(&output, "<"), 2); + assert_eq!(contains_string(&output, ">"), 3); + } + + #[test] + fn test_insert_remove_middle() { + let params = generate_params(); + let from_file = b"Header\nContent1\nFooter"; + let to_file = b"Header\nContent2\nFooter"; + let mut output = vec![]; + diff(from_file, to_file, &mut output, ¶ms); + + assert_eq!(calc_lines(&output), 4); + assert_eq!(contains_string(&output, "Content1"), 1); + assert_eq!(contains_string(&output, "Content2"), 1); + assert_eq!(contains_string(&output, "<"), 1); + assert_eq!(contains_string(&output, ">"), 1); + } + + #[test] + fn test_multiple_adjacent_changes() { + let params = generate_params(); + let from_file = b"A\nB\nC\nD\nE"; + let to_file = b"A\nX\nY\nD\nZ"; + let mut output = vec![]; + diff(from_file, to_file, &mut output, ¶ms); + + assert_eq!(calc_lines(&output), 8); + assert_eq!(contains_string(&output, "<"), 3); + assert_eq!(contains_string(&output, ">"), 3); + } + } + + mod config { + use super::*; + + fn create_config(full_width: usize, tab_size: usize, expanded: bool) -> Config { + Config::new(full_width, tab_size, expanded) + } + + #[test] + fn test_full_width_80_tab_4() { + let config = create_config(80, 4, false); + assert_eq!(config.sdiff_half_width, 37); + assert_eq!(config.sdiff_column_two_offset, 40); + assert_eq!(config.separator_pos, 38); + } + + #[test] + fn test_full_width_40_tab_8() { + let config = create_config(40, 8, true); + assert_eq!(config.sdiff_half_width, 16); + assert_eq!(config.sdiff_column_two_offset, 24); + assert_eq!(config.separator_pos, 19); // (16 +24 -1) /2 = 19.5 + } + + #[test] + fn test_full_width_30_tab_2() { + let config = create_config(30, 2, false); + assert_eq!(config.sdiff_half_width, 13); + assert_eq!(config.sdiff_column_two_offset, 16); + assert_eq!(config.separator_pos, 14); + } + + #[test] + fn test_small_width_10_tab_4() { + let config = create_config(10, 4, false); + assert_eq!(config.sdiff_half_width, 2); + assert_eq!(config.sdiff_column_two_offset, 8); + assert_eq!(config.separator_pos, 4); + } + + #[test] + fn test_minimal_width_3_tab_4() { + let config = create_config(3, 4, false); + assert_eq!(config.sdiff_half_width, 0); + assert_eq!(config.sdiff_column_two_offset, 3); + assert_eq!(config.separator_pos, 1); + } + + #[test] + fn test_odd_width_7_tab_3() { + let config = create_config(7, 3, false); + assert_eq!(config.sdiff_half_width, 1); + assert_eq!(config.sdiff_column_two_offset, 6); + assert_eq!(config.separator_pos, 3); + } + + #[test] + fn test_tab_size_larger_than_width() { + let config = create_config(5, 10, false); + assert_eq!(config.sdiff_half_width, 0); + assert_eq!(config.sdiff_column_two_offset, 5); + assert_eq!(config.separator_pos, 2); + } + } +} diff --git a/src/utils.rs b/src/utils.rs index b0d0232..daca18d 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -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"); - } - } } From a3e57c950ef149e7cec1bd08e93b27f8ea013859 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 11 May 2025 22:12:26 +0000 Subject: [PATCH 155/194] chore(deps): update rust crate tempfile to v3.20.0 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2c5103a..6ff8fb5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -405,9 +405,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.19.1" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ "fastrand", "getrandom", From 45b3072534bbe7e48042986a2af964917edf26a2 Mon Sep 17 00:00:00 2001 From: "Sami Daniel (Tsoi)" Date: Mon, 26 May 2025 08:45:43 -0300 Subject: [PATCH 156/194] Configure CI fuzzer for fuzz_side Configuring CI to run fuzz from fuzz_side --- .github/workflows/fuzzing.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/fuzzing.yml b/.github/workflows/fuzzing.yml index 9ad1c17..8346e49 100644 --- a/.github/workflows/fuzzing.yml +++ b/.github/workflows/fuzzing.yml @@ -46,6 +46,7 @@ jobs: - { name: fuzz_ed, should_pass: true } - { name: fuzz_normal, should_pass: true } - { name: fuzz_patch, should_pass: true } + - { name: fuzz_side, should_pass: true } steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@nightly From c7d4140fa30c3c6ade716d0e6620248ce0d6dc4a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 9 Jun 2025 23:03:42 +0000 Subject: [PATCH 157/194] fix(deps): update rust crate unicode-width to v0.2.1 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6ff8fb5..e437e99 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -430,9 +430,9 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-width" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" [[package]] name = "wait-timeout" From 8261d790f4b7ae6298d6ff43a24395f6d5f66b92 Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Fri, 27 Jun 2025 10:45:40 +0200 Subject: [PATCH 158/194] clippy: fix warnings from uninlined_format_args --- src/cmp.rs | 16 ++++++---------- src/main.rs | 2 +- src/params.rs | 2 +- src/side_diff.rs | 5 +---- 4 files changed, 9 insertions(+), 16 deletions(-) diff --git a/src/cmp.rs b/src/cmp.rs index c0fc397..876c3ca 100644 --- a/src/cmp.rs +++ b/src/cmp.rs @@ -35,7 +35,7 @@ pub struct Params { #[inline] fn usage_string(executable: &str) -> String { - format!("Usage: {} ", executable) + format!("Usage: {executable} ") } #[cfg(not(target_os = "windows"))] @@ -75,8 +75,7 @@ pub fn parse_params>(mut opts: Peekable) -> Resu Err(e) if *e.kind() == std::num::IntErrorKind::PosOverflow => usize::MAX, Err(_) => { return Err(format!( - "{}: invalid --ignore-initial value '{}'", - executable_str, skip_desc + "{executable_str}: invalid --ignore-initial value '{skip_desc}'" )) } }; @@ -103,8 +102,7 @@ pub fn parse_params>(mut opts: Peekable) -> Resu "Y" => usize::MAX, // 1_208_925_819_614_629_174_706_176, _ => { return Err(format!( - "{}: invalid --ignore-initial value '{}'", - executable_str, skip_desc + "{executable_str}: invalid --ignore-initial value '{skip_desc}'" )); } }; @@ -170,8 +168,7 @@ pub fn parse_params>(mut opts: Peekable) -> Resu Err(e) if *e.kind() == std::num::IntErrorKind::PosOverflow => usize::MAX, Err(_) => { return Err(format!( - "{}: invalid --bytes value '{}'", - executable_str, max_bytes + "{executable_str}: invalid --bytes value '{max_bytes}'" )) } }; @@ -210,7 +207,7 @@ pub fn parse_params>(mut opts: Peekable) -> Resu std::process::exit(0); } if param_str.starts_with('-') { - return Err(format!("Unknown option: {:?}", param)); + return Err(format!("Unknown option: {param:?}")); } if from.is_none() { from = Some(param); @@ -236,8 +233,7 @@ pub fn parse_params>(mut opts: Peekable) -> Resu if params.quiet && params.verbose { return Err(format!( - "{}: options -l and -s are incompatible", - executable_str + "{executable_str}: options -l and -s are incompatible" )); } diff --git a/src/main.rs b/src/main.rs index badaaa0..b7c2712 100644 --- a/src/main.rs +++ b/src/main.rs @@ -73,7 +73,7 @@ fn main() -> ExitCode { Some("diff") => diff::main(args), Some("cmp") => cmp::main(args), Some(name) => { - eprintln!("{}: utility not supported", name); + eprintln!("{name}: utility not supported"); ExitCode::from(2) } None => second_arg_error(exe_name), diff --git a/src/params.rs b/src/params.rs index c64b3fc..b8483b7 100644 --- a/src/params.rs +++ b/src/params.rs @@ -195,7 +195,7 @@ pub fn parse_params>(mut opts: Peekable) -> Resu Err(error) => return Err(error), } if param.to_string_lossy().starts_with('-') { - return Err(format!("Unknown option: {:?}", param)); + return Err(format!("Unknown option: {param:?}")); } if from.is_none() { from = Some(param); diff --git a/src/side_diff.rs b/src/side_diff.rs index 72673d4..bb22002 100644 --- a/src/side_diff.rs +++ b/src/side_diff.rs @@ -950,10 +950,7 @@ mod tests { push_output(left_ln, right_ln, symbol, &mut buf, &config).unwrap(); let expected_left = format!("áéíóú\t\t\t\t\t\t\t\t"); let expected_right = "😀😃😄"; - assert_eq!( - buf, - format!("{}{}\n", expected_left, expected_right).as_bytes() - ); + assert_eq!(buf, format!("{expected_left}{expected_right}\n").as_bytes()); } } From 03fe6140873498ac24d4ddaca03813a9a2592973 Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Fri, 27 Jun 2025 10:50:06 +0200 Subject: [PATCH 159/194] clippy: fix warnings from useless_format lint --- src/side_diff.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/side_diff.rs b/src/side_diff.rs index bb22002..45bd325 100644 --- a/src/side_diff.rs +++ b/src/side_diff.rs @@ -901,7 +901,7 @@ mod tests { let symbol = b'<'; // impossible case, just to use different symbol let mut buf = vec![]; push_output(left_ln, right_ln, symbol, &mut buf, &config).unwrap(); - assert_eq!(buf, format!("data\t\t\t\t\t\t\t <\n").as_bytes()); + assert_eq!(buf, "data\t\t\t\t\t\t\t <\n".as_bytes()); } #[test] @@ -948,7 +948,7 @@ mod tests { let symbol = b' '; let mut buf = vec![]; push_output(left_ln, right_ln, symbol, &mut buf, &config).unwrap(); - let expected_left = format!("áéíóú\t\t\t\t\t\t\t\t"); + let expected_left = "áéíóú\t\t\t\t\t\t\t\t"; let expected_right = "😀😃😄"; assert_eq!(buf, format!("{expected_left}{expected_right}\n").as_bytes()); } From 7df02399ba5af0e2b944112a224d8e2d69462006 Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Fri, 27 Jun 2025 10:52:37 +0200 Subject: [PATCH 160/194] clippy: fix warning from ptr_arg lint --- src/side_diff.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/side_diff.rs b/src/side_diff.rs index 45bd325..56953d2 100644 --- a/src/side_diff.rs +++ b/src/side_diff.rs @@ -973,7 +973,7 @@ mod tests { } } - fn contains_string(vec: &Vec, s: &str) -> usize { + fn contains_string(vec: &[u8], s: &str) -> usize { let pattern = s.as_bytes(); vec.windows(pattern.len()).filter(|s| s == &pattern).count() } From b59d9be9437f061d0cbea34850322272b40238a6 Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Fri, 8 Aug 2025 11:48:58 +0200 Subject: [PATCH 161/194] clippy: fix warnings from unnecessary_unwrap lint --- src/params.rs | 40 ++++++++++++++++++---------------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/src/params.rs b/src/params.rs index b8483b7..7d7d4f8 100644 --- a/src/params.rs +++ b/src/params.rs @@ -279,17 +279,15 @@ fn match_context_diff_params( context_count = Some(numvalue.as_str().parse::().unwrap()); } } - if param == "-C" && next_param.is_some() { - match next_param.unwrap().to_string_lossy().parse::() { - Ok(context_size) => { - context_count = Some(context_size); - next_param_consumed = true; - } - Err(_) => { - return Err(format!( - "invalid context length '{}'", - next_param.unwrap().to_string_lossy() - )) + if param == "-C" { + if let Some(p) = next_param { + let size_str = p.to_string_lossy(); + match size_str.parse::() { + Ok(context_size) => { + context_count = Some(context_size); + next_param_consumed = true; + } + Err(_) => return Err(format!("invalid context length '{size_str}'")), } } } @@ -325,17 +323,15 @@ fn match_unified_diff_params( context_count = Some(numvalue.as_str().parse::().unwrap()); } } - if param == "-U" && next_param.is_some() { - match next_param.unwrap().to_string_lossy().parse::() { - Ok(context_size) => { - context_count = Some(context_size); - next_param_consumed = true; - } - Err(_) => { - return Err(format!( - "invalid context length '{}'", - next_param.unwrap().to_string_lossy() - )) + if param == "-U" { + if let Some(p) = next_param { + let size_str = p.to_string_lossy(); + match size_str.parse::() { + Ok(context_size) => { + context_count = Some(context_size); + next_param_consumed = true; + } + Err(_) => return Err(format!("invalid context length '{size_str}'")), } } } From cc67cbcc5906919b0d19d76c562b4778a9152a6d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 20 Aug 2025 01:43:49 +0000 Subject: [PATCH 162/194] chore(deps): update rust crate tempfile to v3.21.0 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e437e99..2faa9ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -405,9 +405,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.20.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" dependencies = [ "fastrand", "getrandom", From a680c4f4675211c6401da3e9aae96f8385777530 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 24 Aug 2025 16:38:04 +0000 Subject: [PATCH 163/194] fix(deps): update rust crate regex to v1.11.2 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2faa9ad..89b9cb5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -323,9 +323,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.1" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" dependencies = [ "aho-corasick", "memchr", From 611e380266d2f017fdfcdb95d49da364aab7d172 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 10:46:32 +0000 Subject: [PATCH 164/194] fix(deps): update rust crate chrono to v0.4.42 --- Cargo.lock | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 89b9cb5..fb7ae9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,12 +11,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -91,11 +85,10 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", @@ -548,9 +541,9 @@ dependencies = [ [[package]] name = "windows-link" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" [[package]] name = "windows-sys" From 3380bab93536dcd5b3a14970ac9867f1073b23b2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 9 Sep 2025 17:31:06 +0000 Subject: [PATCH 165/194] chore(deps): update rust crate tempfile to v3.22.0 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fb7ae9a..1272290 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -398,9 +398,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.21.0" +version = "3.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" +checksum = "84fa4d11fadde498443cca10fd3ac23c951f0dc59e080e9f4b93d4df4e4eea53" dependencies = [ "fastrand", "getrandom", From 644a794067002d5a0687e08ac081c41bbee918b6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 23 Sep 2025 10:46:28 +0000 Subject: [PATCH 166/194] chore(deps): update rust crate tempfile to v3.23.0 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1272290..1733a52 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -398,9 +398,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.22.0" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84fa4d11fadde498443cca10fd3ac23c951f0dc59e080e9f4b93d4df4e4eea53" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", "getrandom", From 392b8fa07b37a4d6a06e5251a47c5b14c5042e0a Mon Sep 17 00:00:00 2001 From: Olivier Tilloy Date: Wed, 24 Sep 2025 18:36:46 +0200 Subject: [PATCH 167/194] cargo dist: update to a recent release --- .github/workflows/release.yml | 133 ++++++++++++++++++++-------------- Cargo.toml | 13 ---- dist-workspace.toml | 15 ++++ 3 files changed, 94 insertions(+), 67 deletions(-) create mode 100644 dist-workspace.toml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4c1f4f8..e06d6fe 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,10 +1,12 @@ +# This file was autogenerated by dist: https://axodotdev.github.io/cargo-dist +# # Copyright 2022-2024, axodotdev # SPDX-License-Identifier: MIT or Apache-2.0 # # CI that: # # * checks for a Git Tag that looks like a release -# * builds artifacts with cargo-dist (archives, installers, hashes) +# * builds artifacts with dist (archives, installers, hashes) # * uploads those artifacts to temporary workflow zip # * on success, uploads the artifacts to a GitHub Release # @@ -12,9 +14,8 @@ # title/body based on your changelogs. name: Release - permissions: - contents: write + "contents": "write" # This task will run whenever you push a git tag that looks like a version # like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc. @@ -23,10 +24,10 @@ permissions: # must be a Cargo-style SemVer Version (must have at least major.minor.patch). # # If PACKAGE_NAME is specified, then the announcement will be for that -# package (erroring out if it doesn't have the given version or isn't cargo-dist-able). +# package (erroring out if it doesn't have the given version or isn't dist-able). # # If PACKAGE_NAME isn't specified, then the announcement will be for all -# (cargo-dist-able) packages in the workspace with that version (this mode is +# (dist-able) packages in the workspace with that version (this mode is # intended for workspaces with only one dist-able package, or with all dist-able # packages versioned/released in lockstep). # @@ -38,15 +39,15 @@ permissions: # If there's a prerelease-style suffix to the version, then the release(s) # will be marked as a prerelease. on: + pull_request: push: tags: - '**[0-9]+.[0-9]+.[0-9]+*' - pull_request: jobs: - # Run 'cargo dist plan' (or host) to determine what tasks we need to do + # Run 'dist plan' (or host) to determine what tasks we need to do plan: - runs-on: ubuntu-latest + runs-on: "ubuntu-22.04" outputs: val: ${{ steps.plan.outputs.manifest }} tag: ${{ !github.event.pull_request && github.ref_name || '' }} @@ -57,12 +58,18 @@ jobs: steps: - uses: actions/checkout@v4 with: + persist-credentials: false submodules: recursive - - name: Install cargo-dist + - name: Install dist # we specify bash to get pipefail; it guards against the `curl` command # failing. otherwise `sh` won't catch that `curl` returned non-0 shell: bash - run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.13.3/cargo-dist-installer.sh | sh" + run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.30.0/cargo-dist-installer.sh | sh" + - name: Cache dist + uses: actions/upload-artifact@v4 + with: + name: cargo-dist-cache + path: ~/.cargo/bin/dist # sure would be cool if github gave us proper conditionals... # so here's a doubly-nested ternary-via-truthiness to try to provide the best possible # functionality based on whether this is a pull_request, and whether it's from a fork. @@ -70,8 +77,8 @@ jobs: # but also really annoying to build CI around when it needs secrets to work right.) - id: plan run: | - cargo dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json - echo "cargo dist ran successfully" + dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json + echo "dist ran successfully" cat plan-dist-manifest.json echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT" - name: "Upload dist-manifest.json" @@ -89,18 +96,19 @@ jobs: if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }} strategy: fail-fast: false - # Target platforms/runners are computed by cargo-dist in create-release. + # Target platforms/runners are computed by dist in create-release. # Each member of the matrix has the following arguments: # # - runner: the github runner - # - dist-args: cli flags to pass to cargo dist - # - install-dist: expression to run to install cargo-dist on the runner + # - dist-args: cli flags to pass to dist + # - install-dist: expression to run to install dist on the runner # # Typically there will be: # - 1 "global" task that builds universal installers # - N "local" tasks that build each platform's binaries and platform-specific installers matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }} runs-on: ${{ matrix.runner }} + container: ${{ matrix.container && matrix.container.image || null }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json @@ -110,12 +118,17 @@ jobs: git config --global core.longpaths true - uses: actions/checkout@v4 with: + persist-credentials: false submodules: recursive - - uses: swatinem/rust-cache@v2 - with: - key: ${{ join(matrix.targets, '-') }} - - name: Install cargo-dist - run: ${{ matrix.install_dist }} + - name: Install Rust non-interactively if not already installed + if: ${{ matrix.container }} + run: | + if ! command -v cargo > /dev/null 2>&1; then + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + fi + - name: Install dist + run: ${{ matrix.install_dist.run }} # Get the dist-manifest - name: Fetch local artifacts uses: actions/download-artifact@v4 @@ -129,8 +142,8 @@ jobs: - name: Build artifacts run: | # Actually do builds and make zips and whatnot - cargo dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json - echo "cargo dist ran successfully" + dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json + echo "dist ran successfully" - id: cargo-dist name: Post-build # We force bash here just because github makes it really hard to get values up @@ -140,7 +153,7 @@ jobs: run: | # Parse out what we just built and upload it to scratch storage echo "paths<> "$GITHUB_OUTPUT" - jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" + dist print-upload-files-from-manifest --manifest dist-manifest.json >> "$GITHUB_OUTPUT" echo "EOF" >> "$GITHUB_OUTPUT" cp dist-manifest.json "$BUILD_MANIFEST_NAME" @@ -157,17 +170,21 @@ jobs: needs: - plan - build-local-artifacts - runs-on: "ubuntu-20.04" + runs-on: "ubuntu-22.04" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json steps: - uses: actions/checkout@v4 with: + persist-credentials: false submodules: recursive - - name: Install cargo-dist - shell: bash - run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.13.3/cargo-dist-installer.sh | sh" + - name: Install cached dist + uses: actions/download-artifact@v4 + with: + name: cargo-dist-cache + path: ~/.cargo/bin/ + - run: chmod +x ~/.cargo/bin/dist # Get all the local artifacts for the global tasks to use (for e.g. checksums) - name: Fetch local artifacts uses: actions/download-artifact@v4 @@ -178,8 +195,8 @@ jobs: - id: cargo-dist shell: bash run: | - cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json - echo "cargo dist ran successfully" + dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json + echo "dist ran successfully" # Parse out what we just built and upload it to scratch storage echo "paths<> "$GITHUB_OUTPUT" @@ -204,15 +221,20 @@ jobs: if: ${{ always() && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - runs-on: "ubuntu-20.04" + runs-on: "ubuntu-22.04" outputs: val: ${{ steps.host.outputs.manifest }} steps: - uses: actions/checkout@v4 with: + persist-credentials: false submodules: recursive - - name: Install cargo-dist - run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.13.3/cargo-dist-installer.sh | sh" + - name: Install cached dist + uses: actions/download-artifact@v4 + with: + name: cargo-dist-cache + path: ~/.cargo/bin/ + - run: chmod +x ~/.cargo/bin/dist # Fetch artifacts from scratch-storage - name: Fetch artifacts uses: actions/download-artifact@v4 @@ -220,11 +242,10 @@ jobs: pattern: artifacts-* path: target/distrib/ merge-multiple: true - # This is a harmless no-op for GitHub Releases, hosting for that happens in "announce" - id: host shell: bash run: | - cargo dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json + dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json echo "artifacts uploaded and released successfully" cat dist-manifest.json echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" @@ -234,8 +255,29 @@ jobs: # Overwrite the previous copy name: artifacts-dist-manifest path: dist-manifest.json + # Create a GitHub Release while uploading all files to it + - name: "Download GitHub Artifacts" + uses: actions/download-artifact@v4 + with: + pattern: artifacts-* + path: artifacts + merge-multiple: true + - name: Cleanup + run: | + # Remove the granular manifests + rm -f artifacts/*-dist-manifest.json + - name: Create GitHub Release + env: + PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}" + ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}" + ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}" + RELEASE_COMMIT: "${{ github.sha }}" + run: | + # Write and read notes from a file to avoid quoting breaking things + echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt + + gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/* - # Create a GitHub Release while uploading all files to it announce: needs: - plan @@ -244,28 +286,11 @@ jobs: # still allowing individual publish jobs to skip themselves (for prereleases). # "host" however must run to completion, no skipping allowed! if: ${{ always() && needs.host.result == 'success' }} - runs-on: "ubuntu-20.04" + runs-on: "ubuntu-22.04" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@v4 with: + persist-credentials: false submodules: recursive - - name: "Download GitHub Artifacts" - uses: actions/download-artifact@v4 - with: - pattern: artifacts-* - path: artifacts - merge-multiple: true - - name: Cleanup - run: | - # Remove the granular manifests - rm -f artifacts/*-dist-manifest.json - - name: Create GitHub Release - uses: ncipollo/release-action@v1 - with: - tag: ${{ needs.plan.outputs.tag }} - name: ${{ fromJson(needs.host.outputs.val).announcement_title }} - body: ${{ fromJson(needs.host.outputs.val).announcement_github_body }} - prerelease: ${{ fromJson(needs.host.outputs.val).announcement_is_prerelease }} - artifacts: "artifacts/*" diff --git a/Cargo.toml b/Cargo.toml index 6fa1a3c..6dd93f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,16 +32,3 @@ tempfile = "3.10.1" [profile.dist] inherits = "release" lto = "thin" - -# Config for 'cargo dist' -[workspace.metadata.dist] -# The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax) -cargo-dist-version = "0.13.3" -# CI backends to support -ci = ["github"] -# The installers to generate for each app -installers = [] -# Target platforms to build apps for (Rust target-triple syntax) -targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"] -# Publish jobs to run in CI -pr-run-mode = "plan" diff --git a/dist-workspace.toml b/dist-workspace.toml new file mode 100644 index 0000000..f31b58b --- /dev/null +++ b/dist-workspace.toml @@ -0,0 +1,15 @@ +[workspace] +members = ["cargo:."] + +# Config for 'dist' +[dist] +# The preferred dist version to use in CI (Cargo.toml SemVer syntax) +cargo-dist-version = "0.30.0" +# CI backends to support +ci = "github" +# The installers to generate for each app +installers = [] +# Target platforms to build apps for (Rust target-triple syntax) +targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"] +# Which actions to run on pull requests +pr-run-mode = "plan" From dbd60416e61665ab30b1a308825eaa8b5cbf9a55 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 9 Oct 2025 10:55:20 +0000 Subject: [PATCH 168/194] chore(deps): update rust crate regex to v1.11.3 --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1733a52..fa0ac3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -316,9 +316,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.2" +version = "1.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" +checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" dependencies = [ "aho-corasick", "memchr", @@ -328,9 +328,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.8" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" dependencies = [ "aho-corasick", "memchr", From 2806ec2029b8496368ba7f253942fc87c013549f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 9 Oct 2025 10:55:25 +0000 Subject: [PATCH 169/194] chore(deps): update rust crate unicode-width to v0.2.2 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fa0ac3c..82e7eba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -423,9 +423,9 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-width" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "wait-timeout" From eadc8c3dc59141b231b204848df68709e6ff4d4c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 11 Oct 2025 00:35:41 +0000 Subject: [PATCH 170/194] chore(deps): update rust crate regex to v1.12.1 --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 82e7eba..7f1438b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -316,9 +316,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.3" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" +checksum = "4a52d8d02cacdb176ef4678de6c052efb4b3da14b78e4db683a4252762be5433" dependencies = [ "aho-corasick", "memchr", @@ -328,9 +328,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" +checksum = "722166aa0d7438abbaa4d5cc2c649dac844e8c56d82fb3d33e9c34b5cd268fc6" dependencies = [ "aho-corasick", "memchr", From 590a4b405e648a25da6970393512019f443c7c4c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 18:59:08 +0000 Subject: [PATCH 171/194] chore(deps): update rust crate regex to v1.12.2 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7f1438b..e1b561e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -316,9 +316,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.1" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a52d8d02cacdb176ef4678de6c052efb4b3da14b78e4db683a4252762be5433" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", From 30b6bd2523132045c74d6936595f52fa39f27276 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 22:53:14 +0000 Subject: [PATCH 172/194] chore(deps): update rust crate assert_cmd to v2.1.0 --- Cargo.lock | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e1b561e..8e8c9d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,13 +28,12 @@ checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" [[package]] name = "assert_cmd" -version = "2.0.17" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bd389a4b2970a01282ee455294913c0a43724daedcd1a24c3eb0ec1c1320b66" +checksum = "ade3e59755df470dc884dbd126416d098b783b193fdc1981b392eb79398a57a3" dependencies = [ "anstyle", "bstr", - "doc-comment", "libc", "predicates", "predicates-core", @@ -130,12 +129,6 @@ dependencies = [ "unicode-width", ] -[[package]] -name = "doc-comment" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" - [[package]] name = "errno" version = "0.3.10" From 15473edcd7b81a622f822a71f025875759f3037a Mon Sep 17 00:00:00 2001 From: E <79379754+oech3@users.noreply.github.com> Date: Wed, 29 Oct 2025 23:13:16 +0900 Subject: [PATCH 173/194] Cargo.toml sync 2 profiles with other uutils --- Cargo.toml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6dd93f1..2c1b4d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,10 @@ assert_cmd = "2.0.14" predicates = "3.1.0" tempfile = "3.10.1" -# The profile that 'cargo dist' will build with -[profile.dist] -inherits = "release" +[profile.release] lto = "thin" +codegen-units = 1 + +[profile.release-fast] +inherits = "release" +panic = "abort" From 5f2ba7a84c18bbd9cdf7965cc98d73dddc53f190 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 15:19:26 +0000 Subject: [PATCH 174/194] chore(deps): update rust crate assert_cmd to v2.1.1 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8e8c9d5..c22fb0b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,9 +28,9 @@ checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" [[package]] name = "assert_cmd" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade3e59755df470dc884dbd126416d098b783b193fdc1981b392eb79398a57a3" +checksum = "bcbb6924530aa9e0432442af08bbcafdad182db80d2e560da42a6d442535bf85" dependencies = [ "anstyle", "bstr", From 83f6d2db7c352010351afb1a16d99cd330571862 Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Fri, 31 Oct 2025 10:19:13 +0100 Subject: [PATCH 175/194] tests: fix deprecation warnings from assert_cmd --- tests/integration.rs | 92 ++++++++++++++++++++++---------------------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/tests/integration.rs b/tests/integration.rs index cfbf529..476660f 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -3,7 +3,7 @@ // For the full copyright and license information, please view the LICENSE-* // files that was distributed with this source code. -use assert_cmd::cmd::Command; +use assert_cmd::cargo::cargo_bin_cmd; use predicates::prelude::*; use std::fs::File; #[cfg(not(windows))] @@ -17,14 +17,14 @@ mod common { #[test] fn unknown_param() -> Result<(), Box> { - let mut cmd = Command::cargo_bin("diffutils")?; + let mut cmd = cargo_bin_cmd!("diffutils"); cmd.arg("patch"); cmd.assert() .code(predicate::eq(2)) .failure() .stderr(predicate::eq("patch: utility not supported\n")); - let mut cmd = Command::cargo_bin("diffutils")?; + let mut cmd = cargo_bin_cmd!("diffutils"); cmd.assert() .code(predicate::eq(0)) .success() @@ -33,7 +33,7 @@ mod common { )); for subcmd in ["diff", "cmp"] { - let mut cmd = Command::cargo_bin("diffutils")?; + let mut cmd = cargo_bin_cmd!("diffutils"); cmd.arg(subcmd); cmd.arg("--foobar"); cmd.assert() @@ -58,7 +58,7 @@ mod common { let error_message = "The system cannot find the file specified."; for subcmd in ["diff", "cmp"] { - let mut cmd = Command::cargo_bin("diffutils")?; + let mut cmd = cargo_bin_cmd!("diffutils"); cmd.arg(subcmd); cmd.arg(&nopath).arg(file.path()); cmd.assert() @@ -69,7 +69,7 @@ mod common { &nopath.as_os_str().to_string_lossy() ))); - let mut cmd = Command::cargo_bin("diffutils")?; + let mut cmd = cargo_bin_cmd!("diffutils"); cmd.arg(subcmd); cmd.arg(file.path()).arg(&nopath); cmd.assert() @@ -81,7 +81,7 @@ mod common { ))); } - let mut cmd = Command::cargo_bin("diffutils")?; + let mut cmd = cargo_bin_cmd!("diffutils"); cmd.arg("diff"); cmd.arg(&nopath).arg(&nopath); cmd.assert().code(predicate::eq(2)).failure().stderr( @@ -105,7 +105,7 @@ mod diff { fn no_differences() -> Result<(), Box> { let file = NamedTempFile::new()?; for option in ["", "-u", "-c", "-e"] { - let mut cmd = Command::cargo_bin("diffutils")?; + let mut cmd = cargo_bin_cmd!("diffutils"); cmd.arg("diff"); if !option.is_empty() { cmd.arg(option); @@ -125,7 +125,7 @@ mod diff { let mut file1 = NamedTempFile::new()?; file1.write_all("foo\n".as_bytes())?; for option in ["", "-u", "-c", "-e"] { - let mut cmd = Command::cargo_bin("diffutils")?; + let mut cmd = cargo_bin_cmd!("diffutils"); cmd.arg("diff"); if !option.is_empty() { cmd.arg(option); @@ -144,7 +144,7 @@ mod diff { let mut file2 = NamedTempFile::new()?; file2.write_all("foo\n".as_bytes())?; for option in ["", "-u", "-c", "-e"] { - let mut cmd = Command::cargo_bin("diffutils")?; + let mut cmd = cargo_bin_cmd!("diffutils"); cmd.arg("diff"); if !option.is_empty() { cmd.arg(option); @@ -169,7 +169,7 @@ mod diff { let mut file2 = NamedTempFile::new()?; file2.write_all("bar\n".as_bytes())?; for option in ["", "-u", "-c", "-e"] { - let mut cmd = Command::cargo_bin("diffutils")?; + let mut cmd = cargo_bin_cmd!("diffutils"); cmd.arg("diff"); if !option.is_empty() { cmd.arg(option); @@ -190,7 +190,7 @@ mod diff { let mut file2 = NamedTempFile::new()?; file2.write_all("bar\n".as_bytes())?; for option in ["", "-u", "-c", "-e"] { - let mut cmd = Command::cargo_bin("diffutils")?; + let mut cmd = cargo_bin_cmd!("diffutils"); cmd.arg("diff"); if !option.is_empty() { cmd.arg(option); @@ -214,7 +214,7 @@ mod diff { file1.write_all("foo".as_bytes())?; let mut file2 = NamedTempFile::new()?; file2.write_all("bar".as_bytes())?; - let mut cmd = Command::cargo_bin("diffutils")?; + let mut cmd = cargo_bin_cmd!("diffutils"); cmd.arg("diff"); cmd.arg("-e").arg(file1.path()).arg(file2.path()); cmd.assert() @@ -231,7 +231,7 @@ mod diff { let mut file2 = NamedTempFile::new()?; file2.write_all("bar\n".as_bytes())?; - let mut cmd = Command::cargo_bin("diffutils")?; + let mut cmd = cargo_bin_cmd!("diffutils"); cmd.arg("diff"); cmd.arg("-u") .arg(file1.path()) @@ -248,7 +248,7 @@ mod diff { ) ); - let mut cmd = Command::cargo_bin("diffutils")?; + let mut cmd = cargo_bin_cmd!("diffutils"); cmd.arg("diff"); cmd.arg("-u") .arg("-") @@ -265,7 +265,7 @@ mod diff { ) ); - let mut cmd = Command::cargo_bin("diffutils")?; + let mut cmd = cargo_bin_cmd!("diffutils"); cmd.arg("diff"); cmd.arg("-u").arg("-").arg("-"); cmd.assert() @@ -275,7 +275,7 @@ mod diff { #[cfg(unix)] { - let mut cmd = Command::cargo_bin("diffutils")?; + let mut cmd = cargo_bin_cmd!("diffutils"); cmd.arg("diff"); cmd.arg("-u") .arg(file1.path()) @@ -311,7 +311,7 @@ mod diff { let mut da = File::create(&da_path).unwrap(); da.write_all(b"da\n").unwrap(); - let mut cmd = Command::cargo_bin("diffutils")?; + let mut cmd = cargo_bin_cmd!("diffutils"); cmd.arg("diff"); cmd.arg("-u").arg(&directory).arg(&a_path); cmd.assert().code(predicate::eq(1)).failure(); @@ -326,7 +326,7 @@ mod diff { ) ); - let mut cmd = Command::cargo_bin("diffutils")?; + let mut cmd = cargo_bin_cmd!("diffutils"); cmd.arg("diff"); cmd.arg("-u").arg(&a_path).arg(&directory); cmd.assert().code(predicate::eq(1)).failure(); @@ -350,7 +350,7 @@ mod cmp { #[test] fn cmp_incompatible_params() -> Result<(), Box> { - let mut cmd = Command::cargo_bin("diffutils")?; + let mut cmd = cargo_bin_cmd!("diffutils"); cmd.arg("cmp"); cmd.arg("-l"); cmd.arg("-s"); @@ -373,7 +373,7 @@ mod cmp { let mut a = File::create(&a_path).unwrap(); a.write_all(b"a\n").unwrap(); - let mut cmd = Command::cargo_bin("diffutils")?; + let mut cmd = cargo_bin_cmd!("diffutils"); cmd.arg("cmp"); cmd.arg(&a_path); cmd.write_stdin("a\n"); @@ -383,7 +383,7 @@ mod cmp { .stderr(predicate::str::is_empty()) .stdout(predicate::str::is_empty()); - let mut cmd = Command::cargo_bin("diffutils")?; + let mut cmd = cargo_bin_cmd!("diffutils"); cmd.env("LC_ALL", "C"); cmd.arg("cmp"); cmd.arg(&a_path); @@ -409,7 +409,7 @@ mod cmp { let mut b = File::create(&b_path).unwrap(); b.write_all(b"a\n").unwrap(); - let mut cmd = Command::cargo_bin("diffutils")?; + let mut cmd = cargo_bin_cmd!("diffutils"); cmd.arg("cmp"); cmd.arg(&a_path).arg(&b_path); cmd.assert() @@ -432,7 +432,7 @@ mod cmp { let b_path = tmp_dir.path().join("b"); let _ = File::create(&b_path).unwrap(); - let mut cmd = Command::cargo_bin("diffutils")?; + let mut cmd = cargo_bin_cmd!("diffutils"); cmd.arg("cmp"); cmd.arg(&a_path).arg(&b_path); cmd.assert() @@ -456,7 +456,7 @@ mod cmp { let mut b = File::create(&b_path).unwrap(); b.write_all(b"bcd\n").unwrap(); - let mut cmd = Command::cargo_bin("diffutils")?; + let mut cmd = cargo_bin_cmd!("diffutils"); cmd.env("LC_ALL", "C"); cmd.arg("cmp"); cmd.arg(&a_path).arg(&b_path); @@ -465,7 +465,7 @@ mod cmp { .failure() .stdout(predicate::str::ends_with(" differ: char 1, line 1\n")); - let mut cmd = Command::cargo_bin("diffutils")?; + let mut cmd = cargo_bin_cmd!("diffutils"); cmd.env("LC_ALL", "C"); cmd.arg("cmp"); cmd.arg("-b"); @@ -478,7 +478,7 @@ mod cmp { " differ: byte 1, line 1 is 141 a 142 b\n", )); - let mut cmd = Command::cargo_bin("diffutils")?; + let mut cmd = cargo_bin_cmd!("diffutils"); cmd.env("LC_ALL", "C"); cmd.arg("cmp"); cmd.arg("-l"); @@ -489,7 +489,7 @@ mod cmp { .stderr(predicate::str::is_empty()) .stdout(predicate::eq("1 141 142\n2 142 143\n3 143 144\n")); - let mut cmd = Command::cargo_bin("diffutils")?; + let mut cmd = cargo_bin_cmd!("diffutils"); cmd.env("LC_ALL", "C"); cmd.arg("cmp"); cmd.arg("-l"); @@ -518,7 +518,7 @@ mod cmp { let mut b = File::create(&b_path).unwrap(); b.write_all(b"abc\ndef\ng").unwrap(); - let mut cmd = Command::cargo_bin("diffutils")?; + let mut cmd = cargo_bin_cmd!("diffutils"); cmd.env("LC_ALL", "C"); cmd.arg("cmp"); cmd.arg(&a_path).arg(&b_path); @@ -528,7 +528,7 @@ mod cmp { .stderr(predicate::str::is_empty()) .stdout(predicate::str::ends_with(" differ: char 8, line 2\n")); - let mut cmd = Command::cargo_bin("diffutils")?; + let mut cmd = cargo_bin_cmd!("diffutils"); cmd.env("LC_ALL", "C"); cmd.arg("cmp"); cmd.arg("-b"); @@ -541,7 +541,7 @@ mod cmp { " differ: byte 8, line 2 is 147 g 12 ^J\n", )); - let mut cmd = Command::cargo_bin("diffutils")?; + let mut cmd = cargo_bin_cmd!("diffutils"); cmd.env("LC_ALL", "C"); cmd.arg("cmp"); cmd.arg("-l"); @@ -553,7 +553,7 @@ mod cmp { .stderr(predicate::str::contains(" EOF on")) .stderr(predicate::str::ends_with(" after byte 8\n")); - let mut cmd = Command::cargo_bin("diffutils")?; + let mut cmd = cargo_bin_cmd!("diffutils"); cmd.env("LC_ALL", "C"); cmd.arg("cmp"); cmd.arg("-b"); @@ -581,7 +581,7 @@ mod cmp { let mut b = File::create(&b_path).unwrap(); b.write_all(b"abcdefghijkl\n").unwrap(); - let mut cmd = Command::cargo_bin("diffutils")?; + let mut cmd = cargo_bin_cmd!("diffutils"); cmd.arg("cmp"); cmd.arg("-l"); cmd.arg("-b"); @@ -594,7 +594,7 @@ mod cmp { .stderr(predicate::str::is_empty()) .stdout(predicate::str::is_empty()); - let mut cmd = Command::cargo_bin("diffutils")?; + let mut cmd = cargo_bin_cmd!("diffutils"); cmd.arg("cmp"); cmd.arg("-l"); cmd.arg("-b"); @@ -607,7 +607,7 @@ mod cmp { .stderr(predicate::str::is_empty()) .stdout(predicate::eq("4 40 144 d\n")); - let mut cmd = Command::cargo_bin("diffutils")?; + let mut cmd = cargo_bin_cmd!("diffutils"); cmd.arg("cmp"); cmd.arg("-l"); cmd.arg("-b"); @@ -634,7 +634,7 @@ mod cmp { let mut b = File::create(&b_path).unwrap(); b.write_all(b"###abc\n").unwrap(); - let mut cmd = Command::cargo_bin("diffutils")?; + let mut cmd = cargo_bin_cmd!("diffutils"); cmd.env("LC_ALL", "C"); cmd.arg("cmp"); cmd.arg("-i"); @@ -647,7 +647,7 @@ mod cmp { .stdout(predicate::str::is_empty()); // Positional skips should be ignored - let mut cmd = Command::cargo_bin("diffutils")?; + let mut cmd = cargo_bin_cmd!("diffutils"); cmd.env("LC_ALL", "C"); cmd.arg("cmp"); cmd.arg("-i"); @@ -661,7 +661,7 @@ mod cmp { .stdout(predicate::str::is_empty()); // Single positional argument should only affect first file. - let mut cmd = Command::cargo_bin("diffutils")?; + let mut cmd = cargo_bin_cmd!("diffutils"); cmd.env("LC_ALL", "C"); cmd.arg("cmp"); cmd.arg(&a_path).arg(&b_path); @@ -672,7 +672,7 @@ mod cmp { .stderr(predicate::str::is_empty()) .stdout(predicate::str::ends_with(" differ: char 1, line 1\n")); - let mut cmd = Command::cargo_bin("diffutils")?; + let mut cmd = cargo_bin_cmd!("diffutils"); cmd.env("LC_ALL", "C"); cmd.arg("cmp"); cmd.arg(&a_path).arg(&b_path); @@ -701,7 +701,7 @@ mod cmp { writeln!(b, "{}c", "b".repeat(1024)).unwrap(); b.flush().unwrap(); - let mut cmd = Command::cargo_bin("diffutils")?; + let mut cmd = cargo_bin_cmd!("diffutils"); cmd.arg("cmp"); cmd.arg("--ignore-initial=1K"); cmd.arg(&a_path).arg(&b_path); @@ -726,7 +726,7 @@ mod cmp { let mut b = File::create(&b_path).unwrap(); b.write_all(b"abcdefghijkl\n").unwrap(); - let mut cmd = Command::cargo_bin("diffutils")?; + let mut cmd = cargo_bin_cmd!("diffutils"); cmd.arg("cmp"); cmd.arg("-l"); cmd.arg("-b"); @@ -739,7 +739,7 @@ mod cmp { .stderr(predicate::str::is_empty()) .stdout(predicate::str::is_empty()); - let mut cmd = Command::cargo_bin("diffutils")?; + let mut cmd = cargo_bin_cmd!("diffutils"); cmd.arg("cmp"); cmd.arg("-b"); cmd.arg("-i"); @@ -772,7 +772,7 @@ mod cmp { let mut b = File::create(&b_path).unwrap(); b.write_all(&bytes).unwrap(); - let mut cmd = Command::cargo_bin("diffutils")?; + let mut cmd = cargo_bin_cmd!("diffutils"); cmd.arg("cmp"); cmd.arg("-l"); cmd.arg("-b"); @@ -817,7 +817,7 @@ mod cmp { let dev_null = OpenOptions::new().write(true).open("/dev/null").unwrap(); - let mut child = std::process::Command::new(assert_cmd::cargo::cargo_bin("diffutils")) + let mut child = std::process::Command::new(assert_cmd::cargo::cargo_bin!("diffutils")) .arg("cmp") .arg(&a_path) .arg(&b_path) @@ -830,7 +830,7 @@ mod cmp { assert_eq!(child.try_wait().unwrap().unwrap().code(), Some(1)); // Two stdins should be equal - let mut cmd = Command::cargo_bin("diffutils")?; + let mut cmd = cargo_bin_cmd!("diffutils"); cmd.arg("cmp"); cmd.arg("-"); cmd.arg("-"); @@ -861,7 +861,7 @@ mod cmp { b.write_all(bytes).unwrap(); b.write_all(b"B").unwrap(); - let mut cmd = Command::cargo_bin("diffutils")?; + let mut cmd = cargo_bin_cmd!("diffutils"); cmd.arg("cmp"); cmd.arg(&a_path).arg(&b_path); cmd.assert() From 67589b933173ae9e501e99d69a2c84120bfd353a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 12 Dec 2025 02:51:06 +0000 Subject: [PATCH 176/194] chore(deps): update actions/cache action to v5 --- .github/workflows/fuzzing.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/fuzzing.yml b/.github/workflows/fuzzing.yml index 8346e49..133946c 100644 --- a/.github/workflows/fuzzing.yml +++ b/.github/workflows/fuzzing.yml @@ -57,7 +57,7 @@ jobs: shared-key: "cargo-fuzz-cache-key" cache-directories: "fuzz/target" - name: Restore Cached Corpus - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: key: corpus-cache-${{ matrix.test-target.name }} path: | @@ -68,7 +68,7 @@ jobs: run: | cargo +nightly fuzz run ${{ matrix.test-target.name }} -- -max_total_time=${{ env.RUN_FOR }} -detect_leaks=0 - name: Save Corpus Cache - uses: actions/cache/save@v4 + uses: actions/cache/save@v5 with: key: corpus-cache-${{ matrix.test-target.name }} path: | From 1e1e968027bc7f38c1a6831add118af168dc8a32 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 21 Dec 2025 05:08:26 +0000 Subject: [PATCH 177/194] chore(deps): update rust crate itoa to v1.0.16 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c22fb0b..c27dec2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -191,9 +191,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "7ee5b5339afb4c41626dde77b7a611bd4f2c202b897852b4bcf5d03eddc61010" [[package]] name = "js-sys" From a09dcac41db5d0f0fdc5a381e042ed474676c6d3 Mon Sep 17 00:00:00 2001 From: LunarEclipse Date: Mon, 24 Nov 2025 19:43:44 +0100 Subject: [PATCH 178/194] Fix compilation for 32-bit targets --- src/cmp.rs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/cmp.rs b/src/cmp.rs index 876c3ca..d53df4f 100644 --- a/src/cmp.rs +++ b/src/cmp.rs @@ -90,12 +90,22 @@ pub fn parse_params>(mut opts: Peekable) -> Resu "M" => 1_048_576, "GB" => 1_000_000_000, "G" => 1_073_741_824, - "TB" => 1_000_000_000_000, - "T" => 1_099_511_627_776, - "PB" => 1_000_000_000_000_000, - "P" => 1_125_899_906_842_624, - "EB" => 1_000_000_000_000_000_000, - "E" => 1_152_921_504_606_846_976, + // This only generates a warning when compiling for target_pointer_width < 64 + #[allow(unused_variables)] + suffix @ ("TB" | "T" | "PB" | "P" | "EB" | "E") => { + #[cfg(target_pointer_width = "64")] + match suffix { + "TB" => 1_000_000_000_000, + "T" => 1_099_511_627_776, + "PB" => 1_000_000_000_000_000, + "P" => 1_125_899_906_842_624, + "EB" => 1_000_000_000_000_000_000, + "E" => 1_152_921_504_606_846_976, + _ => unreachable!(), + } + #[cfg(not(target_pointer_width = "64"))] + usize::MAX + } "ZB" => usize::MAX, // 1_000_000_000_000_000_000_000, "Z" => usize::MAX, // 1_180_591_620_717_411_303_424, "YB" => usize::MAX, // 1_000_000_000_000_000_000_000_000, From f25cad849791582d96e3aeec51d752337d2d4834 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 24 Dec 2025 01:55:24 +0000 Subject: [PATCH 179/194] chore(deps): update rust crate tempfile to v3.24.0 --- Cargo.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c27dec2..b80bdcd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -206,15 +206,15 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.170" +version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "linux-raw-sys" -version = "0.9.2" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db9c683daf087dc577b7506e9695b3d556a9f3849903fa28186283afd6809e9" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "log" @@ -338,9 +338,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rustix" -version = "1.0.0" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f8dcd64f141950290e45c99f7710ede1b600297c91818bb30b3667c0f45dc0" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ "bitflags", "errno", @@ -391,9 +391,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.23.0" +version = "3.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" dependencies = [ "fastrand", "getrandom", From 8997ac06b8eea7ae273afe03fc1dcb850057c65b Mon Sep 17 00:00:00 2001 From: Gustavo Noronha Silva Date: Mon, 22 Dec 2025 13:03:44 -0300 Subject: [PATCH 180/194] Use specific locale for cmp_fast_path test The test was failing in the regular MacOS terminal due to it defaulting to LC_ALL=C. Best to standardize like the other tests that check for locale-dependent output. --- tests/integration.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration.rs b/tests/integration.rs index 476660f..41c9a84 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -864,6 +864,7 @@ mod cmp { let mut cmd = cargo_bin_cmd!("diffutils"); cmd.arg("cmp"); cmd.arg(&a_path).arg(&b_path); + cmd.env("LC_ALL", "en_US"); cmd.assert() .code(predicate::eq(1)) .failure() From 7ddc6c6c4befe6b648509b1aa3260c573e53cd8a Mon Sep 17 00:00:00 2001 From: Gustavo Noronha Silva Date: Sun, 21 Dec 2025 11:38:30 -0300 Subject: [PATCH 181/194] Make cmp_fast_path more robust On my Mac I see this test fail quite consistently. This change makes it more resilient in systems with slower startup times, while still allowing faster systems to finish as soon as possible. --- tests/integration.rs | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/tests/integration.rs b/tests/integration.rs index 41c9a84..b37d7e6 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -825,9 +825,24 @@ mod cmp { .spawn() .unwrap(); - std::thread::sleep(std::time::Duration::from_millis(100)); - - assert_eq!(child.try_wait().unwrap().unwrap().code(), Some(1)); + // Bound the runtime to a very short time that still allows for some resource + // constraint to slow it down while also allowing very fast systems to exit as + // early as possible. + const MAX_TRIES: u8 = 50; + for tries in 0..=MAX_TRIES { + if tries == MAX_TRIES { + panic!("cmp took too long to run, /dev/null optimization probably not working") + } + match child.try_wait() { + Ok(Some(status)) => { + assert_eq!(status.code(), Some(1)); + break; + } + Ok(None) => (), + Err(e) => panic!("{e:#?}"), + } + std::thread::sleep(std::time::Duration::from_millis(10)); + } // Two stdins should be equal let mut cmd = cargo_bin_cmd!("diffutils"); From 940a0e00b6666f00a00d933a311f75fe3b949181 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 27 Dec 2025 09:24:33 +0000 Subject: [PATCH 182/194] chore(deps): update rust crate itoa to v1.0.17 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b80bdcd..86dc16a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -191,9 +191,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ee5b5339afb4c41626dde77b7a611bd4f2c202b897852b4bcf5d03eddc61010" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "js-sys" From 16673cf46668fc38c2bc324aff96d24620438834 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Thu, 1 Jan 2026 18:40:17 +0100 Subject: [PATCH 183/194] prepare version 0.5.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 86dc16a..aa11cc7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -115,7 +115,7 @@ checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" [[package]] name = "diffutils" -version = "0.4.2" +version = "0.5.0" dependencies = [ "assert_cmd", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 2c1b4d9..32e47ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "diffutils" -version = "0.4.2" +version = "0.5.0" edition = "2021" description = "A CLI app for generating diff files" license = "MIT OR Apache-2.0" From c6e8b46d219d46dbfdfc81309a80d67161631944 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Thu, 1 Jan 2026 19:53:45 +0100 Subject: [PATCH 184/194] update cargo dist --- .github/workflows/release.yml | 144 ++++++++++++++-------------------- Cargo.toml | 18 +++++ dist-workspace.toml | 4 +- 3 files changed, 77 insertions(+), 89 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e06d6fe..502a5ff 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,21 +1,20 @@ -# This file was autogenerated by dist: https://axodotdev.github.io/cargo-dist -# -# Copyright 2022-2024, axodotdev +# Copyright 2022-2023, axodotdev # SPDX-License-Identifier: MIT or Apache-2.0 # # CI that: # # * checks for a Git Tag that looks like a release -# * builds artifacts with dist (archives, installers, hashes) +# * builds artifacts with cargo-dist (archives, installers, hashes) # * uploads those artifacts to temporary workflow zip -# * on success, uploads the artifacts to a GitHub Release +# * on success, uploads the artifacts to a Github Release # -# Note that the GitHub Release will be created with a generated +# Note that the Github Release will be created with a generated # title/body based on your changelogs. name: Release + permissions: - "contents": "write" + contents: write # This task will run whenever you push a git tag that looks like a version # like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc. @@ -24,30 +23,30 @@ permissions: # must be a Cargo-style SemVer Version (must have at least major.minor.patch). # # If PACKAGE_NAME is specified, then the announcement will be for that -# package (erroring out if it doesn't have the given version or isn't dist-able). +# package (erroring out if it doesn't have the given version or isn't cargo-dist-able). # # If PACKAGE_NAME isn't specified, then the announcement will be for all -# (dist-able) packages in the workspace with that version (this mode is +# (cargo-dist-able) packages in the workspace with that version (this mode is # intended for workspaces with only one dist-able package, or with all dist-able # packages versioned/released in lockstep). # # If you push multiple tags at once, separate instances of this workflow will -# spin up, creating an independent announcement for each one. However, GitHub +# spin up, creating an independent announcement for each one. However Github # will hard limit this to 3 tags per commit, as it will assume more tags is a # mistake. # # If there's a prerelease-style suffix to the version, then the release(s) # will be marked as a prerelease. on: - pull_request: push: tags: - '**[0-9]+.[0-9]+.[0-9]+*' + pull_request: jobs: - # Run 'dist plan' (or host) to determine what tasks we need to do + # Run 'cargo dist plan' (or host) to determine what tasks we need to do plan: - runs-on: "ubuntu-22.04" + runs-on: ubuntu-latest outputs: val: ${{ steps.plan.outputs.manifest }} tag: ${{ !github.event.pull_request && github.ref_name || '' }} @@ -58,18 +57,12 @@ jobs: steps: - uses: actions/checkout@v4 with: - persist-credentials: false submodules: recursive - - name: Install dist + - name: Install cargo-dist # we specify bash to get pipefail; it guards against the `curl` command # failing. otherwise `sh` won't catch that `curl` returned non-0 shell: bash - run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.30.0/cargo-dist-installer.sh | sh" - - name: Cache dist - uses: actions/upload-artifact@v4 - with: - name: cargo-dist-cache - path: ~/.cargo/bin/dist + run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.12.0/cargo-dist-installer.sh | sh" # sure would be cool if github gave us proper conditionals... # so here's a doubly-nested ternary-via-truthiness to try to provide the best possible # functionality based on whether this is a pull_request, and whether it's from a fork. @@ -77,8 +70,8 @@ jobs: # but also really annoying to build CI around when it needs secrets to work right.) - id: plan run: | - dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json - echo "dist ran successfully" + cargo dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json + echo "cargo dist ran successfully" cat plan-dist-manifest.json echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT" - name: "Upload dist-manifest.json" @@ -96,39 +89,28 @@ jobs: if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }} strategy: fail-fast: false - # Target platforms/runners are computed by dist in create-release. + # Target platforms/runners are computed by cargo-dist in create-release. # Each member of the matrix has the following arguments: # # - runner: the github runner - # - dist-args: cli flags to pass to dist - # - install-dist: expression to run to install dist on the runner + # - dist-args: cli flags to pass to cargo dist + # - install-dist: expression to run to install cargo-dist on the runner # # Typically there will be: # - 1 "global" task that builds universal installers # - N "local" tasks that build each platform's binaries and platform-specific installers matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }} runs-on: ${{ matrix.runner }} - container: ${{ matrix.container && matrix.container.image || null }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json steps: - - name: enable windows longpaths - run: | - git config --global core.longpaths true - uses: actions/checkout@v4 with: - persist-credentials: false submodules: recursive - - name: Install Rust non-interactively if not already installed - if: ${{ matrix.container }} - run: | - if ! command -v cargo > /dev/null 2>&1; then - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y - echo "$HOME/.cargo/bin" >> $GITHUB_PATH - fi - - name: Install dist - run: ${{ matrix.install_dist.run }} + - uses: swatinem/rust-cache@v2 + - name: Install cargo-dist + run: ${{ matrix.install_dist }} # Get the dist-manifest - name: Fetch local artifacts uses: actions/download-artifact@v4 @@ -142,8 +124,8 @@ jobs: - name: Build artifacts run: | # Actually do builds and make zips and whatnot - dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json - echo "dist ran successfully" + cargo dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json + echo "cargo dist ran successfully" - id: cargo-dist name: Post-build # We force bash here just because github makes it really hard to get values up @@ -153,7 +135,7 @@ jobs: run: | # Parse out what we just built and upload it to scratch storage echo "paths<> "$GITHUB_OUTPUT" - dist print-upload-files-from-manifest --manifest dist-manifest.json >> "$GITHUB_OUTPUT" + jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json >> "$GITHUB_OUTPUT" echo "EOF" >> "$GITHUB_OUTPUT" cp dist-manifest.json "$BUILD_MANIFEST_NAME" @@ -170,21 +152,17 @@ jobs: needs: - plan - build-local-artifacts - runs-on: "ubuntu-22.04" + runs-on: "ubuntu-20.04" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json steps: - uses: actions/checkout@v4 with: - persist-credentials: false submodules: recursive - - name: Install cached dist - uses: actions/download-artifact@v4 - with: - name: cargo-dist-cache - path: ~/.cargo/bin/ - - run: chmod +x ~/.cargo/bin/dist + - name: Install cargo-dist + shell: bash + run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.12.0/cargo-dist-installer.sh | sh" # Get all the local artifacts for the global tasks to use (for e.g. checksums) - name: Fetch local artifacts uses: actions/download-artifact@v4 @@ -195,12 +173,12 @@ jobs: - id: cargo-dist shell: bash run: | - dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json - echo "dist ran successfully" + cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json + echo "cargo dist ran successfully" # Parse out what we just built and upload it to scratch storage echo "paths<> "$GITHUB_OUTPUT" - jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" + jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json >> "$GITHUB_OUTPUT" echo "EOF" >> "$GITHUB_OUTPUT" cp dist-manifest.json "$BUILD_MANIFEST_NAME" @@ -221,20 +199,15 @@ jobs: if: ${{ always() && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - runs-on: "ubuntu-22.04" + runs-on: "ubuntu-20.04" outputs: val: ${{ steps.host.outputs.manifest }} steps: - uses: actions/checkout@v4 with: - persist-credentials: false submodules: recursive - - name: Install cached dist - uses: actions/download-artifact@v4 - with: - name: cargo-dist-cache - path: ~/.cargo/bin/ - - run: chmod +x ~/.cargo/bin/dist + - name: Install cargo-dist + run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.12.0/cargo-dist-installer.sh | sh" # Fetch artifacts from scratch-storage - name: Fetch artifacts uses: actions/download-artifact@v4 @@ -242,10 +215,11 @@ jobs: pattern: artifacts-* path: target/distrib/ merge-multiple: true + # This is a harmless no-op for Github Releases, hosting for that happens in "announce" - id: host shell: bash run: | - dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json + cargo dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json echo "artifacts uploaded and released successfully" cat dist-manifest.json echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" @@ -255,29 +229,8 @@ jobs: # Overwrite the previous copy name: artifacts-dist-manifest path: dist-manifest.json - # Create a GitHub Release while uploading all files to it - - name: "Download GitHub Artifacts" - uses: actions/download-artifact@v4 - with: - pattern: artifacts-* - path: artifacts - merge-multiple: true - - name: Cleanup - run: | - # Remove the granular manifests - rm -f artifacts/*-dist-manifest.json - - name: Create GitHub Release - env: - PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}" - ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}" - ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}" - RELEASE_COMMIT: "${{ github.sha }}" - run: | - # Write and read notes from a file to avoid quoting breaking things - echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt - - gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/* + # Create a Github Release while uploading all files to it announce: needs: - plan @@ -286,11 +239,28 @@ jobs: # still allowing individual publish jobs to skip themselves (for prereleases). # "host" however must run to completion, no skipping allowed! if: ${{ always() && needs.host.result == 'success' }} - runs-on: "ubuntu-22.04" + runs-on: "ubuntu-20.04" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@v4 with: - persist-credentials: false submodules: recursive + - name: "Download Github Artifacts" + uses: actions/download-artifact@v4 + with: + pattern: artifacts-* + path: artifacts + merge-multiple: true + - name: Cleanup + run: | + # Remove the granular manifests + rm -f artifacts/*-dist-manifest.json + - name: Create Github Release + uses: ncipollo/release-action@v1 + with: + tag: ${{ needs.plan.outputs.tag }} + name: ${{ fromJson(needs.host.outputs.val).announcement_title }} + body: ${{ fromJson(needs.host.outputs.val).announcement_github_body }} + prerelease: ${{ fromJson(needs.host.outputs.val).announcement_is_prerelease }} + artifacts: "artifacts/*" diff --git a/Cargo.toml b/Cargo.toml index 2c1b4d9..41ff21d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,3 +35,21 @@ codegen-units = 1 [profile.release-fast] inherits = "release" panic = "abort" + +# The profile that 'cargo dist' will build with +[profile.dist] +inherits = "release" +lto = "thin" + +# Config for 'cargo dist' +[workspace.metadata.dist] +# The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax) +cargo-dist-version = "0.12.0" +# CI backends to support +ci = ["github"] +# The installers to generate for each app +installers = [] +# Target platforms to build apps for (Rust target-triple syntax) +targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl", "x86_64-pc-windows-msvc"] +# Publish jobs to run in CI +pr-run-mode = "plan" diff --git a/dist-workspace.toml b/dist-workspace.toml index f31b58b..1e1e411 100644 --- a/dist-workspace.toml +++ b/dist-workspace.toml @@ -4,12 +4,12 @@ members = ["cargo:."] # Config for 'dist' [dist] # The preferred dist version to use in CI (Cargo.toml SemVer syntax) -cargo-dist-version = "0.30.0" +cargo-dist-version = "0.30.3" # CI backends to support ci = "github" # The installers to generate for each app installers = [] # Target platforms to build apps for (Rust target-triple syntax) -targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"] +targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl", "x86_64-pc-windows-msvc"] # Which actions to run on pull requests pr-run-mode = "plan" From 3e02493701e6518fda0ed473a5c6008659fcfaf4 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Thu, 1 Jan 2026 19:54:04 +0100 Subject: [PATCH 185/194] ride along: ship fuzz/Cargo.lock --- fuzz/Cargo.lock | 447 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 447 insertions(+) create mode 100644 fuzz/Cargo.lock diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock new file mode 100644 index 0000000..a6e4746 --- /dev/null +++ b/fuzz/Cargo.lock @@ -0,0 +1,447 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "cc" +version = "1.2.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "diffutils" +version = "0.5.0" +dependencies = [ + "chrono", + "diff", + "itoa", + "regex", + "same-file", + "unicode-width", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.178" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "proc-macro2" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "syn" +version = "2.0.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21f182278bf2d2bcb3c88b1b08a37df029d71ce3d3ae26168e3c653b213b99d4" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unified-diff-fuzz" +version = "0.0.0" +dependencies = [ + "diffutils", + "libfuzzer-sys", +] + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" From 44565de70514401e1c9cd7ab25f1ec940fe0a5d4 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 2 Jan 2026 10:24:46 +0100 Subject: [PATCH 186/194] cargo-dist: refresh & remove duplicate info --- .github/workflows/release.yml | 148 ++++++++++++++++++++-------------- Cargo.toml | 15 +--- dist-workspace.toml | 4 +- 3 files changed, 91 insertions(+), 76 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 502a5ff..3c59af5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,20 +1,21 @@ -# Copyright 2022-2023, axodotdev +# This file was autogenerated by dist: https://axodotdev.github.io/cargo-dist +# +# Copyright 2022-2024, axodotdev # SPDX-License-Identifier: MIT or Apache-2.0 # # CI that: # # * checks for a Git Tag that looks like a release -# * builds artifacts with cargo-dist (archives, installers, hashes) +# * builds artifacts with dist (archives, installers, hashes) # * uploads those artifacts to temporary workflow zip -# * on success, uploads the artifacts to a Github Release +# * on success, uploads the artifacts to a GitHub Release # -# Note that the Github Release will be created with a generated +# Note that the GitHub Release will be created with a generated # title/body based on your changelogs. name: Release - permissions: - contents: write + "contents": "write" # This task will run whenever you push a git tag that looks like a version # like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc. @@ -23,30 +24,30 @@ permissions: # must be a Cargo-style SemVer Version (must have at least major.minor.patch). # # If PACKAGE_NAME is specified, then the announcement will be for that -# package (erroring out if it doesn't have the given version or isn't cargo-dist-able). +# package (erroring out if it doesn't have the given version or isn't dist-able). # # If PACKAGE_NAME isn't specified, then the announcement will be for all -# (cargo-dist-able) packages in the workspace with that version (this mode is +# (dist-able) packages in the workspace with that version (this mode is # intended for workspaces with only one dist-able package, or with all dist-able # packages versioned/released in lockstep). # # If you push multiple tags at once, separate instances of this workflow will -# spin up, creating an independent announcement for each one. However Github +# spin up, creating an independent announcement for each one. However, GitHub # will hard limit this to 3 tags per commit, as it will assume more tags is a # mistake. # # If there's a prerelease-style suffix to the version, then the release(s) # will be marked as a prerelease. on: + pull_request: push: tags: - '**[0-9]+.[0-9]+.[0-9]+*' - pull_request: jobs: - # Run 'cargo dist plan' (or host) to determine what tasks we need to do + # Run 'dist plan' (or host) to determine what tasks we need to do plan: - runs-on: ubuntu-latest + runs-on: "ubuntu-22.04" outputs: val: ${{ steps.plan.outputs.manifest }} tag: ${{ !github.event.pull_request && github.ref_name || '' }} @@ -57,12 +58,18 @@ jobs: steps: - uses: actions/checkout@v4 with: + persist-credentials: false submodules: recursive - - name: Install cargo-dist + - name: Install dist # we specify bash to get pipefail; it guards against the `curl` command # failing. otherwise `sh` won't catch that `curl` returned non-0 shell: bash - run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.12.0/cargo-dist-installer.sh | sh" + run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.30.3/cargo-dist-installer.sh | sh" + - name: Cache dist + uses: actions/upload-artifact@v4 + with: + name: cargo-dist-cache + path: ~/.cargo/bin/dist # sure would be cool if github gave us proper conditionals... # so here's a doubly-nested ternary-via-truthiness to try to provide the best possible # functionality based on whether this is a pull_request, and whether it's from a fork. @@ -70,8 +77,8 @@ jobs: # but also really annoying to build CI around when it needs secrets to work right.) - id: plan run: | - cargo dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json - echo "cargo dist ran successfully" + dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json + echo "dist ran successfully" cat plan-dist-manifest.json echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT" - name: "Upload dist-manifest.json" @@ -89,28 +96,39 @@ jobs: if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }} strategy: fail-fast: false - # Target platforms/runners are computed by cargo-dist in create-release. + # Target platforms/runners are computed by dist in create-release. # Each member of the matrix has the following arguments: # # - runner: the github runner - # - dist-args: cli flags to pass to cargo dist - # - install-dist: expression to run to install cargo-dist on the runner + # - dist-args: cli flags to pass to dist + # - install-dist: expression to run to install dist on the runner # # Typically there will be: # - 1 "global" task that builds universal installers # - N "local" tasks that build each platform's binaries and platform-specific installers matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }} runs-on: ${{ matrix.runner }} + container: ${{ matrix.container && matrix.container.image || null }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json steps: + - name: enable windows longpaths + run: | + git config --global core.longpaths true - uses: actions/checkout@v4 with: + persist-credentials: false submodules: recursive - - uses: swatinem/rust-cache@v2 - - name: Install cargo-dist - run: ${{ matrix.install_dist }} + - name: Install Rust non-interactively if not already installed + if: ${{ matrix.container }} + run: | + if ! command -v cargo > /dev/null 2>&1; then + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + fi + - name: Install dist + run: ${{ matrix.install_dist.run }} # Get the dist-manifest - name: Fetch local artifacts uses: actions/download-artifact@v4 @@ -124,8 +142,8 @@ jobs: - name: Build artifacts run: | # Actually do builds and make zips and whatnot - cargo dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json - echo "cargo dist ran successfully" + dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json + echo "dist ran successfully" - id: cargo-dist name: Post-build # We force bash here just because github makes it really hard to get values up @@ -135,7 +153,7 @@ jobs: run: | # Parse out what we just built and upload it to scratch storage echo "paths<> "$GITHUB_OUTPUT" - jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json >> "$GITHUB_OUTPUT" + dist print-upload-files-from-manifest --manifest dist-manifest.json >> "$GITHUB_OUTPUT" echo "EOF" >> "$GITHUB_OUTPUT" cp dist-manifest.json "$BUILD_MANIFEST_NAME" @@ -152,17 +170,21 @@ jobs: needs: - plan - build-local-artifacts - runs-on: "ubuntu-20.04" + runs-on: "ubuntu-22.04" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json steps: - uses: actions/checkout@v4 with: + persist-credentials: false submodules: recursive - - name: Install cargo-dist - shell: bash - run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.12.0/cargo-dist-installer.sh | sh" + - name: Install cached dist + uses: actions/download-artifact@v4 + with: + name: cargo-dist-cache + path: ~/.cargo/bin/ + - run: chmod +x ~/.cargo/bin/dist # Get all the local artifacts for the global tasks to use (for e.g. checksums) - name: Fetch local artifacts uses: actions/download-artifact@v4 @@ -173,12 +195,12 @@ jobs: - id: cargo-dist shell: bash run: | - cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json - echo "cargo dist ran successfully" + dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json + echo "dist ran successfully" # Parse out what we just built and upload it to scratch storage echo "paths<> "$GITHUB_OUTPUT" - jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json >> "$GITHUB_OUTPUT" + jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" echo "EOF" >> "$GITHUB_OUTPUT" cp dist-manifest.json "$BUILD_MANIFEST_NAME" @@ -195,19 +217,24 @@ jobs: - plan - build-local-artifacts - build-global-artifacts - # Only run if we're "publishing", and only if local and global didn't fail (skipped is fine) - if: ${{ always() && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }} + # Only run if we're "publishing", and only if plan, local and global didn't fail (skipped is fine) + if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - runs-on: "ubuntu-20.04" + runs-on: "ubuntu-22.04" outputs: val: ${{ steps.host.outputs.manifest }} steps: - uses: actions/checkout@v4 with: + persist-credentials: false submodules: recursive - - name: Install cargo-dist - run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.12.0/cargo-dist-installer.sh | sh" + - name: Install cached dist + uses: actions/download-artifact@v4 + with: + name: cargo-dist-cache + path: ~/.cargo/bin/ + - run: chmod +x ~/.cargo/bin/dist # Fetch artifacts from scratch-storage - name: Fetch artifacts uses: actions/download-artifact@v4 @@ -215,11 +242,10 @@ jobs: pattern: artifacts-* path: target/distrib/ merge-multiple: true - # This is a harmless no-op for Github Releases, hosting for that happens in "announce" - id: host shell: bash run: | - cargo dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json + dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json echo "artifacts uploaded and released successfully" cat dist-manifest.json echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" @@ -229,8 +255,29 @@ jobs: # Overwrite the previous copy name: artifacts-dist-manifest path: dist-manifest.json + # Create a GitHub Release while uploading all files to it + - name: "Download GitHub Artifacts" + uses: actions/download-artifact@v4 + with: + pattern: artifacts-* + path: artifacts + merge-multiple: true + - name: Cleanup + run: | + # Remove the granular manifests + rm -f artifacts/*-dist-manifest.json + - name: Create GitHub Release + env: + PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}" + ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}" + ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}" + RELEASE_COMMIT: "${{ github.sha }}" + run: | + # Write and read notes from a file to avoid quoting breaking things + echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt + + gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/* - # Create a Github Release while uploading all files to it announce: needs: - plan @@ -239,28 +286,11 @@ jobs: # still allowing individual publish jobs to skip themselves (for prereleases). # "host" however must run to completion, no skipping allowed! if: ${{ always() && needs.host.result == 'success' }} - runs-on: "ubuntu-20.04" + runs-on: "ubuntu-22.04" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@v4 with: + persist-credentials: false submodules: recursive - - name: "Download Github Artifacts" - uses: actions/download-artifact@v4 - with: - pattern: artifacts-* - path: artifacts - merge-multiple: true - - name: Cleanup - run: | - # Remove the granular manifests - rm -f artifacts/*-dist-manifest.json - - name: Create Github Release - uses: ncipollo/release-action@v1 - with: - tag: ${{ needs.plan.outputs.tag }} - name: ${{ fromJson(needs.host.outputs.val).announcement_title }} - body: ${{ fromJson(needs.host.outputs.val).announcement_github_body }} - prerelease: ${{ fromJson(needs.host.outputs.val).announcement_is_prerelease }} - artifacts: "artifacts/*" diff --git a/Cargo.toml b/Cargo.toml index 6139ebd..29331c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,20 +36,7 @@ codegen-units = 1 inherits = "release" panic = "abort" -# The profile that 'cargo dist' will build with +# The profile that 'dist' will build with [profile.dist] inherits = "release" lto = "thin" - -# Config for 'cargo dist' -[workspace.metadata.dist] -# The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax) -cargo-dist-version = "0.12.0" -# CI backends to support -ci = ["github"] -# The installers to generate for each app -installers = [] -# Target platforms to build apps for (Rust target-triple syntax) -targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl", "x86_64-pc-windows-msvc"] -# Publish jobs to run in CI -pr-run-mode = "plan" diff --git a/dist-workspace.toml b/dist-workspace.toml index 1e1e411..92c4095 100644 --- a/dist-workspace.toml +++ b/dist-workspace.toml @@ -10,6 +10,4 @@ ci = "github" # The installers to generate for each app installers = [] # Target platforms to build apps for (Rust target-triple syntax) -targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl", "x86_64-pc-windows-msvc"] -# Which actions to run on pull requests -pr-run-mode = "plan" +targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"] From e00ff6b108b9f07a990cdc7122c2194ed81ecbbb Mon Sep 17 00:00:00 2001 From: Gustavo Noronha Silva Date: Sun, 21 Dec 2025 11:28:30 -0300 Subject: [PATCH 187/194] cmp: stop allocating for byte printing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This makes verbose comparison of 37MB completely different files 2.34x faster than our own baseline, putting our cmp at almost 6x faster than GNU cmp (/opt/homebrew/bin/cmp) on my M4 Pro Mac. The output remains identical to that of GNU cmp. Mostly equal and smaller files do not regress. Benchmark 1: ./bin/baseline/diffutils cmp -lb t/huge t/eguh Time (mean ± σ): 1.669 s ± 0.011 s [User: 1.594 s, System: 0.073 s] Range (min … max): 1.654 s … 1.689 s 10 runs Warning: Ignoring non-zero exit code. Benchmark 2: ./target/release/diffutils cmp -lb t/huge t/eguh Time (mean ± σ): 714.2 ms ± 4.1 ms [User: 629.3 ms, System: 82.7 ms] Range (min … max): 707.2 ms … 721.5 ms 10 runs Warning: Ignoring non-zero exit code. Benchmark 3: /opt/homebrew/bin/cmp -lb t/huge t/eguh Time (mean ± σ): 4.213 s ± 0.050 s [User: 4.128 s, System: 0.081 s] Range (min … max): 4.160 s … 4.316 s 10 runs Warning: Ignoring non-zero exit code. Benchmark 4: /usr/bin/cmp -lb t/huge t/eguh Time (mean ± σ): 3.892 s ± 0.048 s [User: 3.819 s, System: 0.070 s] Range (min … max): 3.808 s … 3.976 s 10 runs Warning: Ignoring non-zero exit code. Summary ./target/release/diffutils cmp -lb t/huge t/eguh ran 2.34 ± 0.02 times faster than ./bin/baseline/diffutils cmp -lb t/huge t/eguh 5.45 ± 0.07 times faster than /usr/bin/cmp -lb t/huge t/eguh 5.90 ± 0.08 times faster than /opt/homebrew/bin/cmp -lb t/huge t/eguh --- src/cmp.rs | 97 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 60 insertions(+), 37 deletions(-) diff --git a/src/cmp.rs b/src/cmp.rs index d53df4f..3e1eda6 100644 --- a/src/cmp.rs +++ b/src/cmp.rs @@ -500,12 +500,6 @@ pub fn main(opts: Peekable) -> ExitCode { } } -#[inline] -fn is_ascii_printable(byte: u8) -> bool { - let c = byte as char; - c.is_ascii() && !c.is_ascii_control() -} - #[inline] fn format_octal(byte: u8, buf: &mut [u8; 3]) -> &str { *buf = [b' ', b' ', b'0']; @@ -525,32 +519,68 @@ fn format_octal(byte: u8, buf: &mut [u8; 3]) -> &str { } #[inline] -fn format_byte(byte: u8) -> String { - let mut byte = byte; - let mut quoted = vec![]; - - if !is_ascii_printable(byte) { - if byte >= 128 { - quoted.push(b'M'); - quoted.push(b'-'); - byte -= 128; +fn write_visible_byte(output: &mut Vec, byte: u8) -> usize { + match byte { + // Control characters: ^@, ^A, ..., ^_ + 0..=31 => { + output.push(b'^'); + output.push(byte + 64); + 2 } - - if byte < 32 { - quoted.push(b'^'); - byte += 64; - } else if byte == 127 { - quoted.push(b'^'); - byte = b'?'; + // Printable ASCII (space through ~) + 32..=126 => { + output.push(byte); + 1 + } + // DEL: ^? + 127 => { + output.extend_from_slice(b"^?"); + 2 + } + // High bytes with control equivalents: M-^@, M-^A, ..., M-^_ + 128..=159 => { + output.push(b'M'); + output.push(b'-'); + output.push(b'^'); + output.push(byte - 64); + 4 + } + // High bytes: M-, M-!, ..., M-~ + 160..=254 => { + output.push(b'M'); + output.push(b'-'); + output.push(byte - 128); + 3 + } + // Byte 255: M-^? + 255 => { + output.extend_from_slice(b"M-^?"); + 4 } - assert!((byte as char).is_ascii()); } +} - quoted.push(byte); +/// Writes a byte in visible form with right-padding to 4 spaces. +#[inline] +fn write_visible_byte_padded(output: &mut Vec, byte: u8) { + const SPACES: &[u8] = b" "; + const WIDTH: usize = SPACES.len(); + + let display_width = write_visible_byte(output, byte); - // SAFETY: the checks and shifts we do above match what cat and GNU + // Add right-padding spaces + let padding = WIDTH.saturating_sub(display_width); + output.extend_from_slice(&SPACES[..padding]); +} + +/// Formats a byte as a visible string (for non-performance-critical path) +#[inline] +fn format_visible_byte(byte: u8) -> String { + let mut result = Vec::with_capacity(4); + write_visible_byte(&mut result, byte); + // SAFETY: the checks and shifts in write_visible_byte match what cat and GNU // cmp do to ensure characters fall inside the ascii range. - unsafe { String::from_utf8_unchecked(quoted) } + unsafe { String::from_utf8_unchecked(result) } } // This function has been optimized to not use the Rust fmt system, which @@ -588,14 +618,7 @@ fn format_verbose_difference( output.push(b' '); - let from_byte_str = format_byte(from_byte); - let from_byte_padding = 4 - from_byte_str.len(); - - output.extend_from_slice(from_byte_str.as_bytes()); - - for _ in 0..from_byte_padding { - output.push(b' ') - } + write_visible_byte_padded(output, from_byte); output.push(b' '); @@ -603,7 +626,7 @@ fn format_verbose_difference( output.push(b' '); - output.extend_from_slice(format_byte(to_byte).as_bytes()); + write_visible_byte(output, to_byte); output.push(b'\n'); } else { @@ -706,9 +729,9 @@ fn report_difference(from_byte: u8, to_byte: u8, at_byte: usize, at_line: usize, print!( " is {:>3o} {:char_width$} {:>3o} {:char_width$}", from_byte, - format_byte(from_byte), + format_visible_byte(from_byte), to_byte, - format_byte(to_byte) + format_visible_byte(to_byte) ); } println!(); From a1d18a0c09e91336df4b352ea08a751f1e169153 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 9 Jan 2026 17:10:14 +0000 Subject: [PATCH 188/194] chore(deps): update rust crate assert_cmd to v2.1.2 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aa11cc7..9e18dd4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,9 +28,9 @@ checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" [[package]] name = "assert_cmd" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcbb6924530aa9e0432442af08bbcafdad182db80d2e560da42a6d442535bf85" +checksum = "9c5bcfa8749ac45dd12cb11055aeeb6b27a3895560d60d71e3c23bf979e60514" dependencies = [ "anstyle", "bstr", From e2fb192d520d0f3846697fcf0cf2f63a870f9185 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 02:35:41 +0000 Subject: [PATCH 189/194] chore(deps): update rust crate chrono to v0.4.43 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9e18dd4..2ed2e14 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -84,9 +84,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" dependencies = [ "iana-time-zone", "js-sys", From 445e1ea02feb6674733f9da245ca08209a14e6c0 Mon Sep 17 00:00:00 2001 From: oech3 <79379754+oech3@users.noreply.github.com> Date: Sun, 25 Jan 2026 11:58:26 +0900 Subject: [PATCH 190/194] Use preinstalled rust, disable incremental build --- .github/workflows/ci.yml | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8343add..a060835 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,7 @@ name: Basic CI env: CARGO_TERM_COLOR: always + CARGO_INCREMENTAL: 0 jobs: check: @@ -15,7 +16,6 @@ jobs: os: [ubuntu-latest, macOS-latest, windows-latest] steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - run: cargo check test: @@ -27,7 +27,6 @@ jobs: os: [ubuntu-latest, macOS-latest, windows-latest] steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - name: install GNU patch on MacOS if: runner.os == 'macOS' run: | @@ -44,8 +43,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - - run: rustup component add rustfmt - run: cargo fmt --all -- --check clippy: @@ -57,8 +54,6 @@ jobs: os: [ubuntu-latest, macOS-latest, windows-latest] steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - - run: rustup component add clippy - run: cargo clippy -- -D warnings gnu-testsuite: @@ -66,7 +61,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - run: cargo build --release # do not fail, the report is merely informative (at least until all tests pass reliably) - run: ./tests/run-upstream-testsuite.sh release || true @@ -91,26 +85,19 @@ jobs: steps: - uses: actions/checkout@v4 - name: Initialize workflow variables + env: + # Use -Z + RUSTC_BOOTSTRAP: 1 id: vars shell: bash run: | ## VARs setup outputs() { step_id="vars"; for var in "$@" ; do echo steps.${step_id}.outputs.${var}="${!var}"; echo "${var}=${!var}" >> $GITHUB_OUTPUT; done; } - # toolchain - TOOLCHAIN="nightly" ## default to "nightly" toolchain (required for certain required unstable compiler flags) ## !maint: refactor when stable channel has needed support - # * specify gnu-type TOOLCHAIN for windows; `grcov` requires gnu-style code coverage data files - case ${{ matrix.job.os }} in windows-*) TOOLCHAIN="$TOOLCHAIN-x86_64-pc-windows-gnu" ;; esac; - # * use requested TOOLCHAIN if specified - if [ -n "${{ matrix.job.toolchain }}" ]; then TOOLCHAIN="${{ matrix.job.toolchain }}" ; fi - outputs TOOLCHAIN # target-specific options # * CODECOV_FLAGS CODECOV_FLAGS=$( echo "${{ matrix.job.os }}" | sed 's/[^[:alnum:]]/_/g' ) outputs CODECOV_FLAGS - - name: rust toolchain ~ install - uses: dtolnay/rust-toolchain@nightly - - run: rustup component add llvm-tools-preview - name: install GNU patch on MacOS if: runner.os == 'macOS' run: | @@ -128,7 +115,12 @@ jobs: RUSTFLAGS: "-Cinstrument-coverage -Zcoverage-options=branch -Ccodegen-units=1 -Copt-level=0 -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort" RUSTDOCFLAGS: "-Cpanic=abort" LLVM_PROFILE_FILE: "diffutils-%p-%m.profraw" + # Use -Z + RUSTC_BOOTSTRAP: 1 - name: "`grcov` ~ install" + env: + # Use -Z + RUSTC_BOOTSTRAP: 1 id: build_grcov shell: bash run: | From b9b7ea8d2b85a886b6ceb6f92729badbd1cdf25c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 3 Feb 2026 14:47:31 +0000 Subject: [PATCH 191/194] chore(deps): update rust crate regex to v1.12.3 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2ed2e14..c62d355 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -309,9 +309,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", From 95883b462b9c5404611440c7857b0a69c72c8846 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 9 Feb 2026 22:06:03 +0000 Subject: [PATCH 192/194] chore(deps): update rust crate tempfile to v3.25.0 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c62d355..091e301 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -391,9 +391,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.24.0" +version = "3.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" dependencies = [ "fastrand", "getrandom", From 9f419c31ea3bd0bd5d8ac23a534c06f16ae9b403 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 10 Feb 2026 21:53:55 +0000 Subject: [PATCH 193/194] chore(deps): update rust crate libfuzzer-sys to v0.4.12 --- fuzz/Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index a6e4746..c9b6f68 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -169,9 +169,9 @@ checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "libfuzzer-sys" -version = "0.4.10" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" +checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" dependencies = [ "arbitrary", "cc", From 87e0aa28286dd86d2cf797af74bbee77b7ed51ff Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 17:32:03 +0000 Subject: [PATCH 194/194] chore(deps): update rust crate predicates to v3.1.4 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 091e301..5285281 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -251,9 +251,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "predicates" -version = "3.1.3" +version = "3.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" dependencies = [ "anstyle", "difflib",