diff --git a/src/uu/mkdir/Cargo.toml b/src/uu/mkdir/Cargo.toml index bbcd30bae3a..2407ce48032 100644 --- a/src/uu/mkdir/Cargo.toml +++ b/src/uu/mkdir/Cargo.toml @@ -23,6 +23,7 @@ uucore = { workspace = true, features = [ "fs", "fsxattr", "mode", + "quoting-style", ] } fluent = { workspace = true } diff --git a/src/uu/mkdir/locales/en-US.ftl b/src/uu/mkdir/locales/en-US.ftl index 44fd5f5adf3..d724bee59f3 100644 --- a/src/uu/mkdir/locales/en-US.ftl +++ b/src/uu/mkdir/locales/en-US.ftl @@ -10,6 +10,7 @@ mkdir-help-selinux = set SELinux security context of each created directory to t mkdir-help-context = like -Z, or if CTX is specified then set the SELinux or SMACK security context to CTX # Error messages +mkdir-error-invalid-mode = invalid mode { $mode } mkdir-error-empty-directory-name = cannot create directory '': No such file or directory mkdir-error-file-exists = { $path }: File exists mkdir-error-failed-to-create-tree = failed to create whole tree diff --git a/src/uu/mkdir/locales/fr-FR.ftl b/src/uu/mkdir/locales/fr-FR.ftl index d92bed96520..f7b0c58c859 100644 --- a/src/uu/mkdir/locales/fr-FR.ftl +++ b/src/uu/mkdir/locales/fr-FR.ftl @@ -10,6 +10,7 @@ mkdir-help-selinux = définir le contexte de sécurité SELinux de chaque réper mkdir-help-context = comme -Z, ou si CTX est spécifié, définir le contexte de sécurité SELinux ou SMACK à CTX # Messages d'erreur +mkdir-error-invalid-mode = mode invalide { $mode } mkdir-error-empty-directory-name = impossible de créer le répertoire '' : Aucun fichier ou répertoire de ce type mkdir-error-file-exists = { $path } : Le fichier existe mkdir-error-failed-to-create-tree = échec de la création de l'arborescence complète diff --git a/src/uu/mkdir/src/mkdir.rs b/src/uu/mkdir/src/mkdir.rs index f8d62ad1e85..da53d1f0e5d 100644 --- a/src/uu/mkdir/src/mkdir.rs +++ b/src/uu/mkdir/src/mkdir.rs @@ -8,7 +8,7 @@ use clap::builder::ValueParser; use clap::parser::ValuesRef; use clap::{Arg, ArgAction, ArgMatches, Command}; -use std::ffi::OsString; +use std::ffi::{OsStr, OsString}; use std::io::{Write, stdout}; use std::path::{Path, PathBuf}; #[cfg(not(windows))] @@ -16,6 +16,8 @@ use uucore::error::ExitCode; use uucore::error::{UResult, USimpleError}; #[cfg(not(windows))] use uucore::mode; +#[cfg(not(windows))] +use uucore::quoting_style::{Quotes, QuotingStyle, locale_aware_escape_name}; use uucore::translate; use uucore::{display::Quotable, fs::dir_strip_dot_for_creation}; use uucore::{format_usage, show_if_err}; @@ -74,7 +76,18 @@ fn get_mode(matches: &ArgMatches, diag_args: Option<&[OsString]>) -> UResult quoted_mode), + ) } }) } diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index b52c89d62b3..0b20711533e 100644 --- a/src/uucore/Cargo.toml +++ b/src/uucore/Cargo.toml @@ -175,7 +175,7 @@ i18n-datetime = [ "jiff-icu", "jiff", ] -mode = ["diagnostics", "libc"] +mode = ["diagnostics", "libc", "quoting-style"] perms = ["entries", "libc", "walkdir"] buf-copy = [] parser-num = ["extendedbigdecimal", "num-traits"] diff --git a/src/uucore/src/lib/features/mode.rs b/src/uucore/src/lib/features/mode.rs index 1e289cfa3c1..2b1a2ffa555 100644 --- a/src/uucore/src/lib/features/mode.rs +++ b/src/uucore/src/lib/features/mode.rs @@ -7,12 +7,14 @@ // spell-checker:ignore (vars) fperm srwx +use std::ffi::{OsStr, OsString}; use std::fmt::{self, Display}; use std::ops::Range; #[cfg(windows)] use libc::umask; +use crate::quoting_style::{QuotingStyle, locale_aware_escape_name}; use crate::translate; /// A mode string that does not parse, and the part of it that is at fault. @@ -73,7 +75,7 @@ impl ModeError { /// the caller should fall back to the plain one-line message. pub fn render( &self, - args: &[std::ffi::OsString], + args: &[OsString], mode: &str, clause_start: usize, message: &str, @@ -84,16 +86,77 @@ impl ModeError { ModeErrorKind::InvalidNumber => "mode-diag-label-invalid-number", }; - crate::diagnostics::Snapshot::new(args).render_inside( - mode, - clause_start + self.span.start..clause_start + self.span.end, - message, - &translate!(label), - Some(&translate!("mode-diag-help-syntax")), + let range = clause_start + self.span.start..clause_start + self.span.end; + let label = translate!(label); + let help = translate!("mode-diag-help-syntax"); + + if !mode.chars().any(char::is_control) && !message.chars().any(char::is_control) { + return crate::diagnostics::Snapshot::new(args).render_inside( + mode, + range, + message, + &label, + Some(&help), + ); + } + + let Some(prefix) = mode.get(..range.start) else { + return false; + }; + let Some(fault) = mode.get(range.clone()) else { + return false; + }; + let Some(suffix) = mode.get(range.end..) else { + return false; + }; + let escaped_prefix = escape_diagnostic_text(prefix); + let escaped_fault = escape_diagnostic_text(fault); + let escaped_mode = format!( + "{escaped_prefix}{escaped_fault}{}", + escape_diagnostic_text(suffix) + ); + let escaped_range = escaped_prefix.len()..escaped_prefix.len() + escaped_fault.len(); + + let Some(index) = args + .iter() + .position(|arg| arg.as_encoded_bytes().ends_with(mode.as_bytes())) + else { + return false; + }; + let Some(arg) = args[index].to_str() else { + return false; + }; + let arg_prefix = &arg[..arg.len() - mode.len()]; + let mut escaped_args: Vec = args.to_vec(); + escaped_args[index] = format!("{arg_prefix}{escaped_mode}").into(); + + crate::diagnostics::Snapshot::new(&escaped_args).render_inside( + &escaped_mode, + escaped_range, + &escape_diagnostic_text(message), + &label, + Some(&help), ) } } +fn escape_diagnostic_text(text: &str) -> String { + let mut escaped = String::with_capacity(text.len()); + for character in text.chars() { + if character.is_control() { + let mut buffer = [0; 4]; + let character = character.encode_utf8(&mut buffer); + let quoted = locale_aware_escape_name(OsStr::new(character), QuotingStyle::C_NO_QUOTES) + .into_string() + .expect("C-style quoting always produces valid UTF-8"); + escaped.push_str("ed); + } else { + escaped.push(character); + } + } + escaped +} + impl Display for ModeError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&self.message) diff --git a/tests/by-util/test_mkdir.rs b/tests/by-util/test_mkdir.rs index e62c70d6ebb..d2190e08995 100644 --- a/tests/by-util/test_mkdir.rs +++ b/tests/by-util/test_mkdir.rs @@ -1122,12 +1122,34 @@ mod diagnostics { #[test] fn test_plain_message_when_stderr_is_a_pipe() { // The test harness pipes stderr, so the report must not appear. - let result = new_ucmd!() + new_ucmd!() .args(&["-m", "u+rw?x", "some_dir"]) + .fails_with_code(1) + .stderr_only("mkdir: invalid mode 'u+rw?x'\n"); + } + + #[test] + fn test_plain_message_quotes_control_character() { + new_ucmd!() + .args(&["-m", "a=\x01", "some_dir"]) + .fails_with_code(1) + .stderr_only("mkdir: invalid mode 'a=\\001'\n"); + } + + #[test] + fn test_terminal_message_quotes_control_character() { + let result = new_ucmd!() + .terminal_sim_stderr() + .args(&["-m", "a=\x01", "some_dir"]) .fails_with_code(1); let stderr = result.stderr_str(); - assert!(stderr.starts_with("mkdir: "), "{stderr}"); - assert!(!stderr.contains(":1:"), "{stderr}"); + assert!( + stderr.contains("invalid operator (expected +, -, or =, but found \\001)"), + "{stderr}" + ); + assert!(stderr.contains("1 │ -m a=\\001 some_dir"), "{stderr}"); + assert_eq!(caret_column(stderr), Some(6), "{stderr}"); + assert!(!stderr.contains('\x01'), "{stderr:?}"); } }