From da05a5254be8e909163e05477ff0ff2a2ca5d346 Mon Sep 17 00:00:00 2001 From: Michael Howell Date: Sat, 3 Apr 2021 09:42:35 -0700 Subject: [PATCH] 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.