Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 52 additions & 6 deletions src/uucore/src/lib/features/format/num_format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -770,15 +797,28 @@ fn write_output(
super::check_width(remaining_width)?;

match alignment {
NumberAlignment::Left => write!(writer, "{sign_indicator}{s:<remaining_width$}"),
NumberAlignment::Left => {
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 => {
Expand All @@ -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())
}
}
}
Expand Down
19 changes: 19 additions & 0 deletions tests/by-util/test_printf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading