date: extract try_alloc_padded helper and add large-width test

Co-authored-by: Sylvestre Ledru <sylvestre@debian.org>
This commit is contained in:
Sylvestre Ledru
2026-04-05 10:13:31 +02:00
parent c81e9d00b9
commit dbdd2ddfda
2 changed files with 90 additions and 72 deletions
+49 -59
View File
@@ -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<String, FormatError> {
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"
));
}
}
+41 -13
View File
@@ -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() {