From bdf6530faf1af83c9450c7c8d4885fe52a2d46a9 Mon Sep 17 00:00:00 2001 From: Alex Chen Date: Sun, 30 Aug 2026 10:45:42 +0000 Subject: [PATCH] dircolors: skip invalid UTF-8 lines instead of truncating the config Signed-off-by: Alex Chen --- src/uu/dircolors/src/dircolors.rs | 20 +++++++---- tests/by-util/test_dircolors.rs | 56 ++++++++++++++++++++++++++++++- 2 files changed, 69 insertions(+), 7 deletions(-) diff --git a/src/uu/dircolors/src/dircolors.rs b/src/uu/dircolors/src/dircolors.rs index 27bf3ddb3a2..10a87a7a4f0 100644 --- a/src/uu/dircolors/src/dircolors.rs +++ b/src/uu/dircolors/src/dircolors.rs @@ -176,7 +176,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let result = if *file_arg == "-" { let fin = BufReader::new(std::io::stdin()); // For example, for echo "owt 40;33"|dircolors -b - - parse(fin.lines().map_while(Result::ok), &out_format, "-") + parse(config_lines(fin), &out_format, "-") } else { let path = Path::new(&file_arg); if path.is_dir() { @@ -188,11 +188,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let file = File::open(path) .map_err(|e| USimpleError::new(1, format!("{}: {e}", path.maybe_quote())))?; let fin = BufReader::new(file); - parse( - fin.lines().map_while(Result::ok), - &out_format, - &path.to_string_lossy(), - ) + parse(config_lines(fin), &out_format, &path.to_string_lossy()) }; let string = result.map_err(|s| USimpleError::new(1, s))?; @@ -313,6 +309,18 @@ enum ParseState { Pass, } +/// Iterate over the lines of a config file. +/// GNU dircolors is byte-oriented; invalid UTF-8 maps to an empty line, +/// which the parser skips but still counts — line numbers stay aligned. +fn config_lines(fin: impl BufRead) -> impl Iterator { + fin.split(b'\n').map_while(Result::ok).map(|mut line| { + if line.last() == Some(&b'\r') { + line.pop(); + } + String::from_utf8(line).unwrap_or_default() + }) +} + fn parse(user_input: T, fmt: &OutputFmt, fp: &str) -> Result where T: IntoIterator, diff --git a/tests/by-util/test_dircolors.rs b/tests/by-util/test_dircolors.rs index 3760124f286..b8f5bcafe36 100644 --- a/tests/by-util/test_dircolors.rs +++ b/tests/by-util/test_dircolors.rs @@ -5,7 +5,6 @@ // spell-checker:ignore overridable colorterm -#[cfg(target_os = "linux")] use uutests::at_and_ucmd; use uutests::new_ucmd; @@ -249,3 +248,58 @@ fn test_invalid_term_glob() { .succeeds() .stdout_only("LS_COLORS='';\nexport LS_COLORS\n"); } + +#[test] +fn test_invalid_utf8_line_file() { + let (at, mut ucmd) = at_and_ucmd!(); + + let filename = "invalid-utf8-line"; + std::fs::write( + at.plus(filename), + b"DIR 01;31\nBAD\xffLINE 99\n*.txt 00;32\n", + ) + .unwrap(); + + // a line that fails to decode is skipped without truncating the rest + ucmd.args(&["-b", filename]) + .succeeds() + .stdout_contains("di=01;31") + .stdout_contains("*.txt=00;32"); +} + +#[test] +fn test_invalid_utf8_line_stdin() { + // the bad line must be skipped, not truncate the rest of the config + new_ucmd!() + .pipe_in(b"DIR 01;31\nBAD\xffLINE 99\n*.txt 00;32\n".to_vec()) + .args(&["-b", "-"]) + .succeeds() + .stdout_contains("di=01;31") + .stdout_contains("*.txt=00;32"); +} + +#[test] +fn test_invalid_utf8_only_bad() { + let (at, mut ucmd) = at_and_ucmd!(); + + let filename = "invalid-utf8-only"; + std::fs::write(at.plus(filename), b"BAD\xffLINE 99\n").unwrap(); + + // the config holds nothing parseable: empty LS_COLORS, exit 0 + ucmd.args(&["-b", filename]) + .succeeds() + .stdout_only("LS_COLORS='';\nexport LS_COLORS\n"); +} + +#[test] +fn test_invalid_utf8_line_preserves_line_numbers() { + let (at, mut ucmd) = at_and_ucmd!(); + + let filename = "invalid-utf8-line-number"; + std::fs::write(at.plus(filename), b"BAD\xffLINE 99\nONLYKEY\n").unwrap(); + + // GNU counts the skipped line, so the error points at the real line 2 + ucmd.args(&["-b", filename]) + .fails_with_code(1) + .stderr_contains(":2: invalid line"); +}