From ff8a31e835f48bef1313d71f0c63dc1b81a5103b Mon Sep 17 00:00:00 2001 From: Justin Tracey Date: Mon, 23 Dec 2024 00:14:15 -0500 Subject: [PATCH] wc: fix escaping GNU wc only escapes file names with newlines in them. --- Cargo.toml | 2 ++ build.rs | 4 +++- src/uu/wc/src/wc.rs | 33 ++++++++++++++++++++------------- tests/by-util/test_wc.rs | 26 ++++++++++++++++++++++++++ 4 files changed, 51 insertions(+), 14 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1991679d8e6..98c50d6dbb6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,8 @@ windows = ["feat_os_windows"] nightly = [] test_unimplemented = [] expensive_tests = [] +# "test_risky_names" == enable tests that create problematic file names (would make a network share inaccessible to Windows, breaks SVN on Mac OS, etc.) +test_risky_names = [] # * only build `uudoc` when `--feature uudoc` is activated uudoc = ["zip", "dep:uuhelp_parser"] ## features diff --git a/build.rs b/build.rs index 91e9d0427ce..d414de09209 100644 --- a/build.rs +++ b/build.rs @@ -33,7 +33,9 @@ pub fn main() { #[allow(clippy::match_same_arms)] match krate.as_ref() { "default" | "macos" | "unix" | "windows" | "selinux" | "zip" => continue, // common/standard feature names - "nightly" | "test_unimplemented" | "expensive_tests" => continue, // crate-local custom features + "nightly" | "test_unimplemented" | "expensive_tests" | "test_risky_names" => { + continue + } // crate-local custom features "uudoc" => continue, // is not a utility "test" => continue, // over-ridden with 'uu_test' to avoid collision with rust core crate 'test' s if s.starts_with(FEATURE_PREFIX) => continue, // crate feature sets diff --git a/src/uu/wc/src/wc.rs b/src/uu/wc/src/wc.rs index 1c2d99628f7..6fc1efa0a00 100644 --- a/src/uu/wc/src/wc.rs +++ b/src/uu/wc/src/wc.rs @@ -255,13 +255,17 @@ impl<'a> Input<'a> { } /// Converts input to title that appears in stats. - fn to_title(&self) -> Option> { + fn to_title(&self) -> Option> { match self { - Self::Path(path) => Some(match path.to_str() { - Some(s) if !s.contains('\n') => Cow::Borrowed(s), - _ => Cow::Owned(escape_name_wrapper(path.as_os_str())), - }), - Self::Stdin(StdinKind::Explicit) => Some(Cow::Borrowed(STDIN_REPR)), + Self::Path(path) => { + let path = path.as_os_str(); + if path.to_string_lossy().contains('\n') { + Some(Cow::Owned(quoting_style::escape_name(path, QS_ESCAPE))) + } else { + Some(Cow::Borrowed(path)) + } + } + Self::Stdin(StdinKind::Explicit) => Some(Cow::Borrowed(OsStr::new(STDIN_REPR))), Self::Stdin(StdinKind::Implicit) => None, } } @@ -852,14 +856,17 @@ fn wc(inputs: &Inputs, settings: &Settings) -> UResult<()> { let maybe_title = input.to_title(); let maybe_title_str = maybe_title.as_deref(); if let Err(err) = print_stats(settings, &word_count, maybe_title_str, number_width) { - let title = maybe_title_str.unwrap_or(""); - show!(err.map_err_context(|| format!("failed to print result for {title}"))); + let title = maybe_title_str.unwrap_or(OsStr::new("")); + show!(err.map_err_context(|| format!( + "failed to print result for {}", + title.to_string_lossy() + ))); } } } if settings.total_when.is_total_row_visible(num_inputs) { - let title = are_stats_visible.then_some("total"); + let title = are_stats_visible.then_some(OsStr::new("total")); if let Err(err) = print_stats(settings, &total_word_count, title, number_width) { show!(err.map_err_context(|| "failed to print total".into())); } @@ -873,7 +880,7 @@ fn wc(inputs: &Inputs, settings: &Settings) -> UResult<()> { fn print_stats( settings: &Settings, result: &WordCount, - title: Option<&str>, + title: Option<&OsStr>, number_width: usize, ) -> io::Result<()> { let mut stdout = io::stdout().lock(); @@ -893,8 +900,8 @@ fn print_stats( } if let Some(title) = title { - writeln!(stdout, "{space}{title}") - } else { - writeln!(stdout) + write!(stdout, "{space}")?; + stdout.write_all(&uucore::os_str_as_bytes_lossy(title))?; } + writeln!(stdout) } diff --git a/tests/by-util/test_wc.rs b/tests/by-util/test_wc.rs index 0bdb5c843a1..e2af757b360 100644 --- a/tests/by-util/test_wc.rs +++ b/tests/by-util/test_wc.rs @@ -283,6 +283,32 @@ fn test_gnu_compatible_quotation() { .stdout_is("0 0 0 'some-dir1/12'$'\\n''34.txt'\n"); } +#[cfg(feature = "test_risky_names")] +#[test] +fn test_non_unicode_names() { + let scene = TestScenario::new(util_name!()); + let target1 = uucore::os_str_from_bytes(b"some-dir1/1\xC0\n.txt") + .expect("Only unix platforms can test non-unicode names"); + let target2 = uucore::os_str_from_bytes(b"some-dir1/2\xC0\t.txt") + .expect("Only unix platforms can test non-unicode names"); + let at = &scene.fixtures; + at.mkdir("some-dir1"); + at.touch(&target1); + at.touch(&target2); + scene + .ucmd() + .args(&[target1, target2]) + .run() + .stdout_is_bytes( + [ + b"0 0 0 'some-dir1/1'$'\\300\\n''.txt'\n".to_vec(), + b"0 0 0 some-dir1/2\xC0\t.txt\n".to_vec(), + b"0 0 0 total\n".to_vec(), + ] + .concat(), + ); +} + #[test] fn test_multiple_default() { new_ucmd!()