Skip to content
Open
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
39 changes: 35 additions & 4 deletions src/uu/numfmt/src/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -543,7 +543,7 @@ 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;
}
Expand All @@ -557,15 +557,46 @@ 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!(
"{scaled}{}{}",
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}");
if let Some(e_pos) = s.find('e') {
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}")
}
} else {
s
}
}

fn transform_to(
Expand All @@ -577,7 +608,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();
Expand Down
26 changes: 26 additions & 0 deletions tests/by-util/test_numfmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Loading