3333//! - `%^B`: Month name in uppercase (JUNE)
3434//! - `%+4C`: Century with sign, padded to 4 characters (+019)
3535
36+ use fluent:: FluentArgs ;
3637use jiff:: Zoned ;
3738use jiff:: fmt:: strtime:: { BrokenDownTime , Config , PosixCustom } ;
3839use regex:: Regex ;
3940use std:: fmt;
4041use std:: sync:: OnceLock ;
42+ use uucore:: locale:: get_message_with_args;
4143
4244/// Error type for format modifier operations
4345#[ derive( Debug ) ]
4446pub 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
5255impl 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}
0 commit comments