diff --git a/src/uucore/src/lib/features/format/num_format.rs b/src/uucore/src/lib/features/format/num_format.rs index b52859de90..68b60f4983 100644 --- a/src/uucore/src/lib/features/format/num_format.rs +++ b/src/uucore/src/lib/features/format/num_format.rs @@ -749,6 +749,33 @@ fn strip_fractional_zeroes_and_dot(s: &mut String) { } } +/// Write `n` zero bytes to `writer`. +/// +/// Like `write_zeros` in spec.rs, this avoids feeding large widths into +/// Rust's `write!` macro, which panics for widths above `u16::MAX`. +fn write_zeros(mut writer: impl Write, n: usize) -> std::io::Result<()> { + const ZEROS: [u8; 64] = [b'0'; 64]; + let mut remaining = n; + while remaining > 0 { + let chunk = remaining.min(ZEROS.len()); + writer.write_all(&ZEROS[..chunk])?; + remaining -= chunk; + } + Ok(()) +} + +/// Write `n` space bytes to `writer`. +fn write_spaces(mut writer: impl Write, n: usize) -> std::io::Result<()> { + const SPACES: [u8; 64] = [b' '; 64]; + let mut remaining = n; + while remaining > 0 { + let chunk = remaining.min(SPACES.len()); + writer.write_all(&SPACES[..chunk])?; + remaining -= chunk; + } + Ok(()) +} + fn write_output( mut writer: impl Write, sign_indicator: String, @@ -770,15 +797,28 @@ fn write_output( super::check_width(remaining_width)?; match alignment { - NumberAlignment::Left => write!(writer, "{sign_indicator}{s: { + writer.write_all(sign_indicator.as_bytes())?; + writer.write_all(s.as_bytes())?; + let pad = remaining_width.saturating_sub(s.len()); + write_spaces(&mut writer, pad) + } NumberAlignment::RightSpace => { let is_sign = sign_indicator.starts_with('-') || sign_indicator.starts_with('+'); // When sign_indicator is in ['-', '+'] if is_sign && remaining_width > 0 { // Make sure sign_indicator is just next to number, e.g. "% +5.1f" 1 ==> $ +1.0 - let s = sign_indicator + s.as_str(); - write!(writer, "{s:>width$}", width = remaining_width + 1) // Since we now add sign_indicator and s together, plus 1 + // remaining_width already excludes sign_indicator.len(), add 1 for the sign + let total_width = remaining_width + 1; + let pad = total_width.saturating_sub(sign_indicator.len() + s.len()); + write_spaces(&mut writer, pad)?; + writer.write_all(sign_indicator.as_bytes())?; + writer.write_all(s.as_bytes()) } else { - write!(writer, "{sign_indicator}{s:>remaining_width$}") + // remaining_width already excludes sign_indicator.len() + let pad = remaining_width.saturating_sub(s.len()); + write_spaces(&mut writer, pad)?; + writer.write_all(sign_indicator.as_bytes())?; + writer.write_all(s.as_bytes()) } } NumberAlignment::RightZero => { @@ -788,8 +828,14 @@ fn write_output( } else { ("", s.as_str()) }; - let remaining_width = remaining_width.saturating_sub(prefix.len()); - write!(writer, "{sign_indicator}{prefix}{rest:0>remaining_width$}") + // remaining_width excludes sign_indicator.len(); subtract prefix and rest + let pad = remaining_width + .saturating_sub(prefix.len()) + .saturating_sub(rest.len()); + writer.write_all(sign_indicator.as_bytes())?; + writer.write_all(prefix.as_bytes())?; + write_zeros(&mut writer, pad)?; + writer.write_all(rest.as_bytes()) } } } diff --git a/tests/by-util/test_printf.rs b/tests/by-util/test_printf.rs index fbdcb47a84..af59e7c66a 100644 --- a/tests/by-util/test_printf.rs +++ b/tests/by-util/test_printf.rs @@ -1573,6 +1573,25 @@ fn test_extreme_field_width_overflow() { .stderr_contains("printf: write error"); //could contains additional message like "formatting width too large" not in GNU, thats fine. } +#[test] +fn test_width_65536_does_not_panic() { + // Regression test for https://github.com/uutils/coreutils/issues/13850 + // Widths above u16::MAX (65535) previously panicked because Rust's + // write! macro uses u16 internally for formatting width. + new_ucmd!().args(&["%65536d", "1"]).succeeds(); +} + +#[test] +fn test_width_above_u16_max_succeeds() { + // Widths above u16::MAX should succeed when output is not a terminal. + // The fix uses chunked space writing instead of the write! macro. + let result = new_ucmd!().args(&["|%65537d|", "1"]).succeeds(); + assert!( + String::from_utf8_lossy(result.stdout()).contains('1'), + "output should contain the formatted value" + ); +} + #[test] fn test_asterisk_width_i64_min_no_panic() { // Regression test for https://github.com/uutils/coreutils/issues/13766