Skip to content

Commit f1cbcd5

Browse files
committed
date: extract try_alloc_padded helper and add large-width test
Co-authored-by: Sylvestre Ledru <sylvestre@debian.org>
1 parent a92c2d1 commit f1cbcd5

2 files changed

Lines changed: 90 additions & 72 deletions

File tree

src/uu/date/src/format_modifiers.rs

Lines changed: 49 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -33,37 +33,35 @@
3333
//! - `%^B`: Month name in uppercase (JUNE)
3434
//! - `%+4C`: Century with sign, padded to 4 characters (+019)
3535
36-
use fluent::FluentArgs;
3736
use jiff::Zoned;
3837
use jiff::fmt::strtime::{BrokenDownTime, Config, PosixCustom};
3938
use regex::Regex;
4039
use std::fmt;
4140
use std::sync::OnceLock;
42-
use uucore::locale::get_message_with_args;
41+
use uucore::translate;
4342

4443
/// Error type for format modifier operations
4544
#[derive(Debug)]
4645
pub enum FormatError {
4746
/// Error from the underlying jiff library
4847
JiffError(jiff::Error),
4948
/// Field width calculation overflowed or required allocation failed
50-
FieldWidthTooLarge { width: String, specifier: String },
49+
FieldWidthTooLarge { width: usize, specifier: String },
5150
}
5251

5352
impl fmt::Display for FormatError {
5453
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5554
match self {
5655
Self::JiffError(e) => write!(f, "{e}"),
57-
Self::FieldWidthTooLarge { width, specifier } => {
58-
let mut args = FluentArgs::new();
59-
args.set("width", width.clone());
60-
args.set("specifier", specifier.clone());
61-
write!(
62-
f,
63-
"{}",
64-
get_message_with_args("date-error-format-modifier-width-too-large", args)
56+
Self::FieldWidthTooLarge { width, specifier } => write!(
57+
f,
58+
"{}",
59+
translate!(
60+
"date-error-format-modifier-width-too-large",
61+
"width" => width,
62+
"specifier" => specifier
6563
)
66-
}
64+
),
6765
}
6866
}
6967
}
@@ -155,16 +153,7 @@ fn format_with_modifiers(
155153
// Check if this specifier has modifiers
156154
if !flags.is_empty() || !width_str.is_empty() {
157155
// Apply modifiers to the formatted value
158-
let width = if width_str.is_empty() {
159-
0
160-
} else {
161-
width_str
162-
.parse()
163-
.map_err(|_| FormatError::FieldWidthTooLarge {
164-
width: width_str.to_string(),
165-
specifier: spec.to_string(),
166-
})?
167-
};
156+
let width: usize = width_str.parse().unwrap_or(0);
168157
let explicit_width = !width_str.is_empty();
169158
let modified = apply_modifiers(&formatted, flags, width, spec, explicit_width)?;
170159
result.push_str(&modified);
@@ -406,38 +395,14 @@ fn apply_modifiers(
406395
// Zero padding: sign first, then zeros (e.g., "-0022")
407396
let sign = result.chars().next().unwrap();
408397
let rest = &result[1..];
409-
let target_len = result.len().checked_add(padding).ok_or_else(|| {
410-
FormatError::FieldWidthTooLarge {
411-
width: width.to_string(),
412-
specifier: specifier.to_string(),
413-
}
414-
})?;
415-
let mut padded = String::new();
416-
padded
417-
.try_reserve(target_len)
418-
.map_err(|_| FormatError::FieldWidthTooLarge {
419-
width: width.to_string(),
420-
specifier: specifier.to_string(),
421-
})?;
398+
let mut padded = try_alloc_padded(result.len(), padding, effective_width, specifier)?;
422399
padded.push(sign);
423400
padded.extend(std::iter::repeat_n('0', padding));
424401
padded.push_str(rest);
425402
result = padded;
426403
} else {
427404
// Default: pad on the left (e.g., " -22" or " 1999")
428-
let target_len = result.len().checked_add(padding).ok_or_else(|| {
429-
FormatError::FieldWidthTooLarge {
430-
width: width.to_string(),
431-
specifier: specifier.to_string(),
432-
}
433-
})?;
434-
let mut padded = String::new();
435-
padded
436-
.try_reserve(target_len)
437-
.map_err(|_| FormatError::FieldWidthTooLarge {
438-
width: width.to_string(),
439-
specifier: specifier.to_string(),
440-
})?;
405+
let mut padded = try_alloc_padded(result.len(), padding, effective_width, specifier)?;
441406
padded.extend(std::iter::repeat_n(pad_char, padding));
442407
padded.push_str(&result);
443408
result = padded;
@@ -447,6 +412,30 @@ fn apply_modifiers(
447412
Ok(result)
448413
}
449414

415+
/// Allocate a `String` with enough capacity for `current_len + padding`,
416+
/// returning `FieldWidthTooLarge` on arithmetic overflow or allocation failure.
417+
fn try_alloc_padded(
418+
current_len: usize,
419+
padding: usize,
420+
width: usize,
421+
specifier: &str,
422+
) -> Result<String, FormatError> {
423+
let target_len =
424+
current_len
425+
.checked_add(padding)
426+
.ok_or_else(|| FormatError::FieldWidthTooLarge {
427+
width,
428+
specifier: specifier.to_string(),
429+
})?;
430+
let mut s = String::new();
431+
s.try_reserve(target_len)
432+
.map_err(|_| FormatError::FieldWidthTooLarge {
433+
width,
434+
specifier: specifier.to_string(),
435+
})?;
436+
Ok(s)
437+
}
438+
450439
#[cfg(test)]
451440
mod tests {
452441
use super::*;
@@ -734,6 +723,16 @@ mod tests {
734723
}
735724
}
736725

726+
#[test]
727+
fn test_apply_modifiers_width_too_large() {
728+
let err = apply_modifiers("x", "", usize::MAX, "c", true).unwrap_err();
729+
assert!(matches!(
730+
err,
731+
FormatError::FieldWidthTooLarge { width, specifier }
732+
if width == usize::MAX && specifier == "c"
733+
));
734+
}
735+
737736
#[test]
738737
fn test_underscore_flag_without_width() {
739738
// %_m should pad month to default width 2 with spaces
@@ -743,7 +742,8 @@ mod tests {
743742
// %_H should pad hour to default width 2 with spaces
744743
assert_eq!(apply_modifiers("5", "_", 0, "H", false).unwrap(), " 5");
745744
// %_Y should pad year to default width 4 with spaces
746-
assert_eq!(apply_modifiers("1999", "_", 0, "Y", false).unwrap(), "1999"); // already at default width
745+
assert_eq!(apply_modifiers("1999", "_", 0, "Y", false).unwrap(), "1999");
746+
// already at default width
747747
}
748748

749749
#[test]
@@ -793,14 +793,4 @@ mod tests {
793793
"GNU: %_C should produce '19', not ' 19' (default width is 2, not 4)"
794794
);
795795
}
796-
797-
#[test]
798-
fn test_apply_modifiers_width_too_large() {
799-
let err = apply_modifiers("x", "", usize::MAX, "c", true).unwrap_err();
800-
assert!(matches!(
801-
err,
802-
FormatError::FieldWidthTooLarge { width, specifier }
803-
if width == usize::MAX.to_string() && specifier == "c"
804-
));
805-
}
806796
}

tests/by-util/test_date.rs

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2387,6 +2387,47 @@ fn test_date_format_modifier_percent_escape() {
23872387
.stdout_is("%Y=0000001999\n");
23882388
}
23892389

2390+
#[test]
2391+
fn test_date_format_modifier_huge_width_fails_without_abort() {
2392+
// GNU date also exits with failure for extremely large width.
2393+
// Assert exit code only to avoid coupling to implementation-specific error text.
2394+
let format = format!("+%{}c", usize::MAX);
2395+
new_ucmd!().arg(&format).fails().code_is(1);
2396+
}
2397+
2398+
#[test]
2399+
fn test_date_format_large_width_no_oom() {
2400+
// Regression: very large width like %8888888888r caused OOM.
2401+
// GNU caps width to i32::MAX; verify we don't crash.
2402+
// Use a moderate width with a fixed date to check the code path works.
2403+
new_ucmd!()
2404+
.arg("-d")
2405+
.arg("2024-01-01")
2406+
.arg("+%300S")
2407+
.succeeds()
2408+
.stdout_is(format!("{}\n", format_args!("{:0>300}", "00")));
2409+
2410+
// Test with a larger width to exercise the code path without producing
2411+
// gigabytes of output (the original %8888888888r would produce ~2GB).
2412+
new_ucmd!()
2413+
.arg("-d")
2414+
.arg("2024-01-01")
2415+
.arg("+%10000S")
2416+
.succeeds()
2417+
.stdout_is(format!("{}\n", format_args!("{:0>10000}", "00")));
2418+
2419+
// Mixed literal text with multiple width-modified specifiers.
2420+
// 2024-01-01 is Monday (day-of-week 1).
2421+
// %2u → "01", literal "ueuu", %6666u → "1" zero-padded to 6666, literal "-r".
2422+
let expected = format!("01ueuu{}-r\n", format_args!("{:0>6666}", "1"));
2423+
new_ucmd!()
2424+
.arg("-d")
2425+
.arg("2024-01-01")
2426+
.arg("+%2uueuu%6666u-r")
2427+
.succeeds()
2428+
.stdout_is(expected);
2429+
}
2430+
23902431
// Tests for format modifier edge cases (flags without explicit width)
23912432
#[test]
23922433
fn test_date_format_modifier_edge_cases() {
@@ -2507,19 +2548,6 @@ fn test_date_format_modifier_edge_cases() {
25072548
}
25082549
}
25092550

2510-
#[test]
2511-
fn test_date_format_modifier_huge_width_fails_without_abort() {
2512-
// GNU date also exits with failure for extremely large width.
2513-
// Assert exit code only to avoid coupling to implementation-specific error text.
2514-
let formats = [
2515-
format!("+%{}c", usize::MAX),
2516-
"+%184467440737095516160c".into(),
2517-
];
2518-
for format in formats {
2519-
new_ucmd!().arg(&format).fails().code_is(1);
2520-
}
2521-
}
2522-
25232551
// Tests for --debug flag
25242552
#[test]
25252553
fn test_date_debug_basic() {

0 commit comments

Comments
 (0)