From 8acb048e3bd18fa8e7dde629abf507d2ff0b48fc Mon Sep 17 00:00:00 2001 From: binhvq Date: Wed, 11 Nov 2026 14:36:22 +0700 Subject: [PATCH 1/2] diff: cut the -p/-F function line at 40 bytes and trim vertical tabs The function line shown after a hunk header was limited to 40 characters, so a line of multi-byte characters came out up to four times longer than GNU's, which stops at 40 bytes. Vertical tabs around it were kept as well, because is_ascii_whitespace() leaves them out while C's isspace() does not. --- src/utils.rs | 25 ++++++++++++-------- tests/by-util/test_diff.rs | 48 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 10 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index 29ab93df..ae186734 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -182,7 +182,7 @@ pub struct FunctionLineFinder<'a> { } impl<'a> FunctionLineFinder<'a> { - /// Maximum number of characters of a function line that are shown. + /// Maximum number of bytes of a function line that are shown. const MAX_WIDTH: usize = 40; /// Returns `None` when there are no patterns, so no function lines. @@ -209,15 +209,20 @@ impl<'a> FunctionLineFinder<'a> { } } self.next_line = self.next_line.max(end); - let line = self.last_match?.trim_ascii_start(); - let end = match std::str::from_utf8(line) { - Ok(text) => text - .char_indices() - .nth(Self::MAX_WIDTH) - .map_or(line.len(), |(end, _)| end), - Err(_) => line.len().min(Self::MAX_WIDTH), - }; - Some(line[..end].trim_ascii_end().to_vec()) + // C's isspace(): unlike `is_ascii_whitespace`, it includes vertical tab. + let is_space = |byte: &u8| byte.is_ascii_whitespace() || *byte == b'\x0b'; + let line = self.last_match?; + let start = line + .iter() + .position(|byte| !is_space(byte)) + .unwrap_or(line.len()); + let line = &line[start..]; + let line = &line[..line.len().min(Self::MAX_WIDTH)]; + let end = line + .iter() + .rposition(|byte| !is_space(byte)) + .map_or(0, |last| last + 1); + Some(line[..end].to_vec()) } } diff --git a/tests/by-util/test_diff.rs b/tests/by-util/test_diff.rs index 10fc0a10..0d8b3cec 100644 --- a/tests/by-util/test_diff.rs +++ b/tests/by-util/test_diff.rs @@ -1163,6 +1163,54 @@ fn function_lines() -> Result<(), Box> { Ok(()) } +// A function line is cut at 40 bytes, even inside a multi-byte character. +#[test] +fn function_line_is_cut_at_40_bytes() -> Result<(), Box> { + let tmp_dir = tempdir()?; + let old = tmp_dir.path().join("old"); + let new = tmp_dir.path().join("new"); + let function_line = format!("func_{}()", "é".repeat(40)); + std::fs::write(&old, format!("{function_line}\n1\n2\n3\n4\n5\n6\n"))?; + std::fs::write(&new, format!("{function_line}\n1\n2\n3\n4\nX\n6\n"))?; + + let mut expected = b"@@ -3,5 +3,5 @@ ".to_vec(); + expected.extend(&function_line.as_bytes()[..40]); + expected.push(b'\n'); + + let mut cmd = cargo_bin_cmd!("diffutils"); + cmd.arg("diff").arg("-u").arg("-p").arg(&old).arg(&new); + let output = cmd.assert().code(predicate::eq(1)).get_output().clone(); + let header = output.stdout.split_inclusive(|&byte| byte == b'\n').nth(2); + assert_eq!(header, Some(expected.as_slice())); + Ok(()) +} + +// Vertical tabs count as white space around a function line, as they do for +// C's isspace(). +#[test] +fn function_line_vertical_tabs_are_trimmed() -> Result<(), Box> { + let tmp_dir = tempdir()?; + let old = tmp_dir.path().join("old"); + let new = tmp_dir.path().join("new"); + std::fs::write(&old, "\x0b\x0c fg() \t\x0c\r\x0b\n1\n2\n3\n4\n5\n6\n")?; + std::fs::write(&new, "\x0b\x0c fg() \t\x0c\r\x0b\n1\n2\n3\n4\nX\n6\n")?; + + let mut cmd = cargo_bin_cmd!("diffutils"); + cmd.arg("diff").arg("-u").arg("-F").arg("fg"); + cmd.arg(&old).arg(&new); + cmd.assert() + .code(predicate::eq(1)) + .stdout(predicate::str::contains("\n@@ -3,5 +3,5 @@ fg()\n")); + + let mut cmd = cargo_bin_cmd!("diffutils"); + cmd.arg("diff").arg("-c").arg("-F").arg("fg"); + cmd.arg(&old).arg(&new); + cmd.assert() + .code(predicate::eq(1)) + .stdout(predicate::str::contains("\n*************** fg()\n")); + Ok(()) +} + #[cfg(unix)] #[test] fn quoted_file_names() -> Result<(), Box> { From deabc8641eba6a1cefdf8f24e24b1bf36e78e6bb Mon Sep 17 00:00:00 2001 From: binhvq Date: Thu, 12 Nov 2026 04:07:42 +0700 Subject: [PATCH 2/2] tests: port the function line tests to uutests --- tests/by-util/test_diff.rs | 57 +++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 32 deletions(-) diff --git a/tests/by-util/test_diff.rs b/tests/by-util/test_diff.rs index d2f2d48f..20c6677d 100644 --- a/tests/by-util/test_diff.rs +++ b/tests/by-util/test_diff.rs @@ -875,50 +875,43 @@ fn function_lines() { // A function line is cut at 40 bytes, even inside a multi-byte character. #[test] -fn function_line_is_cut_at_40_bytes() -> Result<(), Box> { - let tmp_dir = tempdir()?; - let old = tmp_dir.path().join("old"); - let new = tmp_dir.path().join("new"); +fn function_line_is_cut_at_40_bytes() { + let (at, ts) = at_and_ts!(); let function_line = format!("func_{}()", "é".repeat(40)); - std::fs::write(&old, format!("{function_line}\n1\n2\n3\n4\n5\n6\n"))?; - std::fs::write(&new, format!("{function_line}\n1\n2\n3\n4\nX\n6\n"))?; + at.write("old", &format!("{function_line}\n1\n2\n3\n4\n5\n6\n")); + at.write("new", &format!("{function_line}\n1\n2\n3\n4\nX\n6\n")); let mut expected = b"@@ -3,5 +3,5 @@ ".to_vec(); expected.extend(&function_line.as_bytes()[..40]); expected.push(b'\n'); - let mut cmd = cargo_bin_cmd!("diffutils"); - cmd.arg("diff").arg("-u").arg("-p").arg(&old).arg(&new); - let output = cmd.assert().code(predicate::eq(1)).get_output().clone(); - let header = output.stdout.split_inclusive(|&byte| byte == b'\n').nth(2); + let result = ts + .ucmd() + .args(&["-u", "-p", "old", "new"]) + .fails_with_code(1); + let header = result + .stdout() + .split_inclusive(|&byte| byte == b'\n') + .nth(2); assert_eq!(header, Some(expected.as_slice())); - Ok(()) } // Vertical tabs count as white space around a function line, as they do for // C's isspace(). #[test] -fn function_line_vertical_tabs_are_trimmed() -> Result<(), Box> { - let tmp_dir = tempdir()?; - let old = tmp_dir.path().join("old"); - let new = tmp_dir.path().join("new"); - std::fs::write(&old, "\x0b\x0c fg() \t\x0c\r\x0b\n1\n2\n3\n4\n5\n6\n")?; - std::fs::write(&new, "\x0b\x0c fg() \t\x0c\r\x0b\n1\n2\n3\n4\nX\n6\n")?; - - let mut cmd = cargo_bin_cmd!("diffutils"); - cmd.arg("diff").arg("-u").arg("-F").arg("fg"); - cmd.arg(&old).arg(&new); - cmd.assert() - .code(predicate::eq(1)) - .stdout(predicate::str::contains("\n@@ -3,5 +3,5 @@ fg()\n")); - - let mut cmd = cargo_bin_cmd!("diffutils"); - cmd.arg("diff").arg("-c").arg("-F").arg("fg"); - cmd.arg(&old).arg(&new); - cmd.assert() - .code(predicate::eq(1)) - .stdout(predicate::str::contains("\n*************** fg()\n")); - Ok(()) +fn function_line_vertical_tabs_are_trimmed() { + let (at, ts) = at_and_ts!(); + at.write("old", "\x0b\x0c fg() \t\x0c\r\x0b\n1\n2\n3\n4\n5\n6\n"); + at.write("new", "\x0b\x0c fg() \t\x0c\r\x0b\n1\n2\n3\n4\nX\n6\n"); + + ts.ucmd() + .args(&["-u", "-F", "fg", "old", "new"]) + .fails_with_code(1) + .stdout_contains("\n@@ -3,5 +3,5 @@ fg()\n"); + ts.ucmd() + .args(&["-c", "-F", "fg", "old", "new"]) + .fails_with_code(1) + .stdout_contains("\n*************** fg()\n"); } #[cfg(unix)]