From dbdd2ddfda3c7213cac52ae03df681a0cec34294 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 4 Apr 2026 13:23:25 +0200 Subject: [PATCH] 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 50e444025..8c18c3cca 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 cc9c260d3..56c127a1a 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() {