Skip to content

Commit 1bb1b98

Browse files
committed
numfmt: reject %f values too large to format exactly
1 parent 6b16cc9 commit 1bb1b98

2 files changed

Lines changed: 61 additions & 4 deletions

File tree

src/uu/numfmt/src/format.rs

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -543,7 +543,7 @@ fn try_format_exact_int_without_suffix_scaling(
543543
value: ParsedNumber,
544544
opts: &TransformOptions,
545545
precision: usize,
546-
) -> Option<String> {
546+
) -> Option<Result<String>> {
547547
if opts.to != Unit::None {
548548
return None;
549549
}
@@ -557,15 +557,46 @@ fn try_format_exact_int_without_suffix_scaling(
557557

558558
let scaled = integer / to_unit;
559559

560-
Some(if precision == 0 {
560+
// reject when formatted output would need 20+ digits
561+
const MAX_FORMATTED: u128 = 10_000_000_000_000_000_000;
562+
let precision_factor = 10_u128.pow(precision.min(19) as u32);
563+
if scaled
564+
.unsigned_abs()
565+
.checked_mul(precision_factor)
566+
.is_none_or(|v| v >= MAX_FORMATTED)
567+
{
568+
let value_sci = format_gnu_scientific(scaled as f64);
569+
return Some(Err(format!(
570+
"value/precision too large to be printed: '{value_sci}/{precision}' (consider using --to)"
571+
)));
572+
}
573+
574+
Some(Ok(if precision == 0 {
561575
scaled.to_string()
562576
} else {
563577
format!(
564578
"{scaled}{}{}",
565579
locale_decimal_separator(),
566580
"0".repeat(precision)
567581
)
568-
})
582+
}))
583+
}
584+
585+
fn format_gnu_scientific(v: f64) -> String {
586+
// 6 significant figures with trimmed trailing zeros and signed exponent
587+
let s = format!("{v:.5e}");
588+
if let Some(e_pos) = s.find('e') {
589+
let (mantissa, rest) = s.split_at(e_pos);
590+
let exp = &rest[1..];
591+
let mantissa = mantissa.trim_end_matches('0').trim_end_matches('.');
592+
if exp.starts_with('-') {
593+
format!("{mantissa}e{exp}")
594+
} else {
595+
format!("{mantissa}e+{exp}")
596+
}
597+
} else {
598+
s
599+
}
569600
}
570601

571602
fn transform_to(
@@ -577,7 +608,7 @@ fn transform_to(
577608
is_precision_specified: bool,
578609
) -> Result<String> {
579610
if let Some(result) = try_format_exact_int_without_suffix_scaling(s, opts, precision) {
580-
return Ok(result);
611+
return result;
581612
}
582613

583614
let s = s.to_f64();

tests/by-util/test_numfmt.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1471,6 +1471,32 @@ fn test_invalid_utf8_input() {
14711471
.stderr_is("numfmt: invalid number: '\\377'\n");
14721472
}
14731473

1474+
#[test]
1475+
fn test_format_value_too_large_issue_11936() {
1476+
// value * 10^precision needing 20+ digits should be rejected
1477+
let cases = [
1478+
(vec!["--format=%5.1f", "1000000000000000000"], "1e+18/1"),
1479+
(vec!["--format=%.2f", "100000000000000000"], "1e+17/2"),
1480+
(vec!["--format=%.3f", "10000000000000000"], "1e+16/3"),
1481+
];
1482+
for (args, hint) in cases {
1483+
new_ucmd!()
1484+
.args(&args)
1485+
.fails_with_code(2)
1486+
.stderr_contains("value/precision too large")
1487+
.stderr_contains(hint);
1488+
}
1489+
}
1490+
1491+
#[test]
1492+
fn test_format_value_below_large_threshold_ok() {
1493+
// one below the cutoff still formats
1494+
new_ucmd!()
1495+
.args(&["--format=%5.1f", "999999999999999999"])
1496+
.succeeds()
1497+
.stdout_is("999999999999999999.0\n");
1498+
}
1499+
14741500
#[test]
14751501
#[cfg_attr(wasi_runner, ignore = "WASI: locale env vars not propagated")]
14761502
fn test_locale_fr_output() {

0 commit comments

Comments
 (0)