Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions src/uu/dircolors/src/dircolors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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))?;
Expand Down Expand Up @@ -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<Item = String> {
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<T>(user_input: T, fmt: &OutputFmt, fp: &str) -> Result<String, String>
where
T: IntoIterator,
Expand Down
56 changes: 55 additions & 1 deletion tests/by-util/test_dircolors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

// spell-checker:ignore overridable colorterm

#[cfg(target_os = "linux")]
use uutests::at_and_ucmd;
use uutests::new_ucmd;

Expand Down Expand Up @@ -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");
}
Loading