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
5 changes: 3 additions & 2 deletions src/uu/seq/src/numberparse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -373,8 +373,9 @@ mod tests {

#[test]
fn test_parse_max_exponents() {
// Make sure exponents much bigger than i64::MAX cause errors
assert!("1e9223372036854775807".parse::<PreciseNumber>().is_ok());
// Exponents at or beyond i64::MAX cause errors, matching GNU seq
// (e.g. `seq -w 1e9223372036854775807 1` fails on GNU coreutils too).
assert!("1e9223372036854775807".parse::<PreciseNumber>().is_err());
assert!("1e92233720368547758070".parse::<PreciseNumber>().is_err());
}
}
9 changes: 6 additions & 3 deletions src/uu/seq/src/seq.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,15 +165,18 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {

let padding = if options.equal_width {
let precision_value = precision.unwrap_or(0);
// Saturate rather than overflow: a value with an astronomically large
// exponent makes `num_integral_digits` near `usize::MAX`. The resulting
// width is rejected later by the formatter's width check.
first
.num_integral_digits
.max(increment.num_integral_digits)
.max(last.num_integral_digits)
+ if precision_value > 0 {
precision_value + 1
.saturating_add(if precision_value > 0 {
precision_value.saturating_add(1)
} else {
0
}
})
} else {
0
};
Expand Down
38 changes: 38 additions & 0 deletions src/uucore/src/lib/features/format/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,18 @@ fn check_width(width: usize) -> std::io::Result<()> {
}
}

/// Reject a precision larger than printf/C allows (`i32::MAX`).
///
/// A precision near `usize::MAX` would otherwise overflow the precision/exponent
/// arithmetic in the float formatters, so we cap it the way C `printf` does.
pub(crate) fn check_precision(precision: usize) -> Result<(), FormatError> {
if precision > i32::MAX as usize {
Err(FormatError::InvalidPrecision(precision.to_string()))
} else {
Ok(())
}
}

/// A single item to format
pub enum FormatItem<C: FormatChar> {
/// A format specifier
Expand Down Expand Up @@ -410,3 +422,29 @@ impl<F: Formatter<T>, T> Format<F, T> {
Ok(())
}
}

#[cfg(test)]
mod tests {
use super::{FormatError, check_precision};

#[test]
fn check_precision_caps_at_i32_max() {
// Anything up to i32::MAX is accepted.
assert!(check_precision(0).is_ok());
assert!(check_precision(42).is_ok());
assert!(check_precision(i32::MAX as usize).is_ok());

// Anything above i32::MAX is rejected, reporting the offending value.
let precision = i32::MAX as usize + 1;
match check_precision(precision) {
Err(FormatError::InvalidPrecision(reported)) => {
assert_eq!(reported, precision.to_string());
}
other => panic!("expected InvalidPrecision, got {other:?}"),
}
assert!(matches!(
check_precision(usize::MAX),
Err(FormatError::InvalidPrecision(_))
));
}
}
37 changes: 32 additions & 5 deletions 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 bignums
//! Utilities for formatting numbers in various formats

use bigdecimal::BigDecimal;
Expand All @@ -13,7 +13,7 @@ use std::cmp::min;
use std::io::Write;

use super::{
ExtendedBigDecimal, FormatError,
ExtendedBigDecimal, FormatError, check_precision,
spec::{CanAsterisk, Spec},
};

