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
35 changes: 34 additions & 1 deletion src/uucore/src/lib/features/format/num_format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// spell-checker:ignore bigdecimal prec cppreference
// spell-checker:ignore bigdecimal prec cppreference bignum
//! Utilities for formatting numbers in various formats

use bigdecimal::BigDecimal;
Expand Down Expand Up @@ -593,6 +593,19 @@ fn format_float_hexadecimal(
// gracefully though.
let exp10 = -p;

// Guard against exponents far outside the range of any long-double
// (80-bit: ±4932, 128-bit: ±4932). A huge negative exponent makes
// `margin ≈ -exp10 * 3` enormous, so `frac10 << margin` tries to
// allocate exabytes and aborts. Decimal exponents below −5000 always
// underflow to zero in any long-double implementation (#13222).
if exp10 < -5000 {
return if force_decimal == ForceDecimal::Yes && precision.unwrap_or(0) == 0 {
format!("0x0.{exp_char}+0")
} else {
format!("0x{:.*}{exp_char}+0", precision.unwrap_or(0), 0.0)
};
}

// We want something that looks like this: frac2 * 2^exp2,
// without losing precision.
// frac10 * 10^exp10 = (frac10 * 5^exp10) * 2^exp10 = frac2 * 2^exp2
Expand Down Expand Up @@ -1120,6 +1133,26 @@ mod test {
assert_eq!(f(0.into(), 10), "0x0.000000p+0");
}

#[test]
fn hexadecimal_float_huge_negative_exponent_does_not_oom() {
// Values with a decimal exponent below −5000 underflow to zero in
// any long-double representation. This must not abort with an OOM
// from trying to allocate exabytes in the bignum shift (#13222).
use super::format_float_hexadecimal;
let f = |x| {
format_float_hexadecimal(
&BigDecimal::from_str(x).unwrap(),
None,
Case::Lowercase,
ForceDecimal::No,
)
};
assert_eq!(f("1E-1000000000000000000"), "0x0p+0");
assert_eq!(f("1E-5001"), "0x0p+0");
// Values just inside the limit still compute correctly.
assert_eq!(f("1E-5000"), f("1E-5000")); // must not panic
}

#[test]
fn strip_insignificant_end() {
use super::strip_fractional_zeroes_and_dot;
Expand Down
15 changes: 15 additions & 0 deletions tests/by-util/test_seq.rs
Original file line number Diff line number Diff line change
Expand Up @@ -782,6 +782,21 @@ fn test_parse_error_hex() {
.usage_error("invalid floating point argument: '0xlmnop'");
}

#[test]
fn test_format_a_huge_negative_exponent_does_not_oom() {
// Values with a decimal exponent below −5000 underflow to zero in any
// long-double implementation and must not abort with OOM (#13222).
// Use 1E-5001 rather than 1E-1000000000000000000: the latter would
// hang in BigDecimal arithmetic (10^18-digit integers) before even
// reaching the formatter. The unit test in num_format.rs covers the
// extreme value directly.
// spell-checker:ignore bignum
new_ucmd!()
.args(&["-f", "%a", "1E-5001", "1"])
.succeeds()
.stdout_contains("0x0p+0");
}

#[test]
fn test_format_option() {
new_ucmd!()
Expand Down