From 603f525e79f61c093b6aa0715f611af4849ab7ac Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 1 Nov 2026 19:52:20 +0100 Subject: [PATCH 1/9] tsort: use thiserror for LoopNode --- src/uu/tsort/src/tsort.rs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/uu/tsort/src/tsort.rs b/src/uu/tsort/src/tsort.rs index 1dfdc2c2dac..39078d7f494 100644 --- a/src/uu/tsort/src/tsort.rs +++ b/src/uu/tsort/src/tsort.rs @@ -15,7 +15,6 @@ use rustc_hash::FxHashMap; use std::collections::VecDeque; use std::collections::hash_map::Entry; use std::ffi::OsString; -use std::fmt; use std::fs::File; use std::fs::OpenOptions; use std::io::{self, BufRead, BufReader, BufWriter, Write}; @@ -136,17 +135,10 @@ pub fn uu_app() -> Command { // // Diagnostics go through Display, so invalid UTF-8 bytes are represented // lossily here. -#[derive(Debug)] +#[derive(Debug, thiserror::Error)] +#[error("{}", String::from_utf8_lossy(.0))] struct LoopNode<'a>(&'a [u8]); -impl fmt::Display for LoopNode<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", String::from_utf8_lossy(self.0)) - } -} - -impl std::error::Error for LoopNode<'_> {} - impl UError for Error {} impl UError for LoopNode<'_> {} From 20188c47f3afbb37b248f3f8a5136e698c2cb91a Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 1 Nov 2026 19:52:33 +0100 Subject: [PATCH 2/9] split: use thiserror for Overflow --- src/uu/split/src/number.rs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/uu/split/src/number.rs b/src/uu/split/src/number.rs index 35df4a42b1f..03aa326468c 100644 --- a/src/uu/split/src/number.rs +++ b/src/uu/split/src/number.rs @@ -17,22 +17,14 @@ //! [radix]: https://en.wikipedia.org/wiki/Radix //! [positional notation]: https://en.wikipedia.org/wiki/Positional_notation -use std::error::Error; use std::fmt::{self, Display, Formatter, Write}; use uucore::translate; /// An overflow due to incrementing a number beyond its representable limit. -#[derive(Debug)] +#[derive(Debug, thiserror::Error)] +#[error("{}", translate!("split-error-overflow"))] pub struct Overflow; -impl Display for Overflow { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "{}", translate!("split-error-overflow")) - } -} - -impl Error for Overflow {} - /// A number in arbitrary radix expressed in a positional notation. /// /// Use the [`Number`] enum to represent an arbitrary number in an From 613dcabaf7827e57209253204df9725030d7dedf Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 1 Nov 2026 19:53:19 +0100 Subject: [PATCH 3/9] mv: use thiserror and translate! for HardlinkError --- src/uu/mv/locales/en-US.ftl | 6 ++++ src/uu/mv/locales/fr-FR.ftl | 6 ++++ src/uu/mv/src/hardlink.rs | 69 +++++++++---------------------------- 3 files changed, 29 insertions(+), 52 deletions(-) diff --git a/src/uu/mv/locales/en-US.ftl b/src/uu/mv/locales/en-US.ftl index 40d590dd9d3..130bc466b45 100644 --- a/src/uu/mv/locales/en-US.ftl +++ b/src/uu/mv/locales/en-US.ftl @@ -72,3 +72,9 @@ mv-prompt-overwrite-mode = replace {$target}, overriding mode {$mode_info}? # Progress messages mv-progress-moving = moving mv-error-dest-appeared = destination { $path } was replaced while moving; refusing to follow it + +# Hardlink preservation errors +mv-error-hardlink-io = I/O error during hardlink operation: { $error } +mv-error-hardlink-scan = failed to scan files for hardlinks: { $error } +mv-error-hardlink-preserve = failed to preserve hardlink: { $source } -> { $target } +mv-error-hardlink-metadata = metadata access error for { $path }: { $error } diff --git a/src/uu/mv/locales/fr-FR.ftl b/src/uu/mv/locales/fr-FR.ftl index abf3f552ac9..1a377e926be 100644 --- a/src/uu/mv/locales/fr-FR.ftl +++ b/src/uu/mv/locales/fr-FR.ftl @@ -69,3 +69,9 @@ mv-prompt-overwrite = écraser {$target} ? # Messages de progression mv-progress-moving = déplacement mv-error-dest-appeared = la destination { $path } a été remplacée pendant le déplacement ; refus de la suivre + +# Erreurs de préservation des liens physiques +mv-error-hardlink-io = erreur d'E/S lors de l'opération sur le lien physique : { $error } +mv-error-hardlink-scan = échec de l'analyse des fichiers pour les liens physiques : { $error } +mv-error-hardlink-preserve = échec de la préservation du lien physique : { $source } -> { $target } +mv-error-hardlink-metadata = erreur d'accès aux métadonnées de { $path } : { $error } diff --git a/src/uu/mv/src/hardlink.rs b/src/uu/mv/src/hardlink.rs index bc3a79753ab..4f88b102279 100644 --- a/src/uu/mv/src/hardlink.rs +++ b/src/uu/mv/src/hardlink.rs @@ -15,6 +15,7 @@ use std::io::{self, Write as _}; use std::path::{Path, PathBuf}; use uucore::display::Quotable; +use uucore::translate; /// Tracks hardlinks during cross-partition moves to preserve them #[derive(Debug, Default)] @@ -42,50 +43,23 @@ pub struct HardlinkOptions { } /// Errors that can occur during hardlink operations -#[derive(Debug)] +#[derive(Debug, thiserror::Error)] pub enum HardlinkError { - Io(io::Error), + #[error("{}", translate!("mv-error-hardlink-io", "error" => .0))] + Io(#[from] io::Error), + #[error("{}", translate!("mv-error-hardlink-scan", "error" => .0))] Scan(String), - Preservation { source: PathBuf, target: PathBuf }, - Metadata { path: PathBuf, error: io::Error }, -} - -impl std::fmt::Display for HardlinkError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Io(e) => write!(f, "I/O error during hardlink operation: {e}"), - Self::Scan(msg) => { - write!(f, "Failed to scan files for hardlinks: {msg}") - } - Self::Preservation { source, target } => { - write!( - f, - "Failed to preserve hardlink: {} -> {}", - source.quote(), - target.quote() - ) - } - Self::Metadata { path, error } => { - write!(f, "Metadata access error for {}: {error}", path.quote()) - } - } - } -} - -impl std::error::Error for HardlinkError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Self::Io(e) => Some(e), - Self::Metadata { error, .. } => Some(error), - _ => None, - } - } -} - -impl From for HardlinkError { - fn from(error: io::Error) -> Self { - Self::Io(error) - } + #[error("{}", translate!("mv-error-hardlink-preserve", "source" => .source_path.quote(), "target" => .target_path.quote()))] + Preservation { + source_path: PathBuf, + target_path: PathBuf, + }, + #[error("{}", translate!("mv-error-hardlink-metadata", "path" => .path.quote(), "error" => .error))] + Metadata { + path: PathBuf, + #[source] + error: io::Error, + }, } impl From for io::Error { @@ -93,16 +67,7 @@ impl From for io::Error { match error { HardlinkError::Io(e) => e, HardlinkError::Scan(msg) => Self::other(msg), - HardlinkError::Preservation { source, target } => Self::other(format!( - "Failed to preserve hardlink: {} -> {}", - source.quote(), - target.quote() - )), - - HardlinkError::Metadata { path, error } => Self::other(format!( - "Metadata access error for {}: {error}", - path.quote(), - )), + other => Self::other(other.to_string()), } } } From 7e75ea722523baae87a1aae867ba381dda89273c Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 1 Nov 2026 19:54:55 +0100 Subject: [PATCH 4/9] uucore/backup_control: use thiserror and translate! for BackupError --- src/uucore/locales/en-US.ftl | 5 ++ src/uucore/locales/fr-FR.ftl | 5 ++ src/uucore/src/lib/features/backup_control.rs | 53 ++++++++++--------- 3 files changed, 39 insertions(+), 24 deletions(-) diff --git a/src/uucore/locales/en-US.ftl b/src/uucore/locales/en-US.ftl index 6afc1ab7647..66b21484604 100644 --- a/src/uucore/locales/en-US.ftl +++ b/src/uucore/locales/en-US.ftl @@ -104,3 +104,8 @@ perms-cannot-access-replaced = cannot access { $file }: replaced while it was be # Encoding/decoding error messages (base32, base64, basenc) encoding-error-invalid-input = invalid input encoding-error-invalid-input-z85-length = invalid input (length must be multiple of 4 characters) + +# Backup control messages (uucore::backup_control) +backup-error-invalid-argument = invalid argument { $arg } for '{ $origin }' +backup-error-ambiguous-argument = ambiguous argument { $arg } for '{ $origin }' +backup-valid-arguments = Valid arguments are: diff --git a/src/uucore/locales/fr-FR.ftl b/src/uucore/locales/fr-FR.ftl index 80303701383..803fe7b8757 100644 --- a/src/uucore/locales/fr-FR.ftl +++ b/src/uucore/locales/fr-FR.ftl @@ -93,3 +93,8 @@ mode-diag-help-syntax = un mode est soit octal, comme 644, soit des clauses comm # Messages d'erreur d'encodage/décodage (base32, base64, basenc) encoding-error-invalid-input = entrée invalide encoding-error-invalid-input-z85-length = entrée invalide (la longueur doit être un multiple de 4 caractères) + +# Messages de contrôle des sauvegardes (uucore::backup_control) +backup-error-invalid-argument = argument { $arg } invalide pour '{ $origin }' +backup-error-ambiguous-argument = argument { $arg } ambigu pour '{ $origin }' +backup-valid-arguments = Les arguments valides sont : diff --git a/src/uucore/src/lib/features/backup_control.rs b/src/uucore/src/lib/features/backup_control.rs index 8208ff34a61..16476dac969 100644 --- a/src/uucore/src/lib/features/backup_control.rs +++ b/src/uucore/src/lib/features/backup_control.rs @@ -85,10 +85,10 @@ use crate::display::Quotable; use crate::error::{UError, UResult}; use crate::fs::FileInformation; +use crate::translate; use clap::ArgMatches; -use std::error::Error; use std::ffi::{OsStr, OsString}; -use std::fmt::{Debug, Display}; +use std::fmt::Debug; use std::path::{Path, PathBuf}; use std::{env, fs}; @@ -106,8 +106,8 @@ the VERSION_CONTROL environment variable. Here are the values: existing, nil numbered if numbered backups exist, simple otherwise simple, never always make simple backups"; -static VALID_ARGS_HELP: &str = "Valid arguments are: - - 'none', 'off' +/// The values `--backup` accepts; they are option values, so they are not translated. +static VALID_ARGS_LIST: &str = " - 'none', 'off' - 'simple', 'never' - 'existing', 'nil' - 'numbered', 't'"; @@ -135,15 +135,25 @@ pub enum BackupMode { /// /// Errors are currently raised by [`determine_backup_mode`] only. All errors /// are implemented as [`UError`] for uniform handling across utilities. -#[derive(Debug, Eq, PartialEq)] +#[derive(Debug, Eq, PartialEq, thiserror::Error)] pub enum BackupError { /// An invalid argument (e.g. 'foo') was given as backup type. First /// parameter is the argument, second is the arguments origin (CLI or /// ENV-var) + #[error( + "{}\n{}\n{VALID_ARGS_LIST}", + translate!("backup-error-invalid-argument", "arg" => .0.quote(), "origin" => .1), + translate!("backup-valid-arguments") + )] InvalidArgument(String, String), /// An ambiguous argument (e.g. 'n') was given as backup type. First /// parameter is the argument, second is the arguments origin (CLI or /// ENV-var) + #[error( + "{}\n{}\n{VALID_ARGS_LIST}", + translate!("backup-error-ambiguous-argument", "arg" => .0.quote(), "origin" => .1), + translate!("backup-valid-arguments") + )] AmbiguousArgument(String, String), } @@ -153,25 +163,6 @@ impl UError for BackupError { } } -impl Error for BackupError {} - -impl Display for BackupError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::InvalidArgument(arg, origin) => write!( - f, - "invalid argument {} for '{origin}'\n{VALID_ARGS_HELP}", - arg.quote(), - ), - Self::AmbiguousArgument(arg, origin) => write!( - f, - "ambiguous argument {} for '{origin}'\n{VALID_ARGS_HELP}", - arg.quote(), - ), - } - } -} - /// Arguments for backup-related functionality. /// /// Rather than implementing the `clap`-Arguments for every utility, it is @@ -538,6 +529,16 @@ mod tests { use super::*; use clap::Command; + /// The error messages go through `translate!`, which needs the English + /// strings loaded to give back more than the message key. + fn load_english_messages() { + // SAFETY: test-only; like the uptime tests, it always sets the same value. + unsafe { + env::set_var("LANG", "en_US.UTF-8"); + } + let _ = crate::locale::setup_localization("mv"); + } + fn make_app() -> Command { Command::new("command") .arg(arguments::backup()) @@ -596,6 +597,7 @@ mod tests { // --backup errors on invalid argument #[test] fn test_backup_mode_long_with_args_invalid() { + load_english_messages(); let matches = make_app().get_matches_from(vec!["command", "--backup=foobar"]); let result = determine_backup_mode(None, &matches); assert!(result.is_err()); @@ -606,6 +608,7 @@ mod tests { // --backup errors on ambiguous argument #[test] fn test_backup_mode_long_with_args_ambiguous() { + load_english_messages(); let matches = make_app().get_matches_from(vec!["command", "--backup=n"]); let result = determine_backup_mode(None, &matches); assert!(result.is_err()); @@ -640,6 +643,7 @@ mod tests { // --backup errors on invalid VERSION_CONTROL env var #[test] fn test_backup_mode_long_with_env_var_invalid() { + load_english_messages(); let matches = make_app().get_matches_from(vec!["command", "--backup"]); let result = determine_backup_mode(Some("foobar".into()), &matches); assert!(result.is_err()); @@ -650,6 +654,7 @@ mod tests { // --backup errors on ambiguous VERSION_CONTROL env var #[test] fn test_backup_mode_long_with_env_var_ambiguous() { + load_english_messages(); let matches = make_app().get_matches_from(vec!["command", "--backup"]); let result = determine_backup_mode(Some("n".into()), &matches); assert!(result.is_err()); From 230481fb0cae330e2946d0f9c94f618791701fea Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 1 Nov 2026 19:55:25 +0100 Subject: [PATCH 5/9] uucore/mode: use thiserror for ModeError --- src/uucore/src/lib/features/mode.rs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/uucore/src/lib/features/mode.rs b/src/uucore/src/lib/features/mode.rs index acb2ec370c7..922c4b1c321 100644 --- a/src/uucore/src/lib/features/mode.rs +++ b/src/uucore/src/lib/features/mode.rs @@ -8,7 +8,6 @@ //! Set of functions to parse modes use std::ffi::OsString; -use std::fmt::{self, Display}; use std::ops::Range; #[cfg(windows)] @@ -22,7 +21,8 @@ use crate::translate; /// `span` is a byte range inside the mode string that was handed to the parser, /// so that a caller can point a caret at the one clause — often the one /// character — that broke it. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +#[error("{message}")] pub struct ModeError { pub message: String, pub span: Range, @@ -195,14 +195,6 @@ impl ModeError { } } -impl Display for ModeError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(&self.message) - } -} - -impl std::error::Error for ModeError {} - pub fn parse_numeric(fperm: u32, mode: &str, considering_dir: bool) -> Result { let original = mode; let (op, pos) = parse_op(mode).map_or_else(|_| (None, 0), |(op, pos)| (Some(op), pos)); From 79310d2e373003219cda4bb7616bd878855cf61d Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 1 Nov 2026 19:55:58 +0100 Subject: [PATCH 6/9] uucore/ranges: use thiserror for RangeError --- src/uucore/src/lib/features/ranges.rs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/uucore/src/lib/features/ranges.rs b/src/uucore/src/lib/features/ranges.rs index 5688e93d8a2..733a1c9167a 100644 --- a/src/uucore/src/lib/features/ranges.rs +++ b/src/uucore/src/lib/features/ranges.rs @@ -65,7 +65,8 @@ pub enum RangeErrorKind { /// /// The message is built here so that it reads exactly as it always has; `span` /// and `kind` are what a caret needs on top of it. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +#[error("{message}")] pub struct RangeError { pub message: String, /// Byte range inside the whole list, not inside the one item at fault. @@ -73,14 +74,6 @@ pub struct RangeError { pub kind: RangeErrorKind, } -impl std::fmt::Display for RangeError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&self.message) - } -} - -impl std::error::Error for RangeError {} - impl RangeError { /// Render this error against `args`, with a caret under the one range in /// `list` that is at fault. From 12270524c99faef4080710998bb0eb9ab9d74fdf Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 1 Nov 2026 20:29:29 +0100 Subject: [PATCH 7/9] uucore: use thiserror and translate! for NonUtf8OsStrError --- src/uucore/locales/en-US.ftl | 3 +++ src/uucore/locales/fr-FR.ftl | 3 +++ src/uucore/src/lib/lib.rs | 17 +++++------------ 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/uucore/locales/en-US.ftl b/src/uucore/locales/en-US.ftl index 66b21484604..9d786e42b16 100644 --- a/src/uucore/locales/en-US.ftl +++ b/src/uucore/locales/en-US.ftl @@ -109,3 +109,6 @@ encoding-error-invalid-input-z85-length = invalid input (length must be multiple backup-error-invalid-argument = invalid argument { $arg } for '{ $origin }' backup-error-ambiguous-argument = ambiguous argument { $arg } for '{ $origin }' backup-valid-arguments = Valid arguments are: + +# OsStr conversion errors +error-non-utf8-os-str = invalid UTF-8 input { $input } encountered when converting to bytes on a platform that doesn't expose byte arguments diff --git a/src/uucore/locales/fr-FR.ftl b/src/uucore/locales/fr-FR.ftl index 803fe7b8757..c3b0e82d709 100644 --- a/src/uucore/locales/fr-FR.ftl +++ b/src/uucore/locales/fr-FR.ftl @@ -98,3 +98,6 @@ encoding-error-invalid-input-z85-length = entrée invalide (la longueur doit êt backup-error-invalid-argument = argument { $arg } invalide pour '{ $origin }' backup-error-ambiguous-argument = argument { $arg } ambigu pour '{ $origin }' backup-valid-arguments = Les arguments valides sont : + +# Erreurs de conversion OsStr +error-non-utf8-os-str = entrée UTF-8 invalide { $input } rencontrée lors de la conversion en octets sur une plateforme qui n'expose pas les arguments en octets diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index a27b014fec5..dbb055df37d 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -465,22 +465,15 @@ pub fn read_yes() -> bool { std::io::stdin().read_line(&mut s).is_ok() && matches!(s.chars().next(), Some('y' | 'Y')) } -#[derive(Debug)] +#[derive(Debug, thiserror::Error)] +#[error("{}", translate!( + "error-non-utf8-os-str", + "input" => os_display::Quotable::quote(.input_lossy_string.as_str()) +))] pub struct NonUtf8OsStrError { input_lossy_string: String, } -impl std::fmt::Display for NonUtf8OsStrError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - use os_display::Quotable; - let quoted = self.input_lossy_string.quote(); - f.write_fmt(format_args!( - "invalid UTF-8 input {quoted} encountered when converting to bytes on a platform that doesn't expose byte arguments", - )) - } -} - -impl std::error::Error for NonUtf8OsStrError {} impl error::UError for NonUtf8OsStrError {} /// Converts an `OsStr` to a UTF-8 `&[u8]`. From 7773b5d78c3007fdf48136eff34456d4476069b0 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 1 Nov 2026 20:00:31 +0100 Subject: [PATCH 8/9] uucore/parse_size: use thiserror for ParseSizeError --- .../src/lib/features/parser/parse_size.rs | 31 +++---------------- 1 file changed, 5 insertions(+), 26 deletions(-) diff --git a/src/uucore/src/lib/features/parser/parse_size.rs b/src/uucore/src/lib/features/parser/parse_size.rs index 43d3bc8f70d..39e52ad6d99 100644 --- a/src/uucore/src/lib/features/parser/parse_size.rs +++ b/src/uucore/src/lib/features/parser/parse_size.rs @@ -69,8 +69,6 @@ pub const IEC_BASES: [u128; 11] = [ 1_267_650_600_228_229_401_496_703_205_376, ]; -use std::error::Error; -use std::fmt; use std::num::{IntErrorKind, ParseIntError}; use std::ops::Range; @@ -534,44 +532,25 @@ pub fn parse_size_u128_max(size: &str) -> Result { } /// Error type for [`parse_size`] -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, thiserror::Error)] pub enum ParseSizeError { /// Suffix + #[error("{0}")] InvalidSuffix(String), /// Syntax + #[error("{0}")] ParseFailure(String), /// Overflow + #[error("{0}")] SizeTooBig(String), /// Could not determine total physical memory size. + #[error("{0}")] PhysicalMem(String), } -impl Error for ParseSizeError { - fn description(&self) -> &str { - match *self { - Self::InvalidSuffix(ref s) - | Self::ParseFailure(ref s) - | Self::SizeTooBig(ref s) - | Self::PhysicalMem(ref s) => s, - } - } -} - -impl fmt::Display for ParseSizeError { - fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { - let s = match self { - Self::InvalidSuffix(s) - | Self::ParseFailure(s) - | Self::SizeTooBig(s) - | Self::PhysicalMem(s) => s, - }; - write!(f, "{s}") - } -} - // FIXME: It's more idiomatic to move the formatting into the Display impl, // but there's a lot of downstream code that constructs these errors manually // that would be affected From 0b48f97f9c01d5245a0e0b2916be87bc513e9470 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 1 Nov 2026 20:02:23 +0100 Subject: [PATCH 9/9] uucore/format: use thiserror for FormatError --- src/uucore/src/lib/features/format/mod.rs | 57 +++++++---------------- 1 file changed, 16 insertions(+), 41 deletions(-) diff --git a/src/uucore/src/lib/features/format/mod.rs b/src/uucore/src/lib/features/format/mod.rs index eee8bb1581a..4be0edc6184 100644 --- a/src/uucore/src/lib/features/format/mod.rs +++ b/src/uucore/src/lib/features/format/mod.rs @@ -47,34 +47,47 @@ use self::num_format::Formatter; use crate::error::{UError, strip_errno}; use crate::{NonUtf8OsStrError, translate, translate_text}; pub use spec::Spec; -use std::error::Error; -use std::fmt::Display; use std::io::{Write, stdout}; use std::marker::PhantomData; use std::ops::{ControlFlow, Range}; use os_display::Quotable; -#[derive(Debug)] +// Everything quoted in these messages is text the user typed and has to come +// back unchanged, hence translate_text! rather than translate!. +#[derive(Debug, thiserror::Error)] pub enum FormatError { /// The spec that failed to parse and its byte range in the format string. + #[error("{}", translate_text!("format-error-invalid-spec", "spec" => String::from_utf8_lossy(.0)))] SpecError(Vec, Range), + #[error("{}", translate_text!("format-error-write", "error" => strip_errno(.0)))] IoError(std::io::Error), + #[error("{}", translate!("format-error-no-more-arguments"))] NoMoreArguments, + #[error("{}", translate!("format-error-invalid-argument"))] InvalidArgument(FormatArgument), + #[error("{}", translate_text!("format-error-too-many-specs", "format" => String::from_utf8_lossy(.0)))] TooManySpecs(Vec), + #[error("{}", translate_text!("format-error-no-spec", "format" => String::from_utf8_lossy(.0)))] NeedAtLeastOneSpec(Vec), + // TODO: this message needs some work + #[error("{}", translate!("format-error-wrong-spec-type"))] WrongSpecType, + #[error("{}", translate_text!("format-error-invalid-precision", "precision" => .0))] InvalidPrecision(String), /// The format specifier ends with a %, as in `%f%`. + #[error("{}", translate_text!("format-error-ends-with-percent", "format" => String::from_utf8_lossy(.0).quote()))] EndsWithPercent(Vec), /// The escape sequence `\x` appears without a literal hexadecimal value. /// Carries its byte range in the format string, when it came from one. + #[error("{}", translate!("format-error-missing-hex"))] MissingHex(Option>), /// The hexadecimal characters represent a code point that cannot represent a /// Unicode character (e.g., a surrogate code point) /// Carries its byte range in the format string, when it came from one. + #[error("{}", translate_text!("format-error-invalid-universal-character", "escape" => .0, "digits" => String::from_utf8_lossy(.1)))] InvalidCharacter(char, Vec, Option>), + #[error("{0}")] InvalidEncoding(NonUtf8OsStrError), } @@ -114,7 +127,6 @@ fn spec_error(fmt: &[u8], percent: usize, slice: &[u8]) -> FormatError { FormatError::SpecError(slice.to_vec(), percent..start + slice.len()) } -impl Error for FormatError {} impl UError for FormatError {} impl From for FormatError { @@ -129,43 +141,6 @@ impl From for FormatError { } } -impl Display for FormatError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - // Everything quoted below is text the user typed and has to come back - // unchanged, hence translate_text! rather than translate!. - let message = match self { - Self::SpecError(s, _) => { - translate_text!("format-error-invalid-spec", "spec" => String::from_utf8_lossy(s)) - } - Self::TooManySpecs(s) => { - translate_text!("format-error-too-many-specs", "format" => String::from_utf8_lossy(s)) - } - Self::NeedAtLeastOneSpec(s) => { - translate_text!("format-error-no-spec", "format" => String::from_utf8_lossy(s)) - } - Self::EndsWithPercent(s) => { - translate_text!("format-error-ends-with-percent", "format" => String::from_utf8_lossy(s).quote()) - } - Self::InvalidPrecision(precision) => { - translate_text!("format-error-invalid-precision", "precision" => precision) - } - // TODO: Error message below needs some work - Self::WrongSpecType => translate!("format-error-wrong-spec-type"), - Self::IoError(e) => translate_text!("format-error-write", "error" => strip_errno(e)), - Self::NoMoreArguments => translate!("format-error-no-more-arguments"), - Self::InvalidArgument(_) => translate!("format-error-invalid-argument"), - Self::MissingHex(_) => translate!("format-error-missing-hex"), - Self::InvalidCharacter(escape_char, digits, _) => translate_text!( - "format-error-invalid-universal-character", - "escape" => escape_char, - "digits" => String::from_utf8_lossy(digits) - ), - Self::InvalidEncoding(no) => return no.fmt(f), - }; - f.write_str(&message) - } -} - /// Maximum width for formatting to prevent memory allocation panics. /// Rust's formatter will panic when trying to allocate memory for very large widths. /// This limit is somewhat arbitrary but should be well above any practical use case