Expand Down Expand Up @@ -257,6 +257,29 @@ impl Formatter<&ExtendedBigDecimal> for Float {

let mut alignment = self.alignment;

// The formatters below do arithmetic on the decimal exponent (negating it,
// scaling by the precision, shifting bignums by it), which would overflow
// for a scale outside `i32`. The parser already rejects such magnitudes as
// overflow/underflow, so this only guards values built without going
// through it; treat them the same way a real float would (as +-inf/0)
// rather than erroring.
if let ExtendedBigDecimal::BigDecimal(bd) = &abs {
if i32::try_from(bd.fractional_digit_count()).is_err() {
let overflow = bd.fractional_digit_count().is_negative();
let abs = if overflow {
ExtendedBigDecimal::Infinity
} else {
ExtendedBigDecimal::zero()
};
if alignment == NumberAlignment::RightZero {
alignment = NumberAlignment::RightSpace;
}
let s = format_float_non_finite(&abs, self.case);
let sign_indicator = get_sign_indicator(self.positive_sign, negative);
return write_output(writer, sign_indicator, s, self.width, alignment);
}
}

let s = if let ExtendedBigDecimal::BigDecimal(bd) = abs {
match self.variant {
FloatVariant::Decimal => {
Expand Down Expand Up @@ -314,6 +337,10 @@ impl Formatter<&ExtendedBigDecimal> for Float {
Some(CanAsterisk::Asterisk(_)) => return Err(FormatError::WrongSpecType),
};

if let Some(precision) = precision {
check_precision(precision)?;
}

Ok(Self {
variant,
case,
Expand Down Expand Up @@ -618,7 +645,7 @@ fn format_float_hexadecimal(
// (since 5^-exp10 < 8^-exp10), so we add that, and another bit for
// rounding.
let margin =
((max_precision + 1) as i64 * 4 - frac10.bits() as i64).max(0) + -exp10 * 3 + 1;
((max_precision as i64 + 1) * 4 - frac10.bits() as i64).max(0) + -exp10 * 3 + 1;

// frac10 * 10^exp10 = frac10 * 2^margin * 10^exp10 * 2^-margin =
// (frac10 * 2^margin * 5^exp10) * 2^exp10 * 2^-margin =
Expand All @@ -631,7 +658,7 @@ fn format_float_hexadecimal(

// Emulate x86(-64) behavior, we display 4 binary digits before the decimal point,
// so the value will always be between 0x8 and 0xf.
let wanted_bits = (BEFORE_BITS + max_precision * 4) as u64;
let wanted_bits = BEFORE_BITS as u64 + max_precision as u64 * 4;
let bits = frac2.bits();

exp2 += bits as i64 - wanted_bits as i64;
Expand Down Expand Up @@ -661,7 +688,7 @@ fn format_float_hexadecimal(
digits.make_ascii_uppercase();
}
let (first_digit, remaining_digits) = digits.split_at(1);
let exponent = exp2 + (4 * max_precision) as i64;
let exponent = exp2 + 4 * max_precision as i64;

let mut remaining_digits = remaining_digits.to_string();
if precision.is_none() {
Expand Down
16 changes: 5 additions & 11 deletions src/uucore/src/lib/features/format/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
// spell-checker:ignore (vars) intmax ptrdiff padlen

use super::{
ExtendedBigDecimal, FormatChar, FormatError, OctalParsing,
ExtendedBigDecimal, FormatChar, FormatError, OctalParsing, check_precision,
num_format::{
self, Case, FloatVariant, ForceDecimal, Formatter, NumberAlignment, PositiveSign, Prefix,
UnsignedIntVariant,
Expand Down Expand Up @@ -417,9 +417,7 @@ impl Spec {
let precision = resolve_asterisk_precision(*precision, args).unwrap_or_default();
let i = args.next_i64(*position);

if precision as u64 > i32::MAX as u64 {
return Err(FormatError::InvalidPrecision(precision.to_string()));
}
check_precision(precision)?;

num_format::SignedInt {
width,
Expand All @@ -445,9 +443,7 @@ impl Spec {
let precision = resolve_asterisk_precision(*precision, args).unwrap_or_default();
let i = args.next_u64(*position);

if precision as u64 > i32::MAX as u64 {
return Err(FormatError::InvalidPrecision(precision.to_string()));
}
check_precision(precision)?;

num_format::UnsignedInt {
variant: *variant,
Expand Down Expand Up @@ -476,10 +472,8 @@ impl Spec {
let precision = resolve_asterisk_precision(*precision, args);
let f: ExtendedBigDecimal = args.next_extended_big_decimal(*position);

if precision.is_some_and(|p| p as u64 > i32::MAX as u64) {
return Err(FormatError::InvalidPrecision(
precision.unwrap().to_string(),
));
if let Some(precision) = precision {
check_precision(precision)?;
}

num_format::Float {
Expand Down
10 changes: 7 additions & 3 deletions src/uucore/src/lib/features/parser/num_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -425,10 +425,14 @@ fn construct_extended_big_decimal(
} else {
let new_scale = -exponent + scale;

// BigDecimal "only" supports i64 scale.
// BigDecimal supports an i64 scale, but a magnitude anywhere near that
// range is already many orders beyond any real floating-point type (a
// long double tops out around 10^±4932), and formatting it would mean
// materializing quintillions of digits. Cap the scale at `i32` so such
// values are treated as overflow/underflow, like a real float would.
// Note that new_scale is a negative exponent: large positive value causes an underflow, large negative values an overflow.
if let Some(new_scale) = new_scale.to_i64() {
BigDecimal::from_bigint(signed_digits, new_scale)
if let Some(new_scale) = new_scale.to_i32() {
BigDecimal::from_bigint(signed_digits, new_scale.into())
} else {
return Err(make_error(new_scale.is_negative(), negative));
}
Expand Down
29 changes: 29 additions & 0 deletions tests/by-util/test_printf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -801,6 +801,35 @@ fn test_overflow() {
.stderr_is("printf: '36893488147419103232': Numerical result out of range\n");
}

#[test]
fn test_extreme_exponent_does_not_overflow() {
// A value whose decimal exponent does not fit in an i32 used to overflow the
// exponent arithmetic in the float formatters (panic in debug, wrong value or
// bad allocation in release). GNU printf treats such a magnitude the same
// way it would treat any other out-of-range float: it reports the overflow
// on stderr but still prints inf (or 0, on underflow) to stdout.
for spec in ["%a", "%e", "%g", "%f"] {
new_ucmd!()
.args(&[spec, "5e8123456789012345678"])
.fails_with_code(1)
.stderr_contains("Numerical result out of range")
.stdout_contains("inf");

let zero = match spec {
"%a" => "0x0p+0",
"%e" => "0.000000e+00",
"%g" => "0",
"%f" => "0.000000",
_ => unreachable!(),
};
new_ucmd!()
.args(&[spec, "7E-8123456789012345678"])
.fails_with_code(1)
.stderr_contains("Numerical result out of range")
.stdout_is(zero);
}
}

#[test]
fn partial_char() {
new_ucmd!()
Expand Down
43 changes: 43 additions & 0 deletions tests/by-util/test_seq.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,49 @@ fn test_format_and_equal_width() {
.stderr_contains("format string may not be specified");
}

#[test]
fn test_format_precision_too_large() {
// A precision near usize::MAX used to overflow the precision arithmetic
// shared with the float formatters; it must be rejected as invalid.
for spec in ["%.18446744073709551615e", "%.9999999999999999999a"] {
new_ucmd!()
.args(&[format!("--format={spec}"), "1".to_string()])
.fails_with_code(1)
.no_stdout()
.stderr_contains("invalid precision");
}
}

#[test]
fn test_format_extreme_exponent_does_not_overflow() {
// A value with an exponent that does not fit in an i32 used to overflow the
// hex-float exponent math. GNU seq rejects such astronomically large
// magnitudes outright (unlike printf, which tolerates them as inf/0), so it
// must now fail cleanly with a parse error instead of panicking.
new_ucmd!()
.args(&[
"--format=%a",
"5e8123456789012345678",
"5e8123456789012345678",
])
.fails_with_code(1)
.no_stdout()
.usage_error("invalid floating point argument: '5e8123456789012345678'");
}

#[test]
fn test_equal_width_huge_exponent_does_not_overflow() {
// Equal-width padding adds the integral-digit count to the precision; a value
// with an astronomically large exponent used to overflow that arithmetic.
// GNU seq rejects such magnitudes as an invalid argument rather than
// accepting them, so it must fail cleanly instead of panicking or succeeding.
new_ucmd!()
.args(&["-w", "1e9223372036854775807", "1e-9223372036854775807", "1"])
.fails_with_code(1)
.no_stdout()
.usage_error("invalid floating point argument: '1e9223372036854775807'");
}
Comment on lines +64 to +74

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this test is incorrect because GNU seq fails:

$ seq -w 1e9223372036854775807 1e-9223372036854775807 1
seq: invalid floating point argument: ‘1e9223372036854775807’
Try 'seq --help' for more information.
$ echo $?
1


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