From b469e0a9d78057e0cefa9e301ee4aa72ab281b77 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 24 Oct 2026 10:44:56 +0200 Subject: [PATCH] diff: report binary files instead of diffing them --- src/diff.rs | 42 ++++++++++++++++++++++++++----- src/params.rs | 26 +++++++++++++++++++ tests/by-util/test_diff.rs | 51 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 6 deletions(-) diff --git a/src/diff.rs b/src/diff.rs index 9a9a578..b947058 100644 --- a/src/diff.rs +++ b/src/diff.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, MissingFile}; +use crate::params::{parse_params, Format, MissingFile, Params}; use crate::utils::report_failure_to_read_input_file; use crate::{context_diff, ed_diff, normal_diff, side_diff, unified_diff}; use std::env::ArgsOs; @@ -80,6 +80,22 @@ pub fn main(opts: Peekable) -> ExitCode { return ExitCode::from(2); } + if !params.text && (is_binary(&from_content) || is_binary(&to_content)) { + if from_content == to_content { + maybe_report_identical_files(); + return ExitCode::SUCCESS; + } + print_files_differ( + if params.brief { + "Files" + } else { + "Binary files" + }, + ¶ms, + ); + return ExitCode::from(1); + } + // run diff let result: Vec = match params.format { Format::Normal => normal_diff::diff(&from_content, &to_content, ¶ms), @@ -95,11 +111,7 @@ pub fn main(opts: Peekable) -> ExitCode { } }; if params.brief && !result.is_empty() { - println!( - "Files {} and {} differ", - params.from.to_string_lossy(), - params.to.to_string_lossy() - ); + print_files_differ("Files", ¶ms); } else { io::stdout().write_all(&result).unwrap(); } @@ -110,3 +122,21 @@ pub fn main(opts: Peekable) -> ExitCode { ExitCode::from(1) } } + +/// Number of leading bytes inspected when deciding whether an input is binary. +const BINARY_PROBE_LEN: usize = 16 * 1024; + +/// An input is treated as binary when a NUL byte occurs near its start. +fn is_binary(content: &[u8]) -> bool { + content[..content.len().min(BINARY_PROBE_LEN)].contains(&0) +} + +/// Print the one-line summary used instead of the differences, `kind` naming +/// what was compared. +fn print_files_differ(kind: &str, params: &Params) { + println!( + "{kind} {} and {} differ", + params.from.to_string_lossy(), + params.to.to_string_lossy() + ); +} diff --git a/src/params.rs b/src/params.rs index f3e77f1..2e65974 100644 --- a/src/params.rs +++ b/src/params.rs @@ -40,6 +40,7 @@ pub struct Params { pub tabsize: usize, pub width: usize, pub missing_file: MissingFile, + pub text: bool, } impl Default for Params { @@ -56,6 +57,7 @@ impl Default for Params { tabsize: 8, width: 130, missing_file: MissingFile::default(), + text: false, } } } @@ -112,6 +114,10 @@ pub fn parse_params>(mut opts: Peekable) -> Resu } continue; } + if param == "-a" || param == "--text" { + params.text = true; + continue; + } if param == "-t" || param == "--expand-tabs" { params.expand_tabs = true; continue; @@ -710,6 +716,26 @@ mod tests { ); } #[test] + fn text() { + for option in ["-a", "--text"] { + assert_eq!( + Ok(Params { + executable: os("diff"), + from: os("old.bin"), + to: os("new.bin"), + text: true, + ..Default::default() + }), + parse_params( + [os("diff"), os(option), os("old.bin"), os("new.bin")] + .iter() + .cloned() + .peekable() + ) + ); + } + } + #[test] fn expand_tabs() { assert_eq!( Ok(Params { diff --git a/tests/by-util/test_diff.rs b/tests/by-util/test_diff.rs index 3d57243..13a8b04 100644 --- a/tests/by-util/test_diff.rs +++ b/tests/by-util/test_diff.rs @@ -244,6 +244,57 @@ fn missing_file_as_empty() -> Result<(), Box> { Ok(()) } +#[test] +fn binary_files() -> Result<(), Box> { + let tmp_dir = tempdir()?; + let left = tmp_dir.path().join("left.bin"); + let right = tmp_dir.path().join("right.bin"); + std::fs::write(&left, b"header\x00\x7fpayload-one\n")?; + std::fs::write(&right, b"header\x00\x7fpayload-two\n")?; + + let mut cmd = cargo_bin_cmd!("diffutils"); + cmd.arg("diff").arg(&left).arg(&right); + cmd.assert() + .code(predicate::eq(1)) + .failure() + .stderr(predicate::str::is_empty()) + .stdout(format!( + "Binary files {} and {} differ\n", + left.display(), + right.display() + )); + + // Identical binary inputs are not reported. + let mut cmd = cargo_bin_cmd!("diffutils"); + cmd.arg("diff").arg(&left).arg("-"); + cmd.write_stdin(b"header\x00\x7fpayload-one\n".to_vec()); + cmd.assert().code(predicate::eq(0)).success().stdout(""); + + // -q keeps its usual wording. + let mut cmd = cargo_bin_cmd!("diffutils"); + cmd.arg("diff").arg("-q").arg(&left).arg(&right); + cmd.assert() + .code(predicate::eq(1)) + .failure() + .stderr(predicate::str::is_empty()) + .stdout(format!( + "Files {} and {} differ\n", + left.display(), + right.display() + )); + + // -a and --text force a line-by-line comparison. + for option in ["-a", "--text"] { + let mut cmd = cargo_bin_cmd!("diffutils"); + cmd.arg("diff").arg(option).arg(&left).arg(&right); + cmd.assert() + .code(predicate::eq(1)) + .failure() + .stdout(predicate::str::starts_with("1c1\n")); + } + Ok(()) +} + #[test] fn compare_file_to_directory() -> Result<(), Box> { let tmp_dir = tempdir()?;