From 9b32ed5574884379903b72f7da4e513d50b40dae Mon Sep 17 00:00:00 2001 From: fullwoodenshovel Date: Sat, 16 May 2026 19:33:01 +0200 Subject: [PATCH] stat: fix %N quoting of filenames containing control characters --- src/uu/stat/Cargo.toml | 2 ++ src/uu/stat/src/stat.rs | 17 +++++++++++++---- tests/by-util/test_stat.rs | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 4 deletions(-) diff --git a/src/uu/stat/Cargo.toml b/src/uu/stat/Cargo.toml index 607d1ebea62..21693a57939 100644 --- a/src/uu/stat/Cargo.toml +++ b/src/uu/stat/Cargo.toml @@ -26,6 +26,8 @@ uucore = { workspace = true, features = [ "fs", "fsext", "time", + "quoting-style", + "i18n-common", ] } thiserror = { workspace = true } fluent = { workspace = true } diff --git a/src/uu/stat/src/stat.rs b/src/uu/stat/src/stat.rs index 6c5afff2bc6..daa9ba472f1 100644 --- a/src/uu/stat/src/stat.rs +++ b/src/uu/stat/src/stat.rs @@ -5,6 +5,8 @@ // spell-checker:ignore datetime use uucore::error::{UError, UResult, USimpleError}; +use uucore::i18n::UEncoding; +use uucore::quoting_style::{QuotingStyle as UucoreQuotingStyle, escape_name}; use uucore::translate; use clap::builder::ValueParser; @@ -443,10 +445,17 @@ fn quote_file_name(file_name: &str, quoting_style: &QuotingStyle) -> String { let escaped = file_name.replace('\'', r"\'"); format!("'{escaped}'") } - QuotingStyle::ShellEscapeAlways => { - let quote = if file_name.contains('\'') { '"' } else { '\'' }; - format!("{quote}{file_name}{quote}") - } + QuotingStyle::ShellEscapeAlways => escape_name( + OsStr::new(file_name), + UucoreQuotingStyle::Shell { + escape: true, + always_quote: true, + show_control: true, + }, + UEncoding::Utf8, + ) + .to_string_lossy() + .to_string(), QuotingStyle::Quote => file_name.to_string(), } } diff --git a/tests/by-util/test_stat.rs b/tests/by-util/test_stat.rs index c536a7744f7..291aa3b876c 100644 --- a/tests/by-util/test_stat.rs +++ b/tests/by-util/test_stat.rs @@ -449,6 +449,39 @@ fn test_quoting_style_locale() { .stdout_only("\'\"\'\n"); } +#[test] +fn test_quoting_newline_in_filename() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + // example from issue #9925 + at.touch("test\nnewline"); + ts.ucmd() + .args(&["-c", r#"{"name":"%N"}"#, "test\nnewline"]) + .succeeds() + .stdout_only("{\"name\":\"'test'$'\\n''newline'\"}\n"); + + // with stat, contiguous escape characters are clumped into one escape sequence + at.touch("contiguous\n\nescape_characters"); + ts.ucmd() + .args(&["-c", "%N", "contiguous\n\nescape_characters"]) + .succeeds() + .stdout_only("'contiguous'$'\\n\\n''escape_characters'\n"); + + at.touch("multiple\nescape\ncharacters"); + ts.ucmd() + .args(&["-c", "%N", "multiple\nescape\ncharacters"]) + .succeeds() + .stdout_only("'multiple'$'\\n''escape'$'\\n''characters'\n"); + + // testing other escape characters + at.touch("\t \n \r \x01"); + ts.ucmd() + .args(&["-c", "%N", "\t \n \r \x01"]) + .succeeds() + .stdout_only("''$'\\t'' '$'\\n'' '$'\\r'' '$'\\001'\n"); +} + #[test] fn test_quoting_style_invalid_env() { let ts = TestScenario::new(util_name!());