Skip to content

Commit dac58d9

Browse files
committed
date: handle oversized format modifier widths safely
1 parent cc9b3af commit dac58d9

4 files changed

Lines changed: 117 additions & 34 deletions

File tree

src/uu/date/locales/en-US.ftl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,4 @@ date-error-write = write error: {$error}
109109
date-error-format-missing-plus = the argument {$arg} lacks a leading '+';
110110
when using an option to specify date(s), any non-option
111111
argument must be a format string beginning with '+'
112+
date-error-format-modifier-width-too-large = format modifier width '{$width}' is too large for specifier '%{$specifier}'

src/uu/date/locales/fr-FR.ftl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,5 @@ date-error-write = erreur d'écriture: {$error}
104104
date-error-format-missing-plus = l'argument {$arg} ne commence pas par un signe '+';
105105
lorsqu'une option est utilisée pour spécifier une ou plusieurs dates, tout argument autre
106106
qu'une option doit être une chaîne de format commençant par un signe '+'.
107+
date-error-format-modifier-width-too-large = la largeur du modificateur de format '{$width}' est trop grande pour le spécificateur '%{$specifier}'
108+
date-error-format-modifier-width-too-large = la largeur du modificateur de format '{$width}' est trop grande pour le spécificateur '%{$specifier}'

src/uu/date/src/format_modifiers.rs

Lines changed: 101 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -33,27 +33,39 @@
3333
//! - `%^B`: Month name in uppercase (JUNE)
3434
//! - `%+4C`: Century with sign, padded to 4 characters (+019)
3535
36+
use fluent::FluentArgs;
3637
use jiff::Zoned;
3738
use jiff::fmt::strtime::{BrokenDownTime, Config, PosixCustom};
3839
use regex::Regex;
3940
use std::fmt;
4041
use std::sync::OnceLock;
42+
use uucore::locale::get_message_with_args;
4143

4244
/// Error type for format modifier operations
4345
#[derive(Debug)]
4446
pub enum FormatError {
4547
/// Error from the underlying jiff library
4648
JiffError(jiff::Error),
47-
/// Custom error message (reserved for future use)
48-
#[allow(dead_code)]
49-
Custom(String),
49+
FieldWidthTooLarge {
50+
width: String,
51+
specifier: String,
52+
},
5053
}
5154

5255
impl fmt::Display for FormatError {
5356
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5457
match self {
5558
Self::JiffError(e) => write!(f, "{e}"),
56-
Self::Custom(s) => write!(f, "{s}"),
59+
Self::FieldWidthTooLarge { width, specifier } => {
60+
let mut args = FluentArgs::new();
61+
args.set("width", width.clone());
62+
args.set("specifier", specifier.clone());
63+
write!(
64+
f,
65+
"{}",
66+
get_message_with_args("date-error-format-modifier-width-too-large", args)
67+
)
68+
}
5769
}
5870
}
5971
}
@@ -145,8 +157,15 @@ fn format_with_modifiers(
145157
// Check if this specifier has modifiers
146158
if !flags.is_empty() || !width_str.is_empty() {
147159
// Apply modifiers to the formatted value
148-
let width: usize = width_str.parse().unwrap_or(0);
149-
let modified = apply_modifiers(&formatted, flags, width, spec);
160+
let width = if width_str.is_empty() {
161+
0
162+
} else {
163+
width_str.parse().map_err(|_| FormatError::FieldWidthTooLarge {
164+
width: width_str.to_string(),
165+
specifier: spec.to_string(),
166+
})?
167+
};
168+
let modified = apply_modifiers(&formatted, flags, width, spec)?;
150169
result.push_str(&modified);
151170
} else {
152171
// No modifiers, use formatted value as-is
@@ -203,7 +222,12 @@ fn strip_default_padding(value: &str) -> String {
203222
/// which determines the default padding character (space for text, zero for numeric).
204223
/// Flags are processed in order so that when conflicting flags appear,
205224
/// the last one takes precedence (e.g., `_+` means `+` wins for padding).
206-
fn apply_modifiers(value: &str, flags: &str, width: usize, specifier: &str) -> String {
225+
fn apply_modifiers(
226+
value: &str,
227+
flags: &str,
228+
width: usize,
229+
specifier: &str,
230+
) -> Result<String, FormatError> {
207231
let mut result = value.to_string();
208232

209233
// Determine default pad character based on specifier type
@@ -268,12 +292,12 @@ fn apply_modifiers(value: &str, flags: &str, width: usize, specifier: &str) -> S
268292

269293
// If no_pad flag is active, suppress all padding and return
270294
if no_pad {
271-
return strip_default_padding(&result);
295+
return Ok(strip_default_padding(&result));
272296
}
273297

274298
// Handle width smaller than result: strip default padding to fit
275299
if width > 0 && width < result.len() {
276-
return strip_default_padding(&result);
300+
return Ok(strip_default_padding(&result));
277301
}
278302

279303
// Strip leading zeros when switching to space padding on numeric fields
@@ -301,14 +325,47 @@ fn apply_modifiers(value: &str, flags: &str, width: usize, specifier: &str) -> S
301325
// Zero padding: sign first, then zeros (e.g., "-0022")
302326
let sign = result.chars().next().unwrap();
303327
let rest = &result[1..];
304-
result = format!("{sign}{}{rest}", "0".repeat(padding));
328+
let target_len = result
329+
.len()
330+
.checked_add(padding)
331+
.ok_or_else(|| FormatError::FieldWidthTooLarge {
332+
width: width.to_string(),
333+
specifier: specifier.to_string(),
334+
})?;
335+
let mut padded = String::new();
336+
padded.try_reserve(target_len).map_err(|_| {
337+
FormatError::FieldWidthTooLarge {
338+
width: width.to_string(),
339+
specifier: specifier.to_string(),
340+
}
341+
})?;
342+
padded.push(sign);
343+
padded.extend(std::iter::repeat_n('0', padding));
344+
padded.push_str(rest);
345+
result = padded;
305346
} else {
306347
// Default: pad on the left (e.g., " -22" or " 1999")
307-
result = format!("{}{result}", pad_char.to_string().repeat(padding));
348+
let target_len = result
349+
.len()
350+
.checked_add(padding)
351+
.ok_or_else(|| FormatError::FieldWidthTooLarge {
352+
width: width.to_string(),
353+
specifier: specifier.to_string(),
354+
})?;
355+
let mut padded = String::new();
356+
padded.try_reserve(target_len).map_err(|_| {
357+
FormatError::FieldWidthTooLarge {
358+
width: width.to_string(),
359+
specifier: specifier.to_string(),
360+
}
361+
})?;
362+
padded.extend(std::iter::repeat_n(pad_char, padding));
363+
padded.push_str(&result);
364+
result = padded;
308365
}
309366
}
310367

311-
result
368+
Ok(result)
312369
}
313370

314371
#[cfg(test)]
@@ -488,59 +545,59 @@ mod tests {
488545
#[test]
489546
fn test_apply_modifiers_basic() {
490547
// No modifiers (numeric specifier)
491-
assert_eq!(apply_modifiers("1999", "", 0, "Y"), "1999");
548+
assert_eq!(apply_modifiers("1999", "", 0, "Y").unwrap(), "1999");
492549
// Zero padding
493-
assert_eq!(apply_modifiers("1999", "0", 10, "Y"), "0000001999");
550+
assert_eq!(apply_modifiers("1999", "0", 10, "Y").unwrap(), "0000001999");
494551
// Space padding (strips leading zeros)
495-
assert_eq!(apply_modifiers("06", "_", 5, "m"), " 6");
552+
assert_eq!(apply_modifiers("06", "_", 5, "m").unwrap(), " 6");
496553
// No-pad (strips leading zeros, width ignored)
497-
assert_eq!(apply_modifiers("01", "-", 5, "d"), "1");
554+
assert_eq!(apply_modifiers("01", "-", 5, "d").unwrap(), "1");
498555
// Uppercase
499-
assert_eq!(apply_modifiers("june", "^", 0, "B"), "JUNE");
556+
assert_eq!(apply_modifiers("june", "^", 0, "B").unwrap(), "JUNE");
500557
// Swap case: all uppercase → lowercase
501-
assert_eq!(apply_modifiers("UTC", "#", 0, "Z"), "utc");
558+
assert_eq!(apply_modifiers("UTC", "#", 0, "Z").unwrap(), "utc");
502559
// Swap case: mixed case → uppercase
503-
assert_eq!(apply_modifiers("June", "#", 0, "B"), "JUNE");
560+
assert_eq!(apply_modifiers("June", "#", 0, "B").unwrap(), "JUNE");
504561
}
505562

506563
#[test]
507564
fn test_apply_modifiers_signs() {
508565
// Force sign
509-
assert_eq!(apply_modifiers("1970", "+", 6, "Y"), "+01970");
566+
assert_eq!(apply_modifiers("1970", "+", 6, "Y").unwrap(), "+01970");
510567
// Negative with zero padding: sign first, then zeros
511-
assert_eq!(apply_modifiers("-22", "0", 5, "s"), "-0022");
568+
assert_eq!(apply_modifiers("-22", "0", 5, "s").unwrap(), "-0022");
512569
// Negative with space padding: spaces first, then sign
513-
assert_eq!(apply_modifiers("-22", "_", 5, "s"), " -22");
570+
assert_eq!(apply_modifiers("-22", "_", 5, "s").unwrap(), " -22");
514571
// Force sign (_+): + is last, overrides _ → zero pad with sign
515-
assert_eq!(apply_modifiers("5", "_+", 5, "s"), "+0005");
572+
assert_eq!(apply_modifiers("5", "_+", 5, "s").unwrap(), "+0005");
516573
// No-pad + uppercase: no padding applied
517-
assert_eq!(apply_modifiers("june", "-^", 10, "B"), "JUNE");
574+
assert_eq!(apply_modifiers("june", "-^", 10, "B").unwrap(), "JUNE");
518575
}
519576

520577
#[test]
521578
fn test_case_flag_precedence() {
522579
// Test that ^ (uppercase) overrides # (swap case)
523-
assert_eq!(apply_modifiers("June", "^#", 0, "B"), "JUNE");
524-
assert_eq!(apply_modifiers("June", "#^", 0, "B"), "JUNE");
580+
assert_eq!(apply_modifiers("June", "^#", 0, "B").unwrap(), "JUNE");
581+
assert_eq!(apply_modifiers("June", "#^", 0, "B").unwrap(), "JUNE");
525582
// Test # alone (swap case)
526-
assert_eq!(apply_modifiers("June", "#", 0, "B"), "JUNE");
527-
assert_eq!(apply_modifiers("JUNE", "#", 0, "B"), "june");
583+
assert_eq!(apply_modifiers("June", "#", 0, "B").unwrap(), "JUNE");
584+
assert_eq!(apply_modifiers("JUNE", "#", 0, "B").unwrap(), "june");
528585
}
529586

530587
#[test]
531588
fn test_apply_modifiers_text_specifiers() {
532589
// Text specifiers default to space padding
533-
assert_eq!(apply_modifiers("June", "", 10, "B"), " June");
534-
assert_eq!(apply_modifiers("Mon", "", 10, "a"), " Mon");
590+
assert_eq!(apply_modifiers("June", "", 10, "B").unwrap(), " June");
591+
assert_eq!(apply_modifiers("Mon", "", 10, "a").unwrap(), " Mon");
535592
// Numeric specifiers default to zero padding
536-
assert_eq!(apply_modifiers("6", "", 10, "m"), "0000000006");
593+
assert_eq!(apply_modifiers("6", "", 10, "m").unwrap(), "0000000006");
537594
}
538595

539596
#[test]
540597
fn test_apply_modifiers_width_smaller_than_result() {
541598
// Width smaller than result strips default padding
542-
assert_eq!(apply_modifiers("01", "", 1, "d"), "1");
543-
assert_eq!(apply_modifiers("06", "", 1, "m"), "6");
599+
assert_eq!(apply_modifiers("01", "", 1, "d").unwrap(), "1");
600+
assert_eq!(apply_modifiers("06", "", 1, "m").unwrap(), "6");
544601
}
545602

546603
#[test]
@@ -560,10 +617,20 @@ mod tests {
560617

561618
for (value, flags, width, spec, expected) in test_cases {
562619
assert_eq!(
563-
apply_modifiers(value, flags, width, spec),
620+
apply_modifiers(value, flags, width, spec).unwrap(),
564621
expected,
565622
"value='{value}', flags='{flags}', width={width}, spec='{spec}'",
566623
);
567624
}
568625
}
626+
627+
#[test]
628+
fn test_apply_modifiers_width_too_large() {
629+
let err = apply_modifiers("x", "", usize::MAX, "c").unwrap_err();
630+
assert!(matches!(
631+
err,
632+
FormatError::FieldWidthTooLarge { width, specifier }
633+
if width == usize::MAX.to_string() && specifier == "c"
634+
));
635+
}
569636
}

tests/by-util/test_date.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2414,6 +2414,19 @@ fn test_date_format_modifier_percent_escape() {
24142414
.stdout_is("%Y=0000001999\n");
24152415
}
24162416

2417+
#[test]
2418+
fn test_date_format_modifier_huge_width_fails_without_abort() {
2419+
// GNU date also exits with failure for extreme width values.
2420+
let formats = [format!("+%{}c", usize::MAX), "+%184467440737095516160c".to_string()];
2421+
2422+
for format in formats {
2423+
new_ucmd!()
2424+
.env("TZ", "UTC")
2425+
.args(&["-d", "1999-06-01", format.as_str()])
2426+
.fails();
2427+
}
2428+
}
2429+
24172430
// Tests for --debug flag
24182431
#[test]
24192432
fn test_date_debug_basic() {

0 commit comments

Comments
 (0)