From 5377ac82ea9ea02a4b16260f08f1c0c6de13c0b1 Mon Sep 17 00:00:00 2001 From: Asifur Rahaman Meeru Date: Thu, 27 Aug 2026 17:26:08 +0600 Subject: [PATCH] diff: don't panic on an operand ending in --width=N The `--width` regex was missing the `^` anchor that its `--tabsize` sibling one line above already has, so it matched any argument whose lossy form ended in `--width=` rather than the option itself. Two symptoms follow, and the quieter one is worse: diff $'\xff--width=5' A B # aborts, exit 134 diff xx--width=5 A B # taken as a width, the operand is discarded The first reaches `into_string().unwrap()` with a non-UTF-8 argument and, under `panic = "abort"`, takes the process down. The second does not crash at all: the operand is swallowed as a width and diff compares the remaining two files, exiting 1. GNU treats both as file operands, reports the extra operand, exits 2. Anchoring restores the invariant the `--tabsize` block documents, that a match implies valid UTF-8, so the existing `unwrap` is sound rather than merely unlikely to fire. I mirrored that block rather than reworking the `unwrap` separately, to keep the two option paths reading the same way. `--width` had no test coverage, which is how this survived. Added a `width` test alongside `tabsize` covering valid values and the invalid forms, plus both cases above. The non-UTF-8 one is `cfg(unix)`, since it needs bytes an `OsString` cannot hold on Windows. Exit codes now match GNU diffutils 3.12 for `--width=5`, `--width=5x`, `xx--width=5` and the non-UTF-8 form. The error text still differs, because uu prints its usage line where GNU names the extra operand, but that gap predates this change and affects any three-operand invocation. Closes #247 --- src/params.rs | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 81 insertions(+), 1 deletion(-) diff --git a/src/params.rs b/src/params.rs index 74ef3e37..6f139cdf 100644 --- a/src/params.rs +++ b/src/params.rs @@ -60,7 +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(); + let width_re = Regex::new(r"^--width=(?P\d+)$").unwrap(); while let Some(param) = opts.next() { let next_param = opts.peek(); if param == "--" { @@ -113,6 +113,8 @@ pub fn parse_params>(mut opts: Peekable) -> Resu continue; } if width_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. let param = param.into_string().unwrap(); let width_str: &str = width_re .captures(param.as_str()) @@ -813,6 +815,84 @@ mod tests { .is_err()); } #[test] + fn width() { + assert_eq!( + Ok(Params { + executable: os("diff"), + from: os("foo"), + to: os("bar"), + width: 1, + ..Default::default() + }), + parse_params( + [os("diff"), os("--width=1"), os("foo"), os("bar")] + .iter() + .cloned() + .peekable() + ) + ); + assert_eq!( + Ok(Params { + executable: os("diff"), + from: os("foo"), + to: os("bar"), + width: 42, + ..Default::default() + }), + parse_params( + [os("diff"), os("--width=42"), os("foo"), os("bar")] + .iter() + .cloned() + .peekable() + ) + ); + for bad in [ + "--width", + "--width=", + "--width=r2", + "--width=-1", + "--width=0", + "--width=92233720368547758088", + ] { + assert!( + parse_params( + [os("diff"), os(bad), os("foo"), os("bar")] + .iter() + .cloned() + .peekable() + ) + .is_err(), + "expected an error for {bad}" + ); + } + // An argument that merely ends in "--width=" is a file operand and + // not the option, so three operands is an error. While the regex was + // unanchored this was accepted as a width and the operand silently dropped. + assert!(parse_params( + [os("diff"), os("xx--width=5"), os("foo"), os("bar")] + .iter() + .cloned() + .peekable() + ) + .is_err()); + } + // The same operand, but not valid UTF-8. Its lossy form ends in + // "--width=5", which used to match the unanchored regex and then abort in + // into_string(). GNU diff treats it as an operand and exits 2. + #[cfg(unix)] + #[test] + fn width_non_utf8_operand() { + use std::os::unix::ffi::OsStringExt; + let operand = OsString::from_vec(b"\xff--width=5".to_vec()); + assert!(parse_params( + [os("diff"), operand, os("foo"), os("bar")] + .iter() + .cloned() + .peekable() + ) + .is_err()); + } + #[test] fn double_dash() { assert_eq!( Ok(Params {