From 1b3ae995d68cb523c2e3e72f36af1b78572e2b33 Mon Sep 17 00:00:00 2001 From: mattsu Date: Sat, 21 Feb 2026 13:44:32 +0900 Subject: [PATCH 1/6] fix(date): handle width overflow in format modifiers Update `apply_modifiers` to return `Result` and check for integer overflow when calculating padding length. This prevents potential panics or incorrect formatting when very large width values are specified. The change adds proper error handling for the "field width too large" case and propagates errors through the formatting pipeline. --- src/uu/date/src/format_modifiers.rs | 148 ++++++++++++++++++++-------- tests/by-util/test_date.rs | 10 ++ 2 files changed, 118 insertions(+), 40 deletions(-) diff --git a/src/uu/date/src/format_modifiers.rs b/src/uu/date/src/format_modifiers.rs index 00e9df77817..6a4e20ecbba 100644 --- a/src/uu/date/src/format_modifiers.rs +++ b/src/uu/date/src/format_modifiers.rs @@ -44,8 +44,7 @@ use std::sync::OnceLock; pub enum FormatError { /// Error from the underlying jiff library JiffError(jiff::Error), - /// Custom error message (reserved for future use) - #[allow(dead_code)] + /// Custom error message Custom(String), } @@ -64,6 +63,12 @@ impl From for FormatError { } } +const ERR_FIELD_WIDTH_TOO_LARGE: &str = "field width too large"; + +fn width_too_large_error() -> FormatError { + FormatError::Custom(ERR_FIELD_WIDTH_TOO_LARGE.to_string()) +} + /// Regex to match format specifiers with optional modifiers /// Pattern: % \[flags\] \[width\] specifier /// Flags: -, _, 0, ^, #, + @@ -147,7 +152,7 @@ fn format_with_modifiers( // Apply modifiers to the formatted value let width: usize = width_str.parse().unwrap_or(0); let explicit_width = !width_str.is_empty(); - let modified = apply_modifiers(&formatted, flags, width, spec, explicit_width); + let modified = apply_modifiers(&formatted, flags, width, spec, explicit_width)?; result.push_str(&modified); } else { // No modifiers, use formatted value as-is @@ -266,7 +271,7 @@ fn apply_modifiers( width: usize, specifier: &str, explicit_width: bool, -) -> String { +) -> Result { let mut result = value.to_string(); // Determine default pad character based on specifier type @@ -336,7 +341,7 @@ fn apply_modifiers( // If no_pad flag is active, suppress all padding and return if no_pad { - return strip_default_padding(&result); + return Ok(strip_default_padding(&result)); } // Handle padding flag without explicit width: use default width @@ -350,7 +355,7 @@ fn apply_modifiers( // Handle width smaller than result: strip default padding to fit if effective_width > 0 && effective_width < result.len() { - return strip_default_padding(&result); + return Ok(strip_default_padding(&result)); } // Strip default padding when switching pad characters on numeric fields @@ -387,14 +392,35 @@ fn apply_modifiers( // Zero padding: sign first, then zeros (e.g., "-0022") let sign = result.chars().next().unwrap(); let rest = &result[1..]; - result = format!("{sign}{}{rest}", "0".repeat(padding)); + let target_len = result + .len() + .checked_add(padding) + .ok_or_else(width_too_large_error)?; + let mut padded = String::new(); + padded + .try_reserve(target_len) + .map_err(|_| width_too_large_error())?; + padded.push(sign); + padded.extend(std::iter::repeat('0').take(padding)); + padded.push_str(rest); + result = padded; } else { // Default: pad on the left (e.g., " -22" or " 1999") - result = format!("{}{result}", pad_char.to_string().repeat(padding)); + let target_len = result + .len() + .checked_add(padding) + .ok_or_else(width_too_large_error)?; + let mut padded = String::new(); + padded + .try_reserve(target_len) + .map_err(|_| width_too_large_error())?; + padded.extend(std::iter::repeat(pad_char).take(padding)); + padded.push_str(&result); + result = padded; } } - result + Ok(result) } #[cfg(test)] @@ -574,63 +600,90 @@ mod tests { #[test] fn test_apply_modifiers_basic() { // No modifiers (numeric specifier) - assert_eq!(apply_modifiers("1999", "", 0, "Y", false), "1999"); + assert_eq!(apply_modifiers("1999", "", 0, "Y", false).unwrap(), "1999"); // Zero padding - assert_eq!(apply_modifiers("1999", "0", 10, "Y", true), "0000001999"); + assert_eq!( + apply_modifiers("1999", "0", 10, "Y", true).unwrap(), + "0000001999" + ); // Space padding (strips leading zeros) - assert_eq!(apply_modifiers("06", "_", 5, "m", true), " 6"); + assert_eq!(apply_modifiers("06", "_", 5, "m", true).unwrap(), " 6"); // No-pad (strips leading zeros, width ignored) - assert_eq!(apply_modifiers("01", "-", 5, "d", true), "1"); + assert_eq!(apply_modifiers("01", "-", 5, "d", true).unwrap(), "1"); // Uppercase - assert_eq!(apply_modifiers("june", "^", 0, "B", false), "JUNE"); + assert_eq!(apply_modifiers("june", "^", 0, "B", false).unwrap(), "JUNE"); // Swap case: all uppercase → lowercase - assert_eq!(apply_modifiers("UTC", "#", 0, "Z", false), "utc"); + assert_eq!(apply_modifiers("UTC", "#", 0, "Z", false).unwrap(), "utc"); // Swap case: mixed case → uppercase - assert_eq!(apply_modifiers("June", "#", 0, "B", false), "JUNE"); + assert_eq!(apply_modifiers("June", "#", 0, "B", false).unwrap(), "JUNE"); } #[test] fn test_apply_modifiers_signs() { // Force sign with explicit width - assert_eq!(apply_modifiers("1970", "+", 6, "Y", true), "+01970"); + assert_eq!( + apply_modifiers("1970", "+", 6, "Y", true).unwrap(), + "+01970" + ); // Force sign without explicit width: should NOT add sign for 4-digit year - assert_eq!(apply_modifiers("1999", "+", 0, "Y", false), "1999"); + assert_eq!(apply_modifiers("1999", "+", 0, "Y", false).unwrap(), "1999"); // Force sign without explicit width: SHOULD add sign for year > 4 digits - assert_eq!(apply_modifiers("12345", "+", 0, "Y", false), "+12345"); + assert_eq!( + apply_modifiers("12345", "+", 0, "Y", false).unwrap(), + "+12345" + ); // Negative with zero padding: sign first, then zeros - assert_eq!(apply_modifiers("-22", "0", 5, "s", true), "-0022"); + assert_eq!(apply_modifiers("-22", "0", 5, "s", true).unwrap(), "-0022"); // Negative with space padding: spaces first, then sign - assert_eq!(apply_modifiers("-22", "_", 5, "s", true), " -22"); + assert_eq!(apply_modifiers("-22", "_", 5, "s", true).unwrap(), " -22"); // Force sign (_+): + is last, overrides _ → zero pad with sign - assert_eq!(apply_modifiers("5", "_+", 5, "s", true), "+0005"); + assert_eq!(apply_modifiers("5", "_+", 5, "s", true).unwrap(), "+0005"); // No-pad + uppercase: no padding applied - assert_eq!(apply_modifiers("june", "-^", 10, "B", true), "JUNE"); + assert_eq!( + apply_modifiers("june", "-^", 10, "B", true).unwrap(), + "JUNE" + ); } #[test] fn test_case_flag_precedence() { // Test that ^ (uppercase) overrides # (swap case) - assert_eq!(apply_modifiers("June", "^#", 0, "B", false), "JUNE"); - assert_eq!(apply_modifiers("June", "#^", 0, "B", false), "JUNE"); + assert_eq!( + apply_modifiers("June", "^#", 0, "B", false).unwrap(), + "JUNE" + ); + assert_eq!( + apply_modifiers("June", "#^", 0, "B", false).unwrap(), + "JUNE" + ); // Test # alone (swap case) - assert_eq!(apply_modifiers("June", "#", 0, "B", false), "JUNE"); - assert_eq!(apply_modifiers("JUNE", "#", 0, "B", false), "june"); + assert_eq!(apply_modifiers("June", "#", 0, "B", false).unwrap(), "JUNE"); + assert_eq!(apply_modifiers("JUNE", "#", 0, "B", false).unwrap(), "june"); } #[test] fn test_apply_modifiers_text_specifiers() { // Text specifiers default to space padding - assert_eq!(apply_modifiers("June", "", 10, "B", true), " June"); - assert_eq!(apply_modifiers("Mon", "", 10, "a", true), " Mon"); + assert_eq!( + apply_modifiers("June", "", 10, "B", true).unwrap(), + " June" + ); + assert_eq!( + apply_modifiers("Mon", "", 10, "a", true).unwrap(), + " Mon" + ); // Numeric specifiers default to zero padding - assert_eq!(apply_modifiers("6", "", 10, "m", true), "0000000006"); + assert_eq!( + apply_modifiers("6", "", 10, "m", true).unwrap(), + "0000000006" + ); } #[test] fn test_apply_modifiers_width_smaller_than_result() { // Width smaller than result strips default padding - assert_eq!(apply_modifiers("01", "", 1, "d", true), "1"); - assert_eq!(apply_modifiers("06", "", 1, "m", true), "6"); + assert_eq!(apply_modifiers("01", "", 1, "d", true).unwrap(), "1"); + assert_eq!(apply_modifiers("06", "", 1, "m", true).unwrap(), "6"); } #[test] @@ -650,7 +703,7 @@ mod tests { for (value, flags, width, spec, explicit_width, expected) in test_cases { assert_eq!( - apply_modifiers(value, flags, width, spec, explicit_width), + apply_modifiers(value, flags, width, spec, explicit_width).unwrap(), expected, "value='{value}', flags='{flags}', width={width}, spec='{spec}', explicit_width={explicit_width}", ); @@ -660,23 +713,29 @@ mod tests { #[test] fn test_underscore_flag_without_width() { // %_m should pad month to default width 2 with spaces - assert_eq!(apply_modifiers("6", "_", 0, "m", false), " 6"); + assert_eq!(apply_modifiers("6", "_", 0, "m", false).unwrap(), " 6"); // %_d should pad day to default width 2 with spaces - assert_eq!(apply_modifiers("1", "_", 0, "d", false), " 1"); + assert_eq!(apply_modifiers("1", "_", 0, "d", false).unwrap(), " 1"); // %_H should pad hour to default width 2 with spaces - assert_eq!(apply_modifiers("5", "_", 0, "H", false), " 5"); + assert_eq!(apply_modifiers("5", "_", 0, "H", false).unwrap(), " 5"); // %_Y should pad year to default width 4 with spaces - assert_eq!(apply_modifiers("1999", "_", 0, "Y", false), "1999"); // already at default width + assert_eq!(apply_modifiers("1999", "_", 0, "Y", false).unwrap(), "1999"); // already at default width } #[test] fn test_plus_flag_without_width() { // %+Y without width should NOT add sign for 4-digit year - assert_eq!(apply_modifiers("1999", "+", 0, "Y", false), "1999"); + assert_eq!(apply_modifiers("1999", "+", 0, "Y", false).unwrap(), "1999"); // %+Y without width SHOULD add sign for year > 4 digits - assert_eq!(apply_modifiers("12345", "+", 0, "Y", false), "+12345"); + assert_eq!( + apply_modifiers("12345", "+", 0, "Y", false).unwrap(), + "+12345" + ); // %+Y with explicit width should add sign - assert_eq!(apply_modifiers("1999", "+", 6, "Y", true), "+01999"); + assert_eq!( + apply_modifiers("1999", "+", 6, "Y", true).unwrap(), + "+01999" + ); } #[test] @@ -710,4 +769,13 @@ mod tests { "GNU: %_C should produce '19', not ' 19' (default width is 2, not 4)" ); } + + #[test] + fn test_apply_modifiers_width_too_large() { + let err = apply_modifiers("x", "", usize::MAX, "c", true).unwrap_err(); + assert!(matches!( + err, + FormatError::Custom(message) if message == ERR_FIELD_WIDTH_TOO_LARGE + )); + } } diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index a6139848c08..4a25d6543a0 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -2507,6 +2507,16 @@ fn test_date_format_modifier_edge_cases() { } } +#[test] +fn test_date_format_modifier_huge_width_fails_without_abort() { + let format = format!("+%{}c", usize::MAX); + new_ucmd!() + .arg(&format) + .fails() + .code_is(1) + .stderr_contains("field width too large"); +} + // Tests for --debug flag #[test] fn test_date_debug_basic() { From 21478adf2cd906bc8dbb333b59a81a3903547601 Mon Sep 17 00:00:00 2001 From: mattsu Date: Sat, 21 Feb 2026 15:26:23 +0900 Subject: [PATCH 2/6] refactor: replace `repeat().take()` with `repeat_n()` for better performance Replace `std::iter::repeat().take()` patterns with `std::iter::repeat_n()` in format_modifiers.rs. This change improves performance by avoiding the overhead of the `take()` iterator adapter and directly creating an iterator that yields the exact number of elements needed. --- src/uu/date/src/format_modifiers.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/uu/date/src/format_modifiers.rs b/src/uu/date/src/format_modifiers.rs index 6a4e20ecbba..b703f2d0f1a 100644 --- a/src/uu/date/src/format_modifiers.rs +++ b/src/uu/date/src/format_modifiers.rs @@ -401,7 +401,7 @@ fn apply_modifiers( .try_reserve(target_len) .map_err(|_| width_too_large_error())?; padded.push(sign); - padded.extend(std::iter::repeat('0').take(padding)); + padded.extend(std::iter::repeat_n('0', padding)); padded.push_str(rest); result = padded; } else { @@ -414,7 +414,7 @@ fn apply_modifiers( padded .try_reserve(target_len) .map_err(|_| width_too_large_error())?; - padded.extend(std::iter::repeat(pad_char).take(padding)); + padded.extend(std::iter::repeat_n(pad_char, padding)); padded.push_str(&result); result = padded; } From ad844ca6179dee10d6d36b2b85a48a3dfe420235 Mon Sep 17 00:00:00 2001 From: mattsu Date: Mon, 23 Feb 2026 10:20:34 +0900 Subject: [PATCH 3/6] fix(date): add error handling for format modifier width overflow Added proper error handling for format modifier width calculations that could overflow or fail allocation. The change replaces generic error messages with a specific error type `FieldWidthTooLarge` that includes the problematic width and specifier values, improving error reporting for date format operations. --- src/uu/date/locales/en-US.ftl | 1 + src/uu/date/locales/fr-FR.ftl | 1 + src/uu/date/src/format_modifiers.rs | 54 ++++++++++++++++++----------- tests/by-util/test_date.rs | 8 ++--- 4 files changed, 39 insertions(+), 25 deletions(-) diff --git a/src/uu/date/locales/en-US.ftl b/src/uu/date/locales/en-US.ftl index 512510c1b53..ea864904285 100644 --- a/src/uu/date/locales/en-US.ftl +++ b/src/uu/date/locales/en-US.ftl @@ -106,6 +106,7 @@ date-error-setting-date-not-supported-redox = setting the date is not supported date-error-cannot-set-date = cannot set date date-error-extra-operand = extra operand '{$operand}' date-error-write = write error: {$error} +date-error-format-modifier-width-too-large = format modifier width '{$width}' is too large for specifier '%{$specifier}' date-error-format-missing-plus = the argument {$arg} lacks a leading '+'; when using an option to specify date(s), any non-option argument must be a format string beginning with '+' diff --git a/src/uu/date/locales/fr-FR.ftl b/src/uu/date/locales/fr-FR.ftl index 3c09d4164d1..9a67704af1e 100644 --- a/src/uu/date/locales/fr-FR.ftl +++ b/src/uu/date/locales/fr-FR.ftl @@ -101,6 +101,7 @@ date-error-setting-date-not-supported-redox = la définition de la date n'est pa date-error-cannot-set-date = impossible de définir la date date-error-extra-operand = opérande supplémentaire '{$operand}' date-error-write = erreur d'écriture: {$error} +date-error-format-modifier-width-too-large = la largeur du modificateur de format '{$width}' est trop grande pour le spécificateur '%{$specifier}' date-error-format-missing-plus = l'argument {$arg} ne commence pas par un signe '+'; lorsqu'une option est utilisée pour spécifier une ou plusieurs dates, tout argument autre qu'une option doit être une chaîne de format commençant par un signe '+'. diff --git a/src/uu/date/src/format_modifiers.rs b/src/uu/date/src/format_modifiers.rs index b703f2d0f1a..d5f1963957e 100644 --- a/src/uu/date/src/format_modifiers.rs +++ b/src/uu/date/src/format_modifiers.rs @@ -38,21 +38,30 @@ use jiff::fmt::strtime::{BrokenDownTime, Config, PosixCustom}; use regex::Regex; use std::fmt; use std::sync::OnceLock; +use uucore::translate; /// Error type for format modifier operations #[derive(Debug)] pub enum FormatError { /// Error from the underlying jiff library JiffError(jiff::Error), - /// Custom error message - Custom(String), + /// Field width calculation overflowed or required allocation failed + FieldWidthTooLarge { width: usize, specifier: String }, } impl fmt::Display for FormatError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::JiffError(e) => write!(f, "{e}"), - Self::Custom(s) => write!(f, "{s}"), + Self::FieldWidthTooLarge { width, specifier } => write!( + f, + "{}", + translate!( + "date-error-format-modifier-width-too-large", + "width" => width, + "specifier" => specifier + ) + ), } } } @@ -63,12 +72,6 @@ impl From for FormatError { } } -const ERR_FIELD_WIDTH_TOO_LARGE: &str = "field width too large"; - -fn width_too_large_error() -> FormatError { - FormatError::Custom(ERR_FIELD_WIDTH_TOO_LARGE.to_string()) -} - /// Regex to match format specifiers with optional modifiers /// Pattern: % \[flags\] \[width\] specifier /// Flags: -, _, 0, ^, #, + @@ -392,28 +395,38 @@ fn apply_modifiers( // Zero padding: sign first, then zeros (e.g., "-0022") let sign = result.chars().next().unwrap(); let rest = &result[1..]; - let target_len = result - .len() - .checked_add(padding) - .ok_or_else(width_too_large_error)?; + let target_len = result.len().checked_add(padding).ok_or_else(|| { + FormatError::FieldWidthTooLarge { + width, + specifier: specifier.to_string(), + } + })?; let mut padded = String::new(); padded .try_reserve(target_len) - .map_err(|_| width_too_large_error())?; + .map_err(|_| FormatError::FieldWidthTooLarge { + width, + specifier: specifier.to_string(), + })?; padded.push(sign); padded.extend(std::iter::repeat_n('0', padding)); padded.push_str(rest); result = padded; } else { // Default: pad on the left (e.g., " -22" or " 1999") - let target_len = result - .len() - .checked_add(padding) - .ok_or_else(width_too_large_error)?; + let target_len = result.len().checked_add(padding).ok_or_else(|| { + FormatError::FieldWidthTooLarge { + width, + specifier: specifier.to_string(), + } + })?; let mut padded = String::new(); padded .try_reserve(target_len) - .map_err(|_| width_too_large_error())?; + .map_err(|_| FormatError::FieldWidthTooLarge { + width, + specifier: specifier.to_string(), + })?; padded.extend(std::iter::repeat_n(pad_char, padding)); padded.push_str(&result); result = padded; @@ -775,7 +788,8 @@ mod tests { let err = apply_modifiers("x", "", usize::MAX, "c", true).unwrap_err(); assert!(matches!( err, - FormatError::Custom(message) if message == ERR_FIELD_WIDTH_TOO_LARGE + FormatError::FieldWidthTooLarge { width, specifier } + if width == usize::MAX && specifier == "c" )); } } diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index 4a25d6543a0..c38253c7ce5 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -2509,12 +2509,10 @@ fn test_date_format_modifier_edge_cases() { #[test] fn test_date_format_modifier_huge_width_fails_without_abort() { + // GNU date also exits with failure for extremely large width. + // Assert exit code only to avoid coupling to implementation-specific error text. let format = format!("+%{}c", usize::MAX); - new_ucmd!() - .arg(&format) - .fails() - .code_is(1) - .stderr_contains("field width too large"); + new_ucmd!().arg(&format).fails().code_is(1); } // Tests for --debug flag From a92c2d1876706fd973c8936e21a211b50e472cf3 Mon Sep 17 00:00:00 2001 From: mattsu Date: Mon, 23 Feb 2026 10:29:12 +0900 Subject: [PATCH 4/6] fix: improve error handling for format modifier width parsing - Changed FormatError::FieldWidthTooLarge to store width as String instead of usize - Updated error message generation to use FluentArgs for proper localization - Fixed width parsing to return FormatError instead of panicking on parse failure - Updated error message formatting to use get_message_with_args for consistency - Fixed test assertions to compare width as String instead of usize --- src/uu/date/src/format_modifiers.rs | 43 ++++++++++++++++++----------- tests/by-util/test_date.rs | 9 ++++-- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/src/uu/date/src/format_modifiers.rs b/src/uu/date/src/format_modifiers.rs index d5f1963957e..50e444025b9 100644 --- a/src/uu/date/src/format_modifiers.rs +++ b/src/uu/date/src/format_modifiers.rs @@ -33,12 +33,13 @@ //! - `%^B`: Month name in uppercase (JUNE) //! - `%+4C`: Century with sign, padded to 4 characters (+019) +use fluent::FluentArgs; use jiff::Zoned; use jiff::fmt::strtime::{BrokenDownTime, Config, PosixCustom}; use regex::Regex; use std::fmt; use std::sync::OnceLock; -use uucore::translate; +use uucore::locale::get_message_with_args; /// Error type for format modifier operations #[derive(Debug)] @@ -46,22 +47,23 @@ pub enum FormatError { /// Error from the underlying jiff library JiffError(jiff::Error), /// Field width calculation overflowed or required allocation failed - FieldWidthTooLarge { width: usize, specifier: String }, + FieldWidthTooLarge { width: String, specifier: String }, } impl fmt::Display for FormatError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::JiffError(e) => write!(f, "{e}"), - Self::FieldWidthTooLarge { width, specifier } => write!( - f, - "{}", - translate!( - "date-error-format-modifier-width-too-large", - "width" => width, - "specifier" => specifier + Self::FieldWidthTooLarge { width, specifier } => { + let mut args = FluentArgs::new(); + args.set("width", width.clone()); + args.set("specifier", specifier.clone()); + write!( + f, + "{}", + get_message_with_args("date-error-format-modifier-width-too-large", args) ) - ), + } } } } @@ -153,7 +155,16 @@ fn format_with_modifiers( // Check if this specifier has modifiers if !flags.is_empty() || !width_str.is_empty() { // Apply modifiers to the formatted value - let width: usize = width_str.parse().unwrap_or(0); + let width = if width_str.is_empty() { + 0 + } else { + width_str + .parse() + .map_err(|_| FormatError::FieldWidthTooLarge { + width: width_str.to_string(), + specifier: spec.to_string(), + })? + }; let explicit_width = !width_str.is_empty(); let modified = apply_modifiers(&formatted, flags, width, spec, explicit_width)?; result.push_str(&modified); @@ -397,7 +408,7 @@ fn apply_modifiers( let rest = &result[1..]; let target_len = result.len().checked_add(padding).ok_or_else(|| { FormatError::FieldWidthTooLarge { - width, + width: width.to_string(), specifier: specifier.to_string(), } })?; @@ -405,7 +416,7 @@ fn apply_modifiers( padded .try_reserve(target_len) .map_err(|_| FormatError::FieldWidthTooLarge { - width, + width: width.to_string(), specifier: specifier.to_string(), })?; padded.push(sign); @@ -416,7 +427,7 @@ fn apply_modifiers( // Default: pad on the left (e.g., " -22" or " 1999") let target_len = result.len().checked_add(padding).ok_or_else(|| { FormatError::FieldWidthTooLarge { - width, + width: width.to_string(), specifier: specifier.to_string(), } })?; @@ -424,7 +435,7 @@ fn apply_modifiers( padded .try_reserve(target_len) .map_err(|_| FormatError::FieldWidthTooLarge { - width, + width: width.to_string(), specifier: specifier.to_string(), })?; padded.extend(std::iter::repeat_n(pad_char, padding)); @@ -789,7 +800,7 @@ mod tests { assert!(matches!( err, FormatError::FieldWidthTooLarge { width, specifier } - if width == usize::MAX && specifier == "c" + if width == usize::MAX.to_string() && specifier == "c" )); } } diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index c38253c7ce5..cc9c260d3b7 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -2511,8 +2511,13 @@ fn test_date_format_modifier_edge_cases() { fn test_date_format_modifier_huge_width_fails_without_abort() { // GNU date also exits with failure for extremely large width. // Assert exit code only to avoid coupling to implementation-specific error text. - let format = format!("+%{}c", usize::MAX); - new_ucmd!().arg(&format).fails().code_is(1); + let formats = [ + format!("+%{}c", usize::MAX), + "+%184467440737095516160c".into(), + ]; + for format in formats { + new_ucmd!().arg(&format).fails().code_is(1); + } } // Tests for --debug flag From f1cbcd590f4004fc44df11146e445b0e9f7955b8 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 4 Apr 2026 13:23:25 +0200 Subject: [PATCH 5/6] date: extract try_alloc_padded helper and add large-width test Co-authored-by: Sylvestre Ledru --- src/uu/date/src/format_modifiers.rs | 108 +++++++++++++--------------- tests/by-util/test_date.rs | 54 ++++++++++---- 2 files changed, 90 insertions(+), 72 deletions(-) diff --git a/src/uu/date/src/format_modifiers.rs b/src/uu/date/src/format_modifiers.rs index 50e444025b9..8c18c3cca71 100644 --- a/src/uu/date/src/format_modifiers.rs +++ b/src/uu/date/src/format_modifiers.rs @@ -33,13 +33,12 @@ //! - `%^B`: Month name in uppercase (JUNE) //! - `%+4C`: Century with sign, padded to 4 characters (+019) -use fluent::FluentArgs; use jiff::Zoned; use jiff::fmt::strtime::{BrokenDownTime, Config, PosixCustom}; use regex::Regex; use std::fmt; use std::sync::OnceLock; -use uucore::locale::get_message_with_args; +use uucore::translate; /// Error type for format modifier operations #[derive(Debug)] @@ -47,23 +46,22 @@ pub enum FormatError { /// Error from the underlying jiff library JiffError(jiff::Error), /// Field width calculation overflowed or required allocation failed - FieldWidthTooLarge { width: String, specifier: String }, + FieldWidthTooLarge { width: usize, specifier: String }, } impl fmt::Display for FormatError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::JiffError(e) => write!(f, "{e}"), - Self::FieldWidthTooLarge { width, specifier } => { - let mut args = FluentArgs::new(); - args.set("width", width.clone()); - args.set("specifier", specifier.clone()); - write!( - f, - "{}", - get_message_with_args("date-error-format-modifier-width-too-large", args) + Self::FieldWidthTooLarge { width, specifier } => write!( + f, + "{}", + translate!( + "date-error-format-modifier-width-too-large", + "width" => width, + "specifier" => specifier ) - } + ), } } } @@ -155,16 +153,7 @@ fn format_with_modifiers( // Check if this specifier has modifiers if !flags.is_empty() || !width_str.is_empty() { // Apply modifiers to the formatted value - let width = if width_str.is_empty() { - 0 - } else { - width_str - .parse() - .map_err(|_| FormatError::FieldWidthTooLarge { - width: width_str.to_string(), - specifier: spec.to_string(), - })? - }; + let width: usize = width_str.parse().unwrap_or(0); let explicit_width = !width_str.is_empty(); let modified = apply_modifiers(&formatted, flags, width, spec, explicit_width)?; result.push_str(&modified); @@ -406,38 +395,14 @@ fn apply_modifiers( // Zero padding: sign first, then zeros (e.g., "-0022") let sign = result.chars().next().unwrap(); let rest = &result[1..]; - let target_len = result.len().checked_add(padding).ok_or_else(|| { - FormatError::FieldWidthTooLarge { - width: width.to_string(), - specifier: specifier.to_string(), - } - })?; - let mut padded = String::new(); - padded - .try_reserve(target_len) - .map_err(|_| FormatError::FieldWidthTooLarge { - width: width.to_string(), - specifier: specifier.to_string(), - })?; + let mut padded = try_alloc_padded(result.len(), padding, effective_width, specifier)?; padded.push(sign); padded.extend(std::iter::repeat_n('0', padding)); padded.push_str(rest); result = padded; } else { // Default: pad on the left (e.g., " -22" or " 1999") - let target_len = result.len().checked_add(padding).ok_or_else(|| { - FormatError::FieldWidthTooLarge { - width: width.to_string(), - specifier: specifier.to_string(), - } - })?; - let mut padded = String::new(); - padded - .try_reserve(target_len) - .map_err(|_| FormatError::FieldWidthTooLarge { - width: width.to_string(), - specifier: specifier.to_string(), - })?; + let mut padded = try_alloc_padded(result.len(), padding, effective_width, specifier)?; padded.extend(std::iter::repeat_n(pad_char, padding)); padded.push_str(&result); result = padded; @@ -447,6 +412,30 @@ fn apply_modifiers( Ok(result) } +/// Allocate a `String` with enough capacity for `current_len + padding`, +/// returning `FieldWidthTooLarge` on arithmetic overflow or allocation failure. +fn try_alloc_padded( + current_len: usize, + padding: usize, + width: usize, + specifier: &str, +) -> Result { + let target_len = + current_len + .checked_add(padding) + .ok_or_else(|| FormatError::FieldWidthTooLarge { + width, + specifier: specifier.to_string(), + })?; + let mut s = String::new(); + s.try_reserve(target_len) + .map_err(|_| FormatError::FieldWidthTooLarge { + width, + specifier: specifier.to_string(), + })?; + Ok(s) +} + #[cfg(test)] mod tests { use super::*; @@ -734,6 +723,16 @@ mod tests { } } + #[test] + fn test_apply_modifiers_width_too_large() { + let err = apply_modifiers("x", "", usize::MAX, "c", true).unwrap_err(); + assert!(matches!( + err, + FormatError::FieldWidthTooLarge { width, specifier } + if width == usize::MAX && specifier == "c" + )); + } + #[test] fn test_underscore_flag_without_width() { // %_m should pad month to default width 2 with spaces @@ -743,7 +742,8 @@ mod tests { // %_H should pad hour to default width 2 with spaces assert_eq!(apply_modifiers("5", "_", 0, "H", false).unwrap(), " 5"); // %_Y should pad year to default width 4 with spaces - assert_eq!(apply_modifiers("1999", "_", 0, "Y", false).unwrap(), "1999"); // already at default width + assert_eq!(apply_modifiers("1999", "_", 0, "Y", false).unwrap(), "1999"); + // already at default width } #[test] @@ -793,14 +793,4 @@ mod tests { "GNU: %_C should produce '19', not ' 19' (default width is 2, not 4)" ); } - - #[test] - fn test_apply_modifiers_width_too_large() { - let err = apply_modifiers("x", "", usize::MAX, "c", true).unwrap_err(); - assert!(matches!( - err, - FormatError::FieldWidthTooLarge { width, specifier } - if width == usize::MAX.to_string() && specifier == "c" - )); - } } diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index cc9c260d3b7..56c127a1a68 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -2387,6 +2387,47 @@ fn test_date_format_modifier_percent_escape() { .stdout_is("%Y=0000001999\n"); } +#[test] +fn test_date_format_modifier_huge_width_fails_without_abort() { + // GNU date also exits with failure for extremely large width. + // Assert exit code only to avoid coupling to implementation-specific error text. + let format = format!("+%{}c", usize::MAX); + new_ucmd!().arg(&format).fails().code_is(1); +} + +#[test] +fn test_date_format_large_width_no_oom() { + // Regression: very large width like %8888888888r caused OOM. + // GNU caps width to i32::MAX; verify we don't crash. + // Use a moderate width with a fixed date to check the code path works. + new_ucmd!() + .arg("-d") + .arg("2024-01-01") + .arg("+%300S") + .succeeds() + .stdout_is(format!("{}\n", format_args!("{:0>300}", "00"))); + + // Test with a larger width to exercise the code path without producing + // gigabytes of output (the original %8888888888r would produce ~2GB). + new_ucmd!() + .arg("-d") + .arg("2024-01-01") + .arg("+%10000S") + .succeeds() + .stdout_is(format!("{}\n", format_args!("{:0>10000}", "00"))); + + // Mixed literal text with multiple width-modified specifiers. + // 2024-01-01 is Monday (day-of-week 1). + // %2u → "01", literal "ueuu", %6666u → "1" zero-padded to 6666, literal "-r". + let expected = format!("01ueuu{}-r\n", format_args!("{:0>6666}", "1")); + new_ucmd!() + .arg("-d") + .arg("2024-01-01") + .arg("+%2uueuu%6666u-r") + .succeeds() + .stdout_is(expected); +} + // Tests for format modifier edge cases (flags without explicit width) #[test] fn test_date_format_modifier_edge_cases() { @@ -2507,19 +2548,6 @@ fn test_date_format_modifier_edge_cases() { } } -#[test] -fn test_date_format_modifier_huge_width_fails_without_abort() { - // GNU date also exits with failure for extremely large width. - // Assert exit code only to avoid coupling to implementation-specific error text. - let formats = [ - format!("+%{}c", usize::MAX), - "+%184467440737095516160c".into(), - ]; - for format in formats { - new_ucmd!().arg(&format).fails().code_is(1); - } -} - // Tests for --debug flag #[test] fn test_date_debug_basic() { From 29a1ba75aab44a1d333769d47e9fe473dd21d79d Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 4 Apr 2026 22:28:01 +0200 Subject: [PATCH 6/6] Modify spell-checker ignore comments Updated spell-checker ignore list in test_date.rs --- tests/by-util/test_date.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index 56c127a1a68..df5c58b2f34 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -3,7 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. // -// spell-checker: ignore: AEDT AEST EEST NZDT NZST Kolkata Iseconds févr février janv janvier mercredi samedi sommes juin décembre Januar Juni Dezember enero junio diciembre gennaio giugno dicembre junho dezembro lundi dimanche Montag Sonntag Samstag sábado febr MEST KST +// spell-checker: ignore: AEDT AEST EEST NZDT NZST Kolkata Iseconds févr février janv janvier mercredi samedi sommes juin décembre Januar Juni Dezember enero junio diciembre gennaio giugno dicembre junho dezembro lundi dimanche Montag Sonntag Samstag sábado febr MEST KST uueuu ueuu use std::cmp::Ordering;