diff --git a/po/de.po b/po/de.po index cebefc353..2e724154c 100644 --- a/po/de.po +++ b/po/de.po @@ -17,7 +17,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" #. TRANSLATORS: This is a well-known quote, try to preserve it in translation. -#: /home/marc/sudo-rs/src/common/error.rs:69 +#: src/common/error.rs:84 #, rust-format msgid "I'm sorry {user}. I'm afraid I can't do that" msgstr "Es tut mir leid, {user}. Ich fürchte, das kann ich nicht" diff --git a/po/es.po b/po/es.po index 2235410a2..5d5e88081 100644 --- a/po/es.po +++ b/po/es.po @@ -17,7 +17,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" #. TRANSLATORS: This is a well-known quote, try to preserve it in translation. -#: /home/marc/sudo-rs/src/common/error.rs:69 +#: src/common/error.rs:84 #, rust-format msgid "I'm sorry {user}. I'm afraid I can't do that" msgstr "Lo siento, {user}. Me temo que no puedo hacer eso." diff --git a/po/fr.po b/po/fr.po index 30829af10..6fce19a70 100644 --- a/po/fr.po +++ b/po/fr.po @@ -17,7 +17,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" #. TRANSLATORS: This is a well-known quote, try to preserve it in translation. -#: /home/marc/sudo-rs/src/common/error.rs:69 +#: src/common/error.rs:84 #, rust-format msgid "I'm sorry {user}. I'm afraid I can't do that" msgstr "Je suis désolé, {user}. J'ai bien peur de ne pas pouvoir faire ça." diff --git a/po/fy.po b/po/fy.po index e74adbd23..b74dcba65 100644 --- a/po/fy.po +++ b/po/fy.po @@ -17,7 +17,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" #. TRANSLATORS: This is a well-known quote, try to preserve it in translation. -#: /home/marc/sudo-rs/src/common/error.rs:69 +#: src/common/error.rs:84 #, rust-format msgid "I'm sorry {user}. I'm afraid I can't do that" msgstr "It spyt my, {user}. Ik bin bang dat ik dat net kin." diff --git a/po/it.po b/po/it.po index 0a39bd40b..aef871858 100644 --- a/po/it.po +++ b/po/it.po @@ -17,7 +17,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" #. TRANSLATORS: This is a well-known quote, try to preserve it in translation. -#: /home/marc/sudo-rs/src/common/error.rs:69 +#: src/common/error.rs:84 #, rust-format msgid "I'm sorry {user}. I'm afraid I can't do that" msgstr "Mi dispiace, {user}. Temo di non poterlo fare." diff --git a/po/nl.po b/po/nl.po index ba974b074..44d7b3756 100644 --- a/po/nl.po +++ b/po/nl.po @@ -17,7 +17,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" #. TRANSLATORS: This is a well-known quote, try to preserve it in translation. -#: /home/marc/sudo-rs/src/common/error.rs:69 +#: src/common/error.rs:84 #, rust-format msgid "I'm sorry {user}. I'm afraid I can't do that" msgstr "Het spijt me, {user}. Ik ben bang dat ik dat niet kan." diff --git a/po/ro.po b/po/ro.po index b54fb292d..2cce36517 100644 --- a/po/ro.po +++ b/po/ro.po @@ -17,7 +17,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" #. TRANSLATORS: This is a well-known quote, try to preserve it in translation. -#: /home/marc/sudo-rs/src/common/error.rs:69 +#: src/common/error.rs:84 #, rust-format msgid "I'm sorry {user}. I'm afraid I can't do that" msgstr "Îmi pare rău, {user}. Mi-e teamă că nu pot face asta." diff --git a/po/sudo-rs.pot b/po/sudo-rs.pot index 71b2117a9..44126706e 100644 --- a/po/sudo-rs.pot +++ b/po/sudo-rs.pot @@ -18,7 +18,510 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" #. TRANSLATORS: This is a well-known quote, try to preserve it in translation. -#: /home/marc/sudo-rs/src/common/error.rs:69 +#: src/common/error.rs:84 #, rust-format msgid "I'm sorry {user}. I'm afraid I can't do that" msgstr "" + +#: src/sudoers/entry/verbose.rs:33 +msgid "Commands:" +msgstr "" + +#: src/sudoers/entry/verbose.rs:46 +msgid "Sudoers entry:" +msgstr "" + +#. TRANSLATORS: This is sudo-specific jargon. +#: src/sudoers/entry/verbose.rs:54 +msgid "RunAsUsers" +msgstr "" + +#. TRANSLATORS: This is sudo-specific jargon. +#: src/sudoers/entry/verbose.rs:64 +msgid "RunAsGroups" +msgstr "" + +#: src/sudoers/entry/verbose.rs:70 +msgid "Options" +msgstr "" + +#. TRANSLATORS: This is sudo-specific jargon. +#: src/sudoers/entry/verbose.rs:79 +msgid "Cwd" +msgstr "" + +#: src/pam/askpass.rs:55 +#, rust-format +msgid "Failed to run askpass program {path}: {error}" +msgstr "" + +#: src/pam/error.rs:211 src/common/error.rs:131 src/common/error.rs:138 +msgid "Unexpected null character in input" +msgstr "" + +#: src/pam/error.rs:212 +msgid "Could not read input data as UTF-8 string" +msgstr "" + +#: src/pam/error.rs:214 +msgid "Account validation failure, is your account locked?" +msgstr "" + +#: src/pam/error.rs:219 +msgid "Account or password is expired, reset your password and try again" +msgstr "" + +#: src/pam/error.rs:223 +msgid "Password expired, contact your system administrator" +msgstr "" + +#: src/pam/error.rs:225 +#, rust-format +msgid "PAM error: {error}" +msgstr "" + +#: src/pam/error.rs:226 src/common/error.rs:111 +#, rust-format +msgid "IO error: {error}" +msgstr "" + +#: src/pam/error.rs:227 +msgid "A terminal is required to authenticate" +msgstr "" + +#: src/pam/error.rs:231 +msgid "It was not possible to get a list of environment variables" +msgstr "" + +#: src/pam/error.rs:234 +msgid "Interaction is required" +msgstr "" + +#: src/pam/error.rs:236 +msgid "Authentication required but not attempted" +msgstr "" + +#: src/pam/error.rs:239 +msgid "Incorrect authentication attempt" +msgstr "" + +#: src/pam/error.rs:241 +msgid "timed out" +msgstr "" + +#: src/pam/error.rs:245 +#, rust-format +msgid "Sorry, user {user} is not allowed to authenticate as {other_user}." +msgstr "" + +#: src/pam/error.rs:251 +msgid "No askpass program specified in SUDO_ASKPASS" +msgstr "" + +#: src/pam/error.rs:256 +#, rust-format +msgid "Askpass program '{path}' is not an absolute path" +msgstr "" + +#: src/pam/mod.rs:83 +msgid "authenticate" +msgstr "" + +#: src/pam/converse.rs:147 +msgid "input needed" +msgstr "" + +#: src/system/audit.rs:133 +#, rust-format +msgid "{path} must be owned by root" +msgstr "" + +#: src/system/audit.rs:138 +#, rust-format +msgid "{path} cannot be group-writable" +msgstr "" + +#: src/system/audit.rs:143 +#, rust-format +msgid "{path} cannot be world-writable" +msgstr "" + +#: src/system/audit.rs:184 +#, rust-format +msgid "{path} has no valid parent directory" +msgstr "" + +#: src/system/audit.rs:252 +msgid "invalid path" +msgstr "" + +#: src/system/audit.rs:260 +msgid "path must be absolute" +msgstr "" + +#: src/system/audit.rs:276 +msgid "cannot open a file in a path writable by the user" +msgstr "" + +#: src/system/audit.rs:294 +msgid "error in provided path" +msgstr "" + +#: src/common/error.rs:55 +#, rust-format +msgid "" +"Sorry, user {user} is not allowed to execute '{command}' as {other_user} on " +"{hostname}." +msgstr "" + +#: src/common/error.rs:64 +#, rust-format +msgid "Sorry, user {user} may not run {command} on {hostname}." +msgstr "" + +#: src/common/error.rs:72 +msgid "sudo must be owned by uid 0 and have the setuid bit set" +msgstr "" + +#: src/common/error.rs:75 +#, rust-format +msgid "'{path}': command not found" +msgstr "" + +#: src/common/error.rs:78 +#, rust-format +msgid "'{path}': invalid command" +msgstr "" + +#: src/common/error.rs:80 +#, rust-format +msgid "user '{user}' not found" +msgstr "" + +#: src/common/error.rs:81 +#, rust-format +msgid "group '{group}' not found" +msgstr "" + +#: src/common/error.rs:86 +msgid "interactive authentication is required" +msgstr "" + +#: src/common/error.rs:90 +msgid "you are not allowed to set the following environment variables:" +msgstr "" + +#: src/common/error.rs:106 +#, rust-format +msgid "cannot execute '{path}': {error}" +msgstr "" + +#: src/common/error.rs:117 +#, rust-format +msgid "maximum {num} incorrect authentication attempts" +msgstr "" + +#: src/common/error.rs:123 +#, rust-format +msgid "you are not allowed to use '--chdir {path}' with '{command}'" +msgstr "" + +#: src/common/error.rs:145 +#, rust-format +msgid "unable to change AppArmor profile to {profile}: {error}" +msgstr "" + +#: src/sudo/pam.rs:127 +msgid "Authentication failed, try again." +msgstr "" + +#: src/exec/mod.rs:141 +#, rust-format +msgid "unable to change directory to {path}: {error}" +msgstr "" + +#: src/sudo/edit.rs:46 +#, rust-format +msgid "failed to read metadata for {path}: {error}" +msgstr "" + +#: src/sudo/edit.rs:55 +#, rust-format +msgid "file {path} is not a regular file" +msgstr "" + +#: src/sudo/edit.rs:64 +#, rust-format +msgid "failed to lock {path}: {error}" +msgstr "" + +#: src/sudo/edit.rs:77 +#, rust-format +msgid "failed to read {path}: {error}" +msgstr "" + +#: src/sudo/edit.rs:144 +#, rust-format +msgid "{path} unchanged" +msgstr "" + +#: src/sudo/edit.rs:164 +#, rust-format +msgid "failed to write {path}: {error}" +msgstr "" + +#: src/sudo/edit.rs:183 +#, rust-format +msgid "failed to remove temporary directory {path}: {error}" +msgstr "" + +#: src/sudo/edit.rs:195 +#, rust-format +msgid "{error}" +msgstr "" + +#: src/sudo/edit.rs:213 +#, rust-format +msgid "failed to create temporary directory: {error}" +msgstr "" + +#: src/sudo/edit.rs:221 +#, rust-format +msgid "failed to create temporary directory {path}: {error}" +msgstr "" + +#: src/sudo/edit.rs:235 +#, rust-format +msgid "failed to create temporary file {path}: {error}" +msgstr "" + +#: src/sudo/edit.rs:244 +#, rust-format +msgid "failed to write to temporary file {path}: {error}" +msgstr "" + +#: src/sudo/edit.rs:263 +#, rust-format +msgid "failed to run editor {path}: {error}" +msgstr "" + +#: src/sudo/edit.rs:284 +#, rust-format +msgid "failed to read from temporary file {path}: {error}" +msgstr "" + +#: src/sudo/edit.rs:293 +#, rust-format +msgid "failed to remove temporary file {path}: {error}" +msgstr "" + +#. TRANSLATORS: the initial letters of 'yes' and 'no' responses, in that order +#: src/sudo/edit.rs:302 +msgid "yn" +msgstr "" + +#: src/sudo/edit.rs:306 +#, rust-format +msgid "sudoedit: truncate {path} to zero? (y/n) [n] " +msgstr "" + +#: src/sudo/edit.rs:314 +#, rust-format +msgid "not overwriting {path}" +msgstr "" + +#: src/sudo/edit.rs:318 src/sudo/edit.rs:327 +#, rust-format +msgid "failed to write data to parent: {error}" +msgstr "" + +#: src/sudo/pipeline/edit.rs:38 +#, rust-format +msgid "{path}: editing symbolic links is not permitted" +msgstr "" + +#: src/sudo/pipeline/edit.rs:43 +#, rust-format +msgid "error opening {path}: {error}" +msgstr "" + +#: src/sudo/pipeline/edit.rs:47 +#, rust-format +msgid "invalid path: {path}" +msgstr "" + +#: src/sudo/pipeline/edit.rs:52 +msgid "please address the problems and try again" +msgstr "" + +#: src/sudo/pipeline/list.rs:45 +#, rust-format +msgid "User {user} may run the following commands on {hostname}:" +msgstr "" + +#: src/sudo/pipeline/list.rs:63 +#, rust-format +msgid "User {user} is not allowed to run sudo on {hostname}." +msgstr "" + +#: src/sudo/cli/help_edit.rs:3 +msgid "" +"usage: sudoedit -h | -V\n" +"usage: sudoedit [-BknS] [-p prompt] [-g group] [-u user] file ..." +msgstr "" + +#: src/sudo/cli/help_edit.rs:10 +msgid "sudo - edit files as another user" +msgstr "" + +#: src/sudo/cli/help_edit.rs:15 +msgid "" +"Options:\n" +" -B, --bell ring bell when prompting\n" +" -g, --group=group run command as the specified group name or " +"ID\n" +" -h, --help display help message and exit\n" +" -k, --reset-timestamp invalidate timestamp file\n" +" -n, --non-interactive non-interactive mode, no prompts are used\n" +" -p, --prompt=prompt use the specified password prompt\n" +" -S, --stdin read password from standard input\n" +" -u, --user=user run command (or edit file) as specified " +"user\n" +" name or ID\n" +" -V, --version display version information and exit\n" +" -- stop processing command line arguments" +msgstr "" + +#: src/sudo/cli/mod.rs:210 src/sudo/cli/mod.rs:275 src/sudo/cli/mod.rs:347 +#: src/sudo/cli/mod.rs:433 src/sudo/cli/mod.rs:446 +#, rust-format +msgid "{option1} conflicts with {option2}" +msgstr "" + +#: src/sudo/cli/mod.rs:284 +msgid "must specify at least one file path" +msgstr "" + +#: src/sudo/cli/mod.rs:359 +#, rust-format +msgid "'{option}' flag must be accompanied by a command" +msgstr "" + +#: src/sudo/cli/mod.rs:442 +msgid "command (positional argument)" +msgstr "" + +#: src/sudo/cli/mod.rs:457 +msgid "expected one of: --login, --shell, a command as a positional argument" +msgstr "" + +#: src/sudo/cli/mod.rs:586 +#, rust-format +msgid "'{option}' does not take any arguments" +msgstr "" + +#: src/sudo/cli/mod.rs:595 src/sudo/cli/mod.rs:623 +#, rust-format +msgid "'{option}' expects an argument" +msgstr "" + +#: src/sudo/cli/mod.rs:613 +msgid "invalid option '='" +msgstr "" + +#: src/sudo/cli/mod.rs:666 +msgid "" +"expected one of these actions: --help, --version, --remove-timestamp, --" +"validate, --list, --edit, --login, --shell, a command as a positional " +"argument, --reset-timestamp" +msgstr "" + +#: src/sudo/cli/mod.rs:703 +#, rust-format +msgid "preserving the entire environment is not supported, '{flag}' is ignored" +msgstr "" + +#: src/sudo/cli/mod.rs:747 src/sudo/cli/mod.rs:776 +msgid "invalid option provided" +msgstr "" + +#: src/sudo/cli/mod.rs:794 +msgid "sudoedit doesn't need to be run via sudo" +msgstr "" + +#: src/sudo/cli/mod.rs:846 +#, rust-format +msgid "{context} cannot be used together with {option}" +msgstr "" + +#: src/sudo/cli/mod.rs:897 +msgid "command" +msgstr "" + +#: src/sudo/cli/mod.rs:898 +msgid "environment variable" +msgstr "" + +#: src/sudo/cli/help.rs:3 +msgid "" +"usage: sudo -h | -K | -k | -V\n" +"usage: sudo [-BknS] [-p prompt] [-D directory] [-g group] [-u user] [-i | " +"-s] [command [arg ...]]\n" +"usage: sudo -v [-BknS] [-p prompt] [-g group] [-u user]\n" +"usage: sudo -l [-BknS] [-p prompt] [-U user] [-g group] [-u user] [command " +"[arg ...]]\n" +"usage: sudo -e [-BknS] [-p prompt] [-D directory] [-g group] [-u user] " +"file ..." +msgstr "" + +#: src/sudo/cli/help.rs:13 +msgid "sudo - run commands as another user" +msgstr "" + +#: src/sudo/cli/help.rs:17 +msgid "" +"Options:\n" +" -B, --bell ring bell when prompting\n" +" -D, --chdir=directory change the working directory before running " +"command\n" +" -g, --group=group run command as the specified group name or " +"ID\n" +" -h, --help display help message and exit\n" +" -i, --login run login shell as the target user; a " +"command may also be specified\n" +" -K, --remove-timestamp remove timestamp file completely\n" +" -k, --reset-timestamp invalidate timestamp file\n" +" -l, --list list user's privileges or check a specific " +"command; use twice for longer format\n" +" -n, --non-interactive non-interactive mode, no prompts are used\n" +" -p, --prompt=prompt use the specified password prompt\n" +" -S, --stdin read password from standard input\n" +" -s, --shell run shell as the target user; a command may " +"also be specified\n" +" -U, --other-user=user in list mode, display privileges for user\n" +" -u, --user=user run command (or edit file) as specified user " +"name or ID\n" +" -V, --version display version information and exit\n" +" -v, --validate update user's timestamp without running a " +"command\n" +" -- stop processing command line arguments" +msgstr "" + +#: src/sudo/pipeline.rs:32 +#, rust-format +msgid "" +"sudoers file not found: {path}\n" +"\n" +"The sudoers file is required for sudo-rs to function. Please ensure:\n" +"- The file exists at the expected location\n" +"- You have the necessary permissions to read it\n" +"- If setting up sudo-rs for the first time, create a sudoers file with " +"appropriate permissions\n" +"\n" +"For more information, see the sudo-rs documentation." +msgstr "" + +#: src/sudo/pipeline.rs:43 +#, rust-format +msgid "invalid configuration: {error}" +msgstr "" diff --git a/src/common/error.rs b/src/common/error.rs index ba96bdd06..1db290925 100644 --- a/src/common/error.rs +++ b/src/common/error.rs @@ -1,4 +1,8 @@ -use crate::{gettext::xlat_write, pam::PamError, system::Hostname}; +use crate::{ + gettext::{xlat, xlat_write}, + pam::PamError, + system::Hostname, +}; use std::{borrow::Cow, fmt, path::PathBuf}; use super::{SudoPath, SudoString}; @@ -46,31 +50,42 @@ impl fmt::Display for Error { other_user, } => { if let Some(other_user) = other_user { - write!( + xlat_write!( f, - "Sorry, user {username} is not allowed to execute '{command}' as {other_user} on {hostname}.", + "Sorry, user {user} is not allowed to execute '{command}' as {other_user} on {hostname}.", + user = username, + command = command, + other_user = other_user, + hostname = hostname, ) } else { - write!( + xlat_write!( f, - "Sorry, user {username} may not run {command} on {hostname}.", + "Sorry, user {user} may not run {command} on {hostname}.", + user = username, + command = command, + hostname = hostname, ) } } Error::SelfCheck => { - f.write_str("sudo must be owned by uid 0 and have the setuid bit set") + xlat_write!(f, "sudo must be owned by uid 0 and have the setuid bit set") } - Error::CommandNotFound(p) => write!(f, "'{}': command not found", p.display()), - Error::InvalidCommand(p) => write!(f, "'{}': invalid command", p.display()), - Error::UserNotFound(u) => write!(f, "user '{u}' not found"), - Error::GroupNotFound(g) => write!(f, "group '{g}' not found"), + Error::CommandNotFound(p) => { + xlat_write!(f, "'{path}': command not found", path = p.display()) + } + Error::InvalidCommand(p) => { + xlat_write!(f, "'{path}': invalid command", path = p.display()) + } + Error::UserNotFound(u) => xlat_write!(f, "user '{user}' not found", user = u), + Error::GroupNotFound(g) => xlat_write!(f, "group '{group}' not found", group = g), Error::Authorization(u) => { // TRANSLATORS: This is a well-known quote, try to preserve it in translation. xlat_write!(f, "I'm sorry {user}. I'm afraid I can't do that", user = u) } - Error::InteractionRequired => write!(f, "interactive authentication is required"), + Error::InteractionRequired => xlat_write!(f, "interactive authentication is required"), Error::EnvironmentVar(vs) => { - write!( + xlat_write!( f, "you are not allowed to set the following environment variables:" )?; @@ -86,29 +101,51 @@ impl fmt::Display for Error { Error::Pam(e) => write!(f, "{e}"), Error::Io(location, e) => { if let Some(path) = location { - write!(f, "cannot execute '{}': {e}", path.display()) + xlat_write!( + f, + "cannot execute '{path}': {error}", + path = path.display(), + error = e + ) } else { - write!(f, "IO error: {e}") + xlat_write!(f, "IO error: {error}", error = e) } } Error::MaxAuthAttempts(num) => { - write!(f, "Maximum {num} incorrect authentication attempts") + xlat_write!( + f, + "maximum {num} incorrect authentication attempts", + num = num + ) } - Error::ChDirNotAllowed { chdir, command } => write!( + Error::ChDirNotAllowed { chdir, command } => xlat_write!( f, - "you are not allowed to use '--chdir {}' with '{}'", - chdir.display(), - command.display() + "you are not allowed to use '--chdir {path}' with '{command}'", + path = chdir.display(), + command = command.display() ), Error::StringValidation(string) => { - write!(f, "invalid string: {string:?}") + write!( + f, + "{}: {string:?}", + xlat!("Unexpected null character in input") + ) } Error::PathValidation(path) => { - write!(f, "invalid path: {path:?}") + write!( + f, + "{}: {path:?}", + xlat!("Unexpected null character in input") + ) } #[cfg(feature = "apparmor")] Error::AppArmor(profile, e) => { - write!(f, "unable to change AppArmor profile to {profile}: {e}") + xlat_write!( + f, + "unable to change AppArmor profile to {profile}: {error}", + profile = profile, + error = e + ) } } } diff --git a/src/exec/mod.rs b/src/exec/mod.rs index 8f0b4641e..39f4fb9ef 100644 --- a/src/exec/mod.rs +++ b/src/exec/mod.rs @@ -137,7 +137,11 @@ pub fn run_command( unsafe { command.pre_exec(move || { if let Err(err) = env::set_current_dir(&path) { - user_error!("unable to change directory to {}: {}", path.display(), err); + user_error!( + "unable to change directory to {path}: {error}", + path = path.display(), + error = err + ); if is_chdir { return Err(err); } diff --git a/src/gettext/mod.rs b/src/gettext/mod.rs index 03cda4c73..72d1c3c64 100644 --- a/src/gettext/mod.rs +++ b/src/gettext/mod.rs @@ -89,7 +89,7 @@ macro_rules! xlat { $crate::gettext::gettext(cstr!($text)) }}; - ($text: literal $(, $id: ident = $val: expr)*) => {{ + ($text: literal $(, $id: ident = $val: expr)* $(,)?) => {{ #[allow(unused)] use $crate::gettext::display::{Convert, Reference, Wrap}; use std::ops::Deref; @@ -104,7 +104,7 @@ macro_rules! xlat { $( let result = result.replace( concat!("{", stringify!($id), "}"), - (&&Wrap($val)).display().deref(), + (&&Wrap(&$val)).display().deref(), ); )* @@ -116,21 +116,21 @@ macro_rules! xlat { macro_rules! xlat { ($text: literal) => { $text }; - ($text: literal $(, $id: ident = $val: expr)*) => {{ + ($text: literal $(, $id: ident = $val: expr)* $(,)?) => {{ format!($text $(,$id = $val)*) }}; } #[cfg(feature = "gettext")] macro_rules! xlat_write { - ($f: expr, $fmt: literal $(, $id: ident = $val: expr)*) => { + ($f: expr, $fmt: literal $(, $id: ident = $val: expr)* $(,)?) => { write!($f, "{}", $crate::gettext::xlat!($fmt $(, $id = $val)*)) }; } #[cfg(not(feature = "gettext"))] macro_rules! xlat_write { - ($f: expr, $fmt: literal $(, $id: ident = $val: expr)*) => { + ($f: expr, $fmt: literal $(, $id: ident = $val: expr)* $(,)?) => { write!($f, $fmt $(, $id = $val)*) }; } diff --git a/src/lib.rs b/src/lib.rs index e36c7d952..3bb1efc88 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ pub(crate) mod common; pub(crate) mod cutils; pub(crate) mod defaults; pub(crate) mod exec; +#[macro_use] pub(crate) mod gettext; pub(crate) mod log; pub(crate) mod pam; @@ -22,10 +23,3 @@ pub use visudo::main as visudo_main; #[cfg(feature = "do-not-use-all-features")] compile_error!("Refusing to compile using 'cargo --all-features' --- please read the README"); - -//FIXME: this is here to ensure xlat! is used (with gettext disabled) and that -// the Convert trait is exercised. -#[allow(unused)] -fn simulate_use() -> String { - gettext::xlat!("{answer}", answer = 42) -} diff --git a/src/log/mod.rs b/src/log/mod.rs index baee06189..b8904c4ab 100644 --- a/src/log/mod.rs +++ b/src/log/mod.rs @@ -9,33 +9,34 @@ mod simple_logger; mod syslog; macro_rules! logger_macro { - ($name:ident is $rule_level:ident to $target:expr, $d:tt) => { + ($name:ident is $rule_level:ident to $target:literal with $($path1:ident)?$(::$path2:ident)*, $d:tt) => { macro_rules! $name { ($d($d arg:tt)+) => { if let Some(logger) = $crate::log::LOGGER.get() { - logger.log($crate::log::Level::$rule_level, $target, format_args!($d($d arg)+)); + logger.log($crate::log::Level::$rule_level, $target, $($path1)?$(::$path2)*!($d($d arg)+)); } }; } pub(crate) use $name; }; - ($name:ident is $rule_level:ident to $target:expr) => { - logger_macro!($name is $rule_level to $target, $); + + ($name:ident is $rule_level:ident to $target:literal with $($path1:ident)?$(::$path2:ident)*) => { + logger_macro!($name is $rule_level to $target with $($path1)?$(::$path2)*, $); }; } // logger_macro!(auth_error is Error to "sudo::auth"); -logger_macro!(auth_warn is Warn to "sudo::auth"); -logger_macro!(auth_info is Info to "sudo::auth"); -// logger_macro!(auth_debug is Debug to "sudo::auth"); -// logger_macro!(auth_trace is Trace to "sudo::auth"); +logger_macro!(auth_warn is Warn to "sudo::auth" with format_args); +logger_macro!(auth_info is Info to "sudo::auth" with format_args); +// logger_macro!(auth_debug is Debug to "sudo::auth" with format_args); +// logger_macro!(auth_trace is Trace to "sudo::auth" with format_args); -logger_macro!(user_error is Error to "sudo::user"); -logger_macro!(user_warn is Warn to "sudo::user"); -logger_macro!(user_info is Info to "sudo::user"); -// logger_macro!(user_debug is Debug to "sudo::user"); -// logger_macro!(user_trace is Trace to "sudo::user"); +logger_macro!(user_error is Error to "sudo::user" with crate::gettext::xlat); +logger_macro!(user_warn is Warn to "sudo::user" with crate::gettext::xlat); +logger_macro!(user_info is Info to "sudo::user" with crate::gettext::xlat); +// logger_macro!(user_debug is Debug to "sudo::user" with crate::gettext::xlat); +// logger_macro!(user_trace is Trace to "sudo::user" with crate::gettext::xlat); macro_rules! dev_logger_macro { ($name:ident is $rule_level:ident to $target:expr, $d:tt) => { @@ -119,7 +120,7 @@ impl SudoLogger { } impl SudoLogger { - pub fn log(&self, level: Level, target: &str, args: fmt::Arguments<'_>) { + pub fn log(&self, level: Level, target: &str, args: impl fmt::Display) { for (prefix, l) in self.0.iter() { if target == &prefix[..prefix.len() - 2] || target.starts_with(prefix) { l.log(level, &args); @@ -139,7 +140,7 @@ pub enum Level { } trait Log: Send + Sync { - fn log(&self, level: Level, args: &fmt::Arguments<'_>); + fn log(&self, level: Level, args: &dyn fmt::Display); } #[cfg(test)] diff --git a/src/log/simple_logger.rs b/src/log/simple_logger.rs index 928fd25ca..5e96339f1 100644 --- a/src/log/simple_logger.rs +++ b/src/log/simple_logger.rs @@ -18,7 +18,7 @@ impl Log for SimpleLogger where for<'a> &'a W: Write, { - fn log(&self, _level: Level, args: &fmt::Arguments<'_>) { + fn log(&self, _level: Level, args: &dyn fmt::Display) { let s = format!("{}{}\n", self.prefix, args); let _ = (&self.target).write_all(s.as_bytes()); let _ = (&self.target).flush(); diff --git a/src/log/syslog.rs b/src/log/syslog.rs index 4aa2765e6..5116a5f58 100644 --- a/src/log/syslog.rs +++ b/src/log/syslog.rs @@ -131,7 +131,7 @@ impl Write for SysLogMessageWriter { const FACILITY: c_int = libc::LOG_AUTH; impl Log for Syslog { - fn log(&self, level: Level, args: &fmt::Arguments<'_>) { + fn log(&self, level: Level, args: &dyn fmt::Display) { let priority = match level { Level::Error => libc::LOG_ERR, Level::Warn => libc::LOG_WARNING, diff --git a/src/pam/askpass.rs b/src/pam/askpass.rs index ad8f0e672..45ba27f0f 100644 --- a/src/pam/askpass.rs +++ b/src/pam/askpass.rs @@ -7,6 +7,7 @@ use std::{io, process}; use libc::O_CLOEXEC; use crate::cutils::cerr; +use crate::log::user_error; use crate::system::interface::ProcessId; use crate::system::{fork, mark_fds_as_cloexec, ForkResult}; @@ -50,9 +51,10 @@ fn handle_child(program: &Path, prompt: &str, stdout: OwnedFd) -> ! { // Exec askpass program let error = Command::new(program).arg(prompt).stdout(stdout).exec(); - eprintln_ignore_io_error!( - "Failed to run askpass program {}: {error}", - program.display() + user_error!( + "Failed to run askpass program {path}: {error}", + path = program.display(), + error = error ); process::exit(1); } diff --git a/src/pam/converse.rs b/src/pam/converse.rs index 0ca556215..454df9b6e 100644 --- a/src/pam/converse.rs +++ b/src/pam/converse.rs @@ -144,8 +144,9 @@ impl CLIConverser { impl Converser for CLIConverser { fn handle_normal_prompt(&self, msg: &str) -> PamResult { let (mut tty, _guard) = self.open()?; + let input_needed = xlat!("input needed"); tty.read_input( - &format!("[{}: input needed] {msg} ", self.name), + &format!("[{}: {input_needed} {msg} ", self.name), None, Hidden::No, ) diff --git a/src/pam/error.rs b/src/pam/error.rs index 0024825c4..65aab9bc8 100644 --- a/src/pam/error.rs +++ b/src/pam/error.rs @@ -208,45 +208,53 @@ impl From for PamError { impl fmt::Display for PamError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - PamError::UnexpectedNulByte(_) => write!(f, "Unexpected nul byte in input"), - PamError::Utf8Error(_) => write!(f, "Could not read input data as UTF-8 string"), + PamError::UnexpectedNulByte(_) => xlat_write!(f, "Unexpected null character in input"), + PamError::Utf8Error(_) => xlat_write!(f, "Could not read input data as UTF-8 string"), PamError::Pam(PamErrorType::AuthError) => { - write!(f, "Account validation failure, is your account locked?") + xlat_write!(f, "Account validation failure, is your account locked?") } PamError::Pam(PamErrorType::NewAuthTokenRequired) => { - write!( + xlat_write!( f, "Account or password is expired, reset your password and try again" ) } PamError::Pam(PamErrorType::AuthTokenExpired) => { - write!(f, "Password expired, contact your system administrator") + xlat_write!(f, "Password expired, contact your system administrator") } - PamError::Pam(tp) => write!(f, "PAM error: {}", tp.get_err_msg()), - PamError::IoError(e) => write!(f, "IO error: {e}"), - PamError::TtyRequired => write!(f, "A terminal is required to authenticate"), + PamError::Pam(tp) => xlat_write!(f, "PAM error: {error}", error = tp.get_err_msg()), + PamError::IoError(e) => xlat_write!(f, "IO error: {error}", error = e), + PamError::TtyRequired => xlat_write!(f, "A terminal is required to authenticate"), PamError::EnvListFailure => { - write!( + xlat_write!( f, "It was not possible to get a list of environment variables" ) } - PamError::InteractionRequired => write!(f, "Interaction is required"), - PamError::NoPasswordProvided => write!(f, "Authentication required but not attempted"), - PamError::IncorrectPasswordAttempt => write!(f, "Incorrect authentication attempt"), - PamError::TimedOut => write!(f, "timed out"), + PamError::InteractionRequired => xlat_write!(f, "Interaction is required"), + PamError::NoPasswordProvided => { + xlat_write!(f, "Authentication required but not attempted") + } + PamError::IncorrectPasswordAttempt => { + xlat_write!(f, "Incorrect authentication attempt") + } + PamError::TimedOut => xlat_write!(f, "timed out"), PamError::InvalidUser(username, other_user) => { - write!( + xlat_write!( f, - "Sorry, user {username} is not allowed to authenticate as {other_user}.", + "Sorry, user {user} is not allowed to authenticate as {other_user}.", + user = username, + other_user = other_user, ) } - PamError::NoAskpassProgram => write!(f, "No askpass program specified in SUDO_ASKPASS"), + PamError::NoAskpassProgram => { + xlat_write!(f, "No askpass program specified in SUDO_ASKPASS") + } PamError::InvalidAskpassProgram(program) => { - write!( + xlat_write!( f, - "Askpass program `{}` is not an absolute path", - program.display() + "Askpass program '{path}' is not an absolute path", + path = program.display() ) } } diff --git a/src/pam/mod.rs b/src/pam/mod.rs index 6e034c8ea..c9dba90e2 100644 --- a/src/pam/mod.rs +++ b/src/pam/mod.rs @@ -80,7 +80,7 @@ impl PamContext { converser, converser_name: converser_name.to_owned(), no_interact, - auth_prompt: Some("authenticate".to_owned()), + auth_prompt: Some(xlat!("authenticate").to_owned()), error: None, panicked: false, })); diff --git a/src/su/context.rs b/src/su/context.rs index 62831234f..7940f5a70 100644 --- a/src/su/context.rs +++ b/src/su/context.rs @@ -144,8 +144,8 @@ impl SuContext { // taken into account, unless su is called by root. if is_restricted(user_shell.as_path()) && !is_current_root { user_warn!( - "using restricted shell {}", - user_shell.as_os_str().to_string_lossy() + "using restricted shell {path}", + path = user_shell.as_os_str().to_string_lossy() ); command = user_shell; } diff --git a/src/sudo/cli/help.rs b/src/sudo/cli/help.rs index cbf4e20ba..a0603a386 100644 --- a/src/sudo/cli/help.rs +++ b/src/sudo/cli/help.rs @@ -1,13 +1,20 @@ -pub const USAGE_MSG: &str = "\ +pub fn usage_msg() -> &'static str { + xlat!( + "\ usage: sudo -h | -K | -k | -V usage: sudo [-BknS] [-p prompt] [-D directory] [-g group] [-u user] [-i | -s] [command [arg ...]] usage: sudo -v [-BknS] [-p prompt] [-g group] [-u user] usage: sudo -l [-BknS] [-p prompt] [-U user] [-g group] [-u user] [command [arg ...]] -usage: sudo -e [-BknS] [-p prompt] [-D directory] [-g group] [-u user] file ..."; +usage: sudo -e [-BknS] [-p prompt] [-D directory] [-g group] [-u user] file ..." + ) +} -const DESCRIPTOR: &str = "sudo - run commands as another user"; +fn descriptor() -> &'static str { + xlat!("sudo - run commands as another user") +} -const HELP_MSG: &str = "Options: +fn help_msg() -> &'static str { + xlat!("Options: -B, --bell ring bell when prompting -D, --chdir=directory change the working directory before running command -g, --group=group run command as the specified group name or ID @@ -24,8 +31,9 @@ const HELP_MSG: &str = "Options: -u, --user=user run command (or edit file) as specified user name or ID -V, --version display version information and exit -v, --validate update user's timestamp without running a command - -- stop processing command line arguments"; + -- stop processing command line arguments") +} pub fn long_help_message() -> String { - format!("{DESCRIPTOR}\n{USAGE_MSG}\n{HELP_MSG}") + format!("{}\n{}\n{}", descriptor(), usage_msg(), help_msg()) } diff --git a/src/sudo/cli/help_edit.rs b/src/sudo/cli/help_edit.rs index 654626821..d9b785858 100644 --- a/src/sudo/cli/help_edit.rs +++ b/src/sudo/cli/help_edit.rs @@ -1,11 +1,18 @@ -pub const USAGE_MSG: &str = "\ +pub fn usage_msg() -> &'static str { + xlat!( + "\ usage: sudoedit -h | -V -usage: sudoedit [-BknS] [-p prompt] [-g group] [-u user] file ..."; +usage: sudoedit [-BknS] [-p prompt] [-g group] [-u user] file ..." + ) +} -const DESCRIPTOR: &str = "sudo - edit files as another user"; +fn descriptor() -> &'static str { + xlat!("sudo - edit files as another user") +} -const HELP_MSG: &str = "Options: -Options: +fn help_msg() -> &'static str { + xlat!( + "Options: -B, --bell ring bell when prompting -g, --group=group run command as the specified group name or ID -h, --help display help message and exit @@ -16,8 +23,10 @@ Options: -u, --user=user run command (or edit file) as specified user name or ID -V, --version display version information and exit - -- stop processing command line arguments"; + -- stop processing command line arguments" + ) +} pub fn long_help_message() -> String { - format!("{DESCRIPTOR}\n{USAGE_MSG}\n{HELP_MSG}") + format!("{}\n{}\n{}", descriptor(), usage_msg(), help_msg()) } diff --git a/src/sudo/cli/mod.rs b/src/sudo/cli/mod.rs index 53057074b..a0e9fc65f 100644 --- a/src/sudo/cli/mod.rs +++ b/src/sudo/cli/mod.rs @@ -206,7 +206,11 @@ impl TryFrom for SudoValidateOptions { let user = mem::take(&mut opts.user); if bell && stdin { - return Err("--bell conflicts with --stdin".into()); + return Err(xlat!( + "{option1} conflicts with {option2}", + option1 = "--bell", + option2 = "--stdin" + )); } reject_all("--validate", opts)?; @@ -267,13 +271,17 @@ impl TryFrom for SudoEditOptions { let positional_args = mem::take(&mut opts.positional_args); if bell && stdin { - return Err("--bell conflicts with --stdin".into()); + return Err(xlat!( + "{option1} conflicts with {option2}", + option1 = "--bell", + option2 = "--stdin" + )); } reject_all("--edit", opts)?; if positional_args.is_empty() { - return Err("must specify at least one file path".into()); + return Err(xlat!("must specify at least one file path").into()); } Ok(Self { @@ -335,7 +343,11 @@ impl TryFrom for SudoListOptions { let positional_args = mem::take(&mut opts.positional_args); if bell && stdin { - return Err("--bell conflicts with --stdin".into()); + return Err(xlat!( + "{option1} conflicts with {option2}", + option1 = "--bell", + option2 = "--stdin" + )); } // when present, `-u` must be accompanied by a command @@ -343,7 +355,10 @@ impl TryFrom for SudoListOptions { let valid_user_flag = user.is_none() || has_command; if !valid_user_flag { - return Err("'--user' flag must be accompanied by a command".into()); + return Err(xlat!( + "'{option}' flag must be accompanied by a command", + option = "--user" + )); } reject_all("--list", opts)?; @@ -414,24 +429,34 @@ impl TryFrom for SudoRunOptions { let positional_args = mem::take(&mut opts.positional_args); if bell && stdin { - return Err("--bell conflicts with --stdin".into()); + return Err(xlat!( + "{option1} conflicts with {option2}", + option1 = "--bell", + option2 = "--stdin" + )); } let context = match (login, shell, positional_args.is_empty()) { (true, false, _) => "--login", (false, true, _) => "--shell", - (false, false, false) => "command (positional argument)", - - (true, true, _) => return Err("--login conflicts with --shell".into()), + (false, false, false) => xlat!("command (positional argument)"), + + (true, true, _) => { + return Err(xlat!( + "{option1} conflicts with {option2}", + option1 = "--login", + option2 = "--shell" + )) + } (false, false, true) => { if cfg!(debug_assertions) { // see `SudoOptions::validate` panic!(); } else { - return Err( + return Err(xlat!( "expected one of: --login, --shell, a command as a positional argument" - .into(), - ); + ) + .into()); } } }; @@ -557,14 +582,17 @@ impl SudoArg { // only accept arguments when one is expected // `--preserve-env` is special as it only takes an argument using this `key=value` syntax if !Self::TAKES_ARGUMENT.contains(&key) && key != "preserve-env" { - Err(format!("'{key}' does not take any arguments"))?; + Err(xlat!( + "'{option}' does not take any arguments", + option = key + ))?; } processed.push(SudoArg::Argument("--".to_string() + key, value.to_string())); } else if Self::TAKES_ARGUMENT.contains(&unprefixed) { if let Some(next) = arg_iter.next() { processed.push(SudoArg::Argument(arg, next)); } else { - Err(format!("'{arg}' expects an argument"))?; + Err(xlat!("'{option}' expects an argument", option = arg))?; } } else { processed.push(SudoArg::Flag(arg)); @@ -582,7 +610,7 @@ impl SudoArg { // assignment syntax is not accepted for shorthand arguments if next == Some('=') { - Err("invalid option '='")?; + Err(xlat!("invalid option '='"))?; } if next.is_some() { processed.push(SudoArg::Argument(flag, rest.to_string())); @@ -592,7 +620,7 @@ impl SudoArg { // short version of --help has no arguments processed.push(SudoArg::Flag(flag)); } else { - Err(format!("'-{curr}' expects an argument"))?; + Err(xlat!("'{option}' expects an argument", option = flag))?; } break; } else { @@ -635,7 +663,7 @@ impl SudoOptions { } else if self.reset_timestamp { SudoAction::ResetTimestamp(self.try_into()?) } else { - return Err("expected one of these actions: --help, --version, --remove-timestamp, --validate, --list, --edit, --login, --shell, a command as a positional argument, --reset-timestamp".into()); + return Err(xlat!("expected one of these actions: --help, --version, --remove-timestamp, --validate, --list, --edit, --login, --shell, a command as a positional argument, --reset-timestamp").into()); } }; @@ -672,7 +700,8 @@ impl SudoOptions { } "-E" | "--preserve-env" => { user_warn!( - "preserving the entire environment is not supported, `{flag}` is ignored" + "preserving the entire environment is not supported, '{flag}' is ignored", + flag = flag ) } "-e" | "--edit" if !invoked_as_sudoedit => { @@ -715,7 +744,7 @@ impl SudoOptions { options.validate = true; } _option => { - Err("invalid option provided")?; + Err(xlat!("invalid option provided"))?; } }, SudoArg::Argument(option, value) => match option.as_str() { @@ -744,7 +773,7 @@ impl SudoOptions { options.user = Some(SudoString::from_cli_string(value)); } _option => { - Err("invalid option provided")?; + Err(xlat!("invalid option provided"))?; } }, SudoArg::Environment(key, value) => { @@ -813,13 +842,17 @@ fn ensure_is_absent(context: &str, thing: &dyn IsAbsent, name: &str) -> Result<( if thing.is_absent() { Ok(()) } else { - Err(format!("{context} cannot be used together with {name}")) + Err(xlat!( + "{context} cannot be used together with {option}", + context = context, + option = name + )) } } fn reject_all(context: &str, opts: SudoOptions) -> Result<(), String> { macro_rules! check_options { - ($($field:ident $(= $name:literal)?,)*) => {{ + ($($field:ident $(= $name:expr)?,)*) => {{ let SudoOptions { $($field),* } = opts; $( @@ -837,7 +870,7 @@ fn reject_all(context: &str, opts: SudoOptions) -> Result<(), String> { Cow::Borrowed(name) } }}; - (@name $field:ident $name:literal) => { + (@name $field:ident $name:expr) => { $name }; } @@ -861,7 +894,7 @@ fn reject_all(context: &str, opts: SudoOptions) -> Result<(), String> { user, validate, version, - positional_args = "command", - env_var_list = "environment variable", + positional_args = xlat!("command"), + env_var_list = xlat!("environment variable"), ) } diff --git a/src/sudo/edit.rs b/src/sudo/edit.rs index c106abdbd..f706f2e08 100644 --- a/src/sudo/edit.rs +++ b/src/sudo/edit.rs @@ -42,25 +42,43 @@ pub(super) fn edit_files( let metadata = file.metadata().map_err(|e| { io::Error::new( e.kind(), - format!("Failed to read metadata for {}: {e}", path.display()), + xlat!( + "failed to read metadata for {path}: {error}", + path = path.display(), + error = e + ), ) })?; if !metadata.is_file() { return Err(io::Error::new( io::ErrorKind::Other, - format!("File {} is not a regular file", path.display()), + xlat!("file {path} is not a regular file", path = path.display()), )); } // Take file lock let lock = FileLock::exclusive(&file, true).map_err(|e| { - io::Error::new(e.kind(), format!("Failed to lock {}: {e}", path.display())) + io::Error::new( + e.kind(), + xlat!( + "failed to lock {path}: {error}", + path = path.display(), + error = e + ), + ) })?; // Read file let mut old_data = Vec::new(); file.read_to_end(&mut old_data).map_err(|e| { - io::Error::new(e.kind(), format!("Failed to read {}: {e}", path.display())) + io::Error::new( + e.kind(), + xlat!( + "failed to read {path}: {error}", + path = path.display(), + error = e + ), + ) })?; // Create socket @@ -123,7 +141,7 @@ pub(super) fn edit_files( let data = file.new_data.expect("filled in above"); if data == file.old_data { // File unchanged. No need to write it again. - user_info!("{} unchanged", file.path.display()); + user_info!("{path} unchanged", path = file.path.display()); continue; } @@ -142,7 +160,11 @@ pub(super) fn edit_files( .map_err(|e| { io::Error::new( e.kind(), - format!("Failed to write {}: {e}", file.path.display()), + xlat!( + "failed to write {path}: {error}", + path = file.path.display(), + error = e + ), ) })?; @@ -158,8 +180,9 @@ impl Drop for TempDirDropGuard { fn drop(&mut self) { if let Err(e) = std::fs::remove_dir_all(&self.0) { user_error!( - "failed to remove temporary directory {}: {e}", - self.0.display(), + "failed to remove temporary directory {path}: {error}", + path = self.0.display(), + error = e ); }; } @@ -169,7 +192,7 @@ fn handle_child(editor: &Path, file: Vec>) -> ! { match handle_child_inner(editor, file) { Ok(()) => process::exit(0), Err(err) => { - user_error!("{err}"); + user_error!("{error}", error = err); process::exit(1); } } @@ -186,16 +209,18 @@ fn handle_child_inner(editor: &Path, mut files: Vec>) -> Resul } let tempdir = TempDirDropGuard( - create_temporary_dir().map_err(|e| format!("failed to create temporary directory: {e}"))?, + create_temporary_dir() + .map_err(|e| xlat!("failed to create temporary directory: {error}", error = e))?, ); for (i, file) in files.iter_mut().enumerate() { // Create temp file let dir = tempdir.0.join(format!("{i}")); std::fs::create_dir(&dir).map_err(|e| { - format!( - "failed to create temporary directory {}: {e}", - dir.display(), + xlat!( + "failed to create temporary directory {path}: {error}", + path = dir.display(), + error = e ) })?; let tempfile_path = dir.join(file.path.file_name().expect("file must have filename")); @@ -206,17 +231,19 @@ fn handle_child_inner(editor: &Path, mut files: Vec>) -> Resul .mode(0o600) .open(&tempfile_path) .map_err(|e| { - format!( - "failed to create temporary file {}: {e}", - tempfile_path.display(), + xlat!( + "failed to create temporary file {path}: {error}", + path = tempfile_path.display(), + error = e ) })?; // Write to temp file tempfile.write_all(&file.old_data).map_err(|e| { - format!( - "failed to write to temporary file {}: {e}", - tempfile_path.display(), + xlat!( + "failed to write to temporary file {path}: {error}", + path = tempfile_path.display(), + error = e ) })?; drop(tempfile); @@ -231,7 +258,13 @@ fn handle_child_inner(editor: &Path, mut files: Vec>) -> Resul .map(|file| file.tempfile_path.as_ref().expect("filled in above")), ) .status() - .map_err(|e| format!("failed to run editor {}: {e}", editor.display()))?; + .map_err(|e| { + xlat!( + "failed to run editor {path}: {error}", + path = editor.display(), + error = e + ) + })?; if !status.success() { drop(tempdir); @@ -247,37 +280,42 @@ fn handle_child_inner(editor: &Path, mut files: Vec>) -> Resul // Read from temp file let new_data = std::fs::read(tempfile_path).map_err(|e| { - format!( - "failed to read from temporary file {}: {e}", - tempfile_path.display(), + xlat!( + "failed to read from temporary file {path}: {error}", + path = tempfile_path.display(), + error = e ) })?; // FIXME preserve temporary file if the original couldn't be written to std::fs::remove_file(tempfile_path).map_err(|e| { - format!( - "failed to remove temporary file {}: {e}", - tempfile_path.display(), + xlat!( + "failed to remove temporary file {path}: {error}", + path = tempfile_path.display(), + error = e ) })?; // If the file has been changed to be empty, ask the user what to do. if new_data.is_empty() && new_data != file.old_data { + // TRANSLATORS: the initial letters of 'yes' and 'no' responses, in that order + let answers = xlat!("yn").as_bytes().get(..2).unwrap_or(b"yn"); + match crate::visudo::ask_response( - format!( - "sudoedit: truncate {} to zero? (y/n) [n] ", - file.path.display() + xlat!( + "sudoedit: truncate {path} to zero? (y/n) [n] ", + path = file.path.display() ) .as_bytes(), - b"yn", + answers, ) { - Ok(b'y') => {} + Ok(val) if val == answers[0] => {} _ => { - user_info!("not overwriting {}", file.path.display()); + user_info!("not overwriting {path}", path = file.path.display()); // Parent ignores write when new data matches old data write_stream(&mut file.new_data_tx, &file.old_data) - .map_err(|e| format!("failed to write data to parent: {e}"))?; + .map_err(|e| xlat!("failed to write data to parent: {error}", error = e))?; continue; } @@ -286,7 +324,7 @@ fn handle_child_inner(editor: &Path, mut files: Vec>) -> Resul // Write to socket write_stream(&mut file.new_data_tx, &new_data) - .map_err(|e| format!("failed to write data to parent: {e}"))?; + .map_err(|e| xlat!("failed to write data to parent: {error}", error = e))?; } process::exit(0); diff --git a/src/sudo/mod.rs b/src/sudo/mod.rs index df1749c9a..313e63788 100644 --- a/src/sudo/mod.rs +++ b/src/sudo/mod.rs @@ -73,10 +73,10 @@ fn sudo_process() -> Result<(), Error> { let usage_msg: &str; let long_help: fn() -> String; if cli::is_sudoedit(std::env::args().next()) { - usage_msg = cli::help_edit::USAGE_MSG; + usage_msg = cli::help_edit::usage_msg(); long_help = cli::help_edit::long_help_message; } else { - usage_msg = cli::help::USAGE_MSG; + usage_msg = cli::help::usage_msg(); long_help = cli::help::long_help_message; } diff --git a/src/sudo/pipeline.rs b/src/sudo/pipeline.rs index 60267f7cb..8eb30a737 100644 --- a/src/sudo/pipeline.rs +++ b/src/sudo/pipeline.rs @@ -28,8 +28,8 @@ fn read_sudoers() -> Result { let (sudoers, syntax_errors) = Sudoers::open(sudoers_path).map_err(|e| { // Provide a more helpful error message when the sudoers file is missing if e.kind() == std::io::ErrorKind::NotFound { - Error::Configuration(format!( - "sudoers file not found: {}\n\ + Error::Configuration(xlat!( + "sudoers file not found: {path}\n\ \n\ The sudoers file is required for sudo-rs to function. Please ensure:\n\ - The file exists at the expected location\n\ @@ -37,10 +37,10 @@ fn read_sudoers() -> Result { - If setting up sudo-rs for the first time, create a sudoers file with appropriate permissions\n\ \n\ For more information, see the sudo-rs documentation.", - sudoers_path.display() + path = sudoers_path.display() )) } else { - Error::Configuration(format!("invalid configuration: {e}")) + Error::Configuration(xlat!("invalid configuration: {error}", error = e)) } })?; diff --git a/src/sudo/pipeline/edit.rs b/src/sudo/pipeline/edit.rs index e247fbde6..5ac597b2f 100644 --- a/src/sudo/pipeline/edit.rs +++ b/src/sudo/pipeline/edit.rs @@ -34,12 +34,17 @@ pub fn run_edit(edit_opts: SudoEditOptions) -> Result<(), Error> { Ok(file) => opened_files.push((path, file)), // ErrorKind::FilesystemLoop was only stabilized in 1.83 Err(error) if error.raw_os_error() == Some(libc::ELOOP) => { - user_error!("{arg}: editing symbolic links is not permitted") + user_error!( + "{path}: editing symbolic links is not permitted", + path = arg + ) + } + Err(error) => { + user_error!("error opening {path}: {error}", path = arg, error = error) } - Err(error) => user_error!("error opening {arg}: {error}"), } } else { - user_error!("invalid path: {arg}"); + user_error!("invalid path: {path}", path = arg); } } diff --git a/src/sudo/pipeline/list.rs b/src/sudo/pipeline/list.rs index 8f7a44043..8d1dc05e4 100644 --- a/src/sudo/pipeline/list.rs +++ b/src/sudo/pipeline/list.rs @@ -40,9 +40,12 @@ pub(in crate::sudo) fn run_list(cmd_opts: SudoListOptions) -> Result<(), Error> if matching_entries.peek().is_some() { println_ignore_io_error!( - "User {} may run the following commands on {}:", - inspected_user.name, - context.hostname + "{}", + xlat!( + "User {user} may run the following commands on {hostname}:", + user = inspected_user.name, + hostname = context.hostname + ) ); for entry in matching_entries { @@ -55,9 +58,12 @@ pub(in crate::sudo) fn run_list(cmd_opts: SudoListOptions) -> Result<(), Error> } } else { println_ignore_io_error!( - "User {} is not allowed to run sudo on {}.", - inspected_user.name, - context.hostname + "{}", + xlat!( + "User {user} is not allowed to run sudo on {hostname}.", + user = inspected_user.name, + hostname = context.hostname, + ), ); } } diff --git a/src/sudoers/ast.rs b/src/sudoers/ast.rs index d27489647..3663731d4 100644 --- a/src/sudoers/ast.rs +++ b/src/sudoers/ast.rs @@ -397,8 +397,8 @@ impl Parse for MetaOrTag { // this is less fatal "LOG_INPUT" | "NOLOG_INPUT" | "LOG_OUTPUT" | "NOLOG_OUTPUT" | "MAIL" | "NOMAIL" | "FOLLOW" => { - crate::log::user_warn!( - "{} tags in the sudoers policy are ignored by sudo-rs", + eprintln_ignore_io_error!( + "sudo-rs: {} tags in the sudoers policy are ignored", keyword.as_str() ); switch(|_| {})? diff --git a/src/sudoers/entry/verbose.rs b/src/sudoers/entry/verbose.rs index 905de4b78..c9f06f111 100644 --- a/src/sudoers/entry/verbose.rs +++ b/src/sudoers/entry/verbose.rs @@ -30,7 +30,7 @@ impl fmt::Display for Verbose<'_> { write_entry_header(run_as, f)?; write_tag(f, tag)?; - f.write_str("\n Commands:")?; + write!(f, "\n {}", xlat!("Commands:"))?; } last_tag = Some(tag); @@ -43,14 +43,15 @@ impl fmt::Display for Verbose<'_> { } fn write_entry_header(run_as: &RunAs, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("\nSudoers entry:")?; + write!(f, "\n{}", xlat!("Sudoers entry:"))?; write_users(run_as, f)?; write_groups(run_as, f) } fn write_users(run_as: &RunAs, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("\n RunAsUsers: ")?; + // TRANSLATORS: This is sudo-specific jargon. + write!(f, "\n {}: ", xlat!("RunAsUsers"))?; super::write_users(run_as, f) } @@ -59,13 +60,14 @@ fn write_groups(run_as: &RunAs, f: &mut fmt::Formatter<'_>) -> fmt::Result { return Ok(()); } - f.write_str("\n RunAsGroups: ")?; + // TRANSLATORS: This is sudo-specific jargon. + write!(f, "\n {}: ", xlat!("RunAsGroups"))?; super::write_groups(run_as, f) } fn write_tag(f: &mut fmt::Formatter, tag: &Tag) -> fmt::Result { if tag.authenticate != Authenticate::None { - f.write_str("\n Options: ")?; + write!(f, "\n {}: ", xlat!("Options"))?; if tag.authenticate != Authenticate::Passwd { f.write_str("!")?; } @@ -73,7 +75,8 @@ fn write_tag(f: &mut fmt::Formatter, tag: &Tag) -> fmt::Result { } if let Some(cwd) = &tag.cwd { - f.write_str("\n Cwd: ")?; + // TRANSLATORS: This is sudo-specific jargon. + write!(f, "\n {}: ", xlat!("Cwd"))?; match cwd { ChDir::Path(path) => write!(f, "{}", path.display())?, ChDir::Any => f.write_str("*")?, diff --git a/src/system/audit.rs b/src/system/audit.rs index 212b844cd..6c884847a 100644 --- a/src/system/audit.rs +++ b/src/system/audit.rs @@ -129,16 +129,19 @@ fn checks(path: &Path, meta: Metadata) -> io::Result<()> { let path_mode = meta.permissions().mode(); if meta.uid() != 0 { - Err(error(format!("{} must be owned by root", path.display()))) + Err(error(xlat!( + "{path} must be owned by root", + path = path.display() + ))) } else if meta.gid() != 0 && (path_mode & mode(Category::Group, Op::Write) != 0) { - Err(error(format!( - "{} cannot be group-writable", - path.display() + Err(error(xlat!( + "{path} cannot be group-writable", + path = path.display() ))) } else if path_mode & mode(Category::World, Op::Write) != 0 { - Err(error(format!( - "{} cannot be world-writable", - path.display() + Err(error(xlat!( + "{path} cannot be world-writable", + path = path.display() ))) } else { Ok(()) @@ -177,9 +180,9 @@ fn secure_open_impl( checks(parent_dir, parent_meta)?; } } else { - return Err(error(format!( - "{} has no valid parent directory", - path.display() + return Err(error(xlat!( + "{path} has no valid parent directory", + path = path.display() ))); } } @@ -244,14 +247,17 @@ fn traversed_secure_open(path: impl AsRef, forbidden_user: &User) -> io::R let path = path.as_ref(); let Some(file_name) = path.file_name() else { - return Err(io::Error::new(ErrorKind::InvalidInput, "invalid path")); + return Err(io::Error::new( + ErrorKind::InvalidInput, + xlat!("invalid path"), + )); }; let mut components = path.parent().unwrap_or(Path::new("")).components(); if components.next() != Some(Component::RootDir) { return Err(io::Error::new( ErrorKind::InvalidInput, - "path must be absolute", + xlat!("path must be absolute"), )); } @@ -267,7 +273,7 @@ fn traversed_secure_open(path: impl AsRef, forbidden_user: &User) -> io::R { Err(io::Error::new( ErrorKind::PermissionDenied, - "cannot open a file in a path writable by the user", + xlat!("cannot open a file in a path writable by the user"), )) } else { Ok(()) @@ -285,7 +291,7 @@ fn traversed_secure_open(path: impl AsRef, forbidden_user: &User) -> io::R _ => { return Err(io::Error::new( ErrorKind::InvalidInput, - "error in provided path", + xlat!("error in provided path"), )) } }; diff --git a/src/visudo/mod.rs b/src/visudo/mod.rs index 9027b68c2..4e350c6bf 100644 --- a/src/visudo/mod.rs +++ b/src/visudo/mod.rs @@ -61,7 +61,7 @@ pub fn main() { std::process::exit(0); } VisudoAction::Version => { - println_ignore_io_error!("visudo version {VERSION}"); + println_ignore_io_error!("visudo-rs {VERSION}"); std::process::exit(0); } VisudoAction::Check => check, diff --git a/test-framework/sudo-compliance-tests/src/sudo/pass_auth/askpass.rs b/test-framework/sudo-compliance-tests/src/sudo/pass_auth/askpass.rs index 441813a0d..851e28faf 100644 --- a/test-framework/sudo-compliance-tests/src/sudo/pass_auth/askpass.rs +++ b/test-framework/sudo-compliance-tests/src/sudo/pass_auth/askpass.rs @@ -174,7 +174,7 @@ fn sudo_askpass_not_absolute_path() { if sudo_test::is_original_sudo() { assert_contains!(stderr, "unable to run askpass: No such file or directory"); } else { - assert_contains!(stderr, "Askpass program `askpass` is not an absolute path"); + assert_contains!(stderr, "Askpass program 'askpass' is not an absolute path"); } } diff --git a/test-framework/sudo-compliance-tests/src/visudo/flag_version.rs b/test-framework/sudo-compliance-tests/src/visudo/flag_version.rs index 885b4698e..4febb3e36 100644 --- a/test-framework/sudo-compliance-tests/src/visudo/flag_version.rs +++ b/test-framework/sudo-compliance-tests/src/visudo/flag_version.rs @@ -20,9 +20,10 @@ fn prints_to_stdout() { assert_eq!(short, long); - assert_contains!(short, "visudo version"); - if sudo_test::is_original_sudo() { + assert_contains!(short, "visudo version"); assert_contains!(short, "visudo grammar version 50"); + } else { + assert_contains!(short, "visudo-rs"); } } diff --git a/util/make-pot.sh b/util/make-pot.sh index 0d21220c2..3acb0426c 100755 --- a/util/make-pot.sh +++ b/util/make-pot.sh @@ -12,7 +12,7 @@ fi potfile="$WORK_ROOT/po/sudo-rs.pot" test -e "$potfile" && EXISTING="--join-existing --omit-header" -find "$WORK_ROOT" -name "*.rs" -not -name "gettext.rs" | xargs xgettext \ +cd "$WORK_ROOT" && find src -name "*.rs" -not -path "*/gettext/*" | xargs xgettext \ --package-name="sudo-rs" \ --package-version="$SUDO_RS_VERSION" \ --msgid-bugs-address="https://github.com/trifectatechfoundation/sudo-rs/issues" \ @@ -20,6 +20,9 @@ find "$WORK_ROOT" -name "*.rs" -not -name "gettext.rs" | xargs xgettext \ --from-code="UTF-8" \ --keyword='xlat!' \ --keyword='xlat_write!:2' \ + --keyword='user_error!:1' \ + --keyword='user_info!:1' \ + --keyword='user_warn!:1' \ --add-comments='TRANSLATORS:' \ ${EXISTING:-} \ -o "$potfile"