From 49e8865e078f19e91369da3a9460f0000fc3bd01 Mon Sep 17 00:00:00 2001 From: pocopepe Date: Sun, 26 Apr 2026 10:08:32 +0000 Subject: [PATCH] numfmt: reject %f values too large to format exactly --- src/uu/numfmt/src/format.rs | 38 ++++++++++++++++++++++++++++++++---- tests/by-util/test_numfmt.rs | 26 ++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/src/uu/numfmt/src/format.rs b/src/uu/numfmt/src/format.rs index fee0cca0850..bf0d4a63cd4 100644 --- a/src/uu/numfmt/src/format.rs +++ b/src/uu/numfmt/src/format.rs @@ -543,7 +543,7 @@ fn try_format_exact_int_without_suffix_scaling( value: ParsedNumber, opts: &TransformOptions, precision: usize, -) -> Option { +) -> Option> { if opts.to != Unit::None { return None; } @@ -557,7 +557,21 @@ fn try_format_exact_int_without_suffix_scaling( let scaled = integer / to_unit; - Some(if precision == 0 { + // reject when formatted output would need 20+ digits + const MAX_FORMATTED: u128 = 10_000_000_000_000_000_000; + let precision_factor = 10_u128.pow(precision.min(19) as u32); + if scaled + .unsigned_abs() + .checked_mul(precision_factor) + .is_none_or(|v| v >= MAX_FORMATTED) + { + let value_sci = format_gnu_scientific(scaled as f64); + return Some(Err(format!( + "value/precision too large to be printed: '{value_sci}/{precision}' (consider using --to)" + ))); + } + + Some(Ok(if precision == 0 { scaled.to_string() } else { format!( @@ -565,7 +579,23 @@ fn try_format_exact_int_without_suffix_scaling( locale_decimal_separator(), "0".repeat(precision) ) - }) + })) +} + +fn format_gnu_scientific(v: f64) -> String { + // 6 significant figures with trimmed trailing zeros and signed exponent + let s = format!("{v:.5e}"); + let Some(e_pos) = s.find('e') else { + return s; + }; + let (mantissa, rest) = s.split_at(e_pos); + let exp = &rest[1..]; + let mantissa = mantissa.trim_end_matches('0').trim_end_matches('.'); + if exp.starts_with('-') { + format!("{mantissa}e{exp}") + } else { + format!("{mantissa}e+{exp}") + } } fn transform_to( @@ -577,7 +607,7 @@ fn transform_to( is_precision_specified: bool, ) -> Result { if let Some(result) = try_format_exact_int_without_suffix_scaling(s, opts, precision) { - return Ok(result); + return result; } let s = s.to_f64(); diff --git a/tests/by-util/test_numfmt.rs b/tests/by-util/test_numfmt.rs index b58fa79c61e..f189371fe3d 100644 --- a/tests/by-util/test_numfmt.rs +++ b/tests/by-util/test_numfmt.rs @@ -1471,6 +1471,32 @@ fn test_invalid_utf8_input() { .stderr_is("numfmt: invalid number: '\\377'\n"); } +#[test] +fn test_format_value_too_large_issue_11936() { + // value * 10^precision needing 20+ digits should be rejected + let cases = [ + (vec!["--format=%5.1f", "1000000000000000000"], "1e+18/1"), + (vec!["--format=%.2f", "100000000000000000"], "1e+17/2"), + (vec!["--format=%.3f", "10000000000000000"], "1e+16/3"), + ]; + for (args, hint) in cases { + new_ucmd!() + .args(&args) + .fails_with_code(2) + .stderr_contains("value/precision too large") + .stderr_contains(hint); + } +} + +#[test] +fn test_format_value_below_large_threshold_ok() { + // one below the cutoff still formats + new_ucmd!() + .args(&["--format=%5.1f", "999999999999999999"]) + .succeeds() + .stdout_is("999999999999999999.0\n"); +} + #[test] #[cfg_attr(wasi_runner, ignore = "WASI: locale env vars not propagated")] fn test_locale_fr_output() {