Skip to content

Commit e48dab6

Browse files
committed
numfmt: reject %f values too large to format exactly
1 parent 99d3620 commit e48dab6

2 files changed

Lines changed: 62 additions & 4 deletions

File tree

src/uu/numfmt/src/format.rs

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -539,11 +539,20 @@ fn consider_suffix(
539539
}
540540
}
541541

542+
fn is_too_large_to_format(scaled: i128, precision: usize) -> bool {
543+
const MAX_FORMATTED: u128 = 10_000_000_000_000_000_000;
544+
let precision_factor = 10_u128.pow(precision.min(19) as u32);
545+
scaled
546+
.unsigned_abs()
547+
.checked_mul(precision_factor)
548+
.is_none_or(|v| v >= MAX_FORMATTED)
549+
}
550+
542551
fn try_format_exact_int_without_suffix_scaling(
543552
value: ParsedNumber,
544553
opts: &TransformOptions,
545554
precision: usize,
546-
) -> Option<String> {
555+
) -> Option<Result<String>> {
547556
if opts.to != Unit::None {
548557
return None;
549558
}
@@ -557,15 +566,38 @@ fn try_format_exact_int_without_suffix_scaling(
557566

558567
let scaled = integer / to_unit;
559568

560-
Some(if precision == 0 {
569+
if is_too_large_to_format(scaled, precision) {
570+
let value_sci = format_gnu_scientific(scaled as f64);
571+
return Some(Err(format!(
572+
"value/precision too large to be printed: '{value_sci}/{precision}' (consider using --to)"
573+
)));
574+
}
575+
576+
Some(Ok(if precision == 0 {
561577
scaled.to_string()
562578
} else {
563579
format!(
564580
"{scaled}{}{}",
565581
locale_decimal_separator(),
566582
"0".repeat(precision)
567583
)
568-
})
584+
}))
585+
}
586+
587+
fn format_gnu_scientific(v: f64) -> String {
588+
// 6 significant figures with trimmed trailing zeros and signed exponent
589+
let s = format!("{v:.5e}");
590+
let Some(e_pos) = s.find('e') else {
591+
return s;
592+
};
593+
let (mantissa, rest) = s.split_at(e_pos);
594+
let exp = &rest[1..];
595+
let mantissa = mantissa.trim_end_matches('0').trim_end_matches('.');
596+
if exp.starts_with('-') {
597+
format!("{mantissa}e{exp}")
598+
} else {
599+
format!("{mantissa}e+{exp}")
600+
}
569601
}
570602

571603
fn transform_to(
@@ -577,7 +609,7 @@ fn transform_to(
577609
is_precision_specified: bool,
578610
) -> Result<String> {
579611
if let Some(result) = try_format_exact_int_without_suffix_scaling(s, opts, precision) {
580-
return Ok(result);
612+
return result;
581613
}
582614

583615
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
@@ -1513,6 +1513,32 @@ fn test_invalid_utf8_input() {
15131513
.stderr_is("numfmt: invalid number: '\\377'\n");
15141514
}
15151515

1516+
#[test]
1517+
fn test_format_value_too_large_issue_11936() {
1518+
// value * 10^precision needing 20+ digits should be rejected
1519+
let cases = [
1520+
(vec!["--format=%5.1f", "1000000000000000000"], "1e+18/1"),
1521+
(vec!["--format=%.2f", "100000000000000000"], "1e+17/2"),
1522+
(vec!["--format=%.3f", "10000000000000000"], "1e+16/3"),
1523+
];
1524+
for (args, hint) in cases {
1525+
new_ucmd!()
1526+
.args(&args)
1527+
.fails_with_code(2)
1528+
.stderr_contains("value/precision too large")
1529+
.stderr_contains(hint);
1530+
}
1531+
}
1532+
1533+
#[test]
1534+
fn test_format_value_below_large_threshold_ok() {
1535+
// one below the cutoff still formats
1536+
new_ucmd!()
1537+
.args(&["--format=%5.1f", "999999999999999999"])
1538+
.succeeds()
1539+
.stdout_is("999999999999999999.0\n");
1540+
}
1541+
15161542
#[test]
15171543
#[cfg_attr(wasi_runner, ignore = "WASI: locale env vars not propagated")]
15181544
fn test_locale_fr_output() {

0 commit comments

Comments
 (0)