numfmt: reject %f values too large to format exactly

This commit is contained in:
pocopepe
2026-05-01 09:40:58 +02:00
committed by Daniel Hofstetter
parent 99d362039b
commit 375fc3cd48
2 changed files with 62 additions and 4 deletions
+36 -4
View File
@@ -539,11 +539,20 @@ fn consider_suffix(
}
}
fn is_too_large_to_format(scaled: i128, precision: usize) -> bool {
const MAX_FORMATTED: u128 = 10_000_000_000_000_000_000;
let precision_factor = 10_u128.pow(precision.min(19) as u32);
scaled
.unsigned_abs()
.checked_mul(precision_factor)
.is_none_or(|v| v >= MAX_FORMATTED)
}
fn try_format_exact_int_without_suffix_scaling(
value: ParsedNumber,
opts: &TransformOptions,
precision: usize,
) -> Option<String> {
) -> Option<Result<String>> {
if opts.to != Unit::None {
return None;
}
@@ -557,7 +566,14 @@ fn try_format_exact_int_without_suffix_scaling(
let scaled = integer / to_unit;
Some(if precision == 0 {
if is_too_large_to_format(scaled, precision) {
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 +581,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 +609,7 @@ fn transform_to(
is_precision_specified: bool,
) -> Result<String> {
if let Some(result) = try_format_exact_int_without_suffix_scaling(s, opts, precision) {
return Ok(result);
return result;
}
let s = s.to_f64();
+26
View File
@@ -1513,6 +1513,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() {