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
1 change: 1 addition & 0 deletions src/uu/mkdir/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ uucore = { workspace = true, features = [
"fs",
"fsxattr",
"mode",
"quoting-style",
] }
fluent = { workspace = true }

Expand Down
1 change: 1 addition & 0 deletions src/uu/mkdir/locales/en-US.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/uu/mkdir/locales/fr-FR.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 15 additions & 2 deletions src/uu/mkdir/src/mkdir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@
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))]
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};
Expand Down Expand Up @@ -74,7 +76,18 @@ fn get_mode(matches: &ArgMatches, diag_args: Option<&[OsString]>) -> UResult<Opt
// The diagnostic is already on stderr; exit quietly.
ExitCode::new(1)
} else {
USimpleError::new(1, err.to_string())
let quoted_mode = locale_aware_escape_name(
OsStr::new(m),
QuotingStyle::C {
quotes: Quotes::Single,
},
)
.into_string()
.expect("C-style quoting always produces valid UTF-8");
USimpleError::new(
1,
translate!("mkdir-error-invalid-mode", "mode" => quoted_mode),
)
}
})
}
Expand Down
2 changes: 1 addition & 1 deletion src/uucore/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
77 changes: 70 additions & 7 deletions src/uucore/src/lib/features/mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand All @@ -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<OsString> = 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(&quoted);
} else {
escaped.push(character);
}
}
escaped
}

impl Display for ModeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.message)
Expand Down
28 changes: 25 additions & 3 deletions tests/by-util/test_mkdir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:?}");
}
}
Loading