mirror of
https://github.com/rust-lang/rust.git
synced 2026-04-27 18:57:42 +03:00
float: Fix panic at max exponential precision
Rust's formatting machinery allows precision values of up to u16::MAX.
Exponential formatting works out the number of significant digits to use
by adding one (for the integral digit before the decimal point).
This previously used usize precision, so the maximum validated precision
did not overflow, but in commit fb9ce02976 ("Limit formatting width
and precision to 16 bits.") the precision type was narrowed to u16
without widening that addition first.
As a result an exponential precision value of 65535 is no longer handled
correctly, because the digit count wraps to 0, and thus "{:.65535e}"
panics in flt2dec::to_exact_exp_str with "assertion failed: ndigits >
0". Other formats (and the parser) accept values up to u16::MAX.
A naive fix would be to widen that addition back to usize, but that
still does not properly address 16-bit targets, where usize is only
guaranteed to be able to represent values up to u16::MAX. The real issue
is that this internal API is expressed in the wrong units for the
formatter.
Fix this by changing exact exponential formatting to take fractional
digits internally as well, and compute the temporary significant digit
bound only when sizing the scratch buffer. To support that let's also
make formatted length accounting saturate so that extremely large
rendered outputs do not reintroduce overflows in padding logic.
This preserves the existing intent and keeps FormattingOptions compact
while making formatting work consistently again.
This commit is contained in:
@@ -171,8 +171,7 @@ fn float_to_exponential_common<T>(fmt: &mut Formatter<'_>, num: &T, upper: bool)
|
||||
};
|
||||
|
||||
if let Some(precision) = fmt.options.get_precision() {
|
||||
// 1 integral digit + `precision` fractional digits = `precision + 1` total digits
|
||||
float_to_exponential_common_exact(fmt, num, sign, precision + 1, upper)
|
||||
float_to_exponential_common_exact(fmt, num, sign, precision, upper)
|
||||
} else {
|
||||
float_to_exponential_common_shortest(fmt, num, sign, upper)
|
||||
}
|
||||
|
||||
@@ -2006,7 +2006,13 @@ unsafe fn pad_formatted_parts(&mut self, formatted: &numfmt::Formatted<'_>) -> R
|
||||
// SAFETY: Per the precondition.
|
||||
unsafe { self.write_formatted_parts(&formatted) }
|
||||
} else {
|
||||
let post_padding = self.padding(width - len as u16, Alignment::Right)?;
|
||||
// Padding widths are capped at `u16`, so reaching this branch means
|
||||
// the formatted output is also shorter than `u16::MAX`.
|
||||
let len = match u16::try_from(len) {
|
||||
Ok(len) => len,
|
||||
Err(_) => unreachable!(),
|
||||
};
|
||||
let post_padding = self.padding(width - len, Alignment::Right)?;
|
||||
// SAFETY: Per the precondition.
|
||||
unsafe {
|
||||
self.write_formatted_parts(&formatted)?;
|
||||
|
||||
@@ -244,18 +244,21 @@ fn digits_to_dec_str<'a>(
|
||||
}
|
||||
|
||||
/// Formats the given decimal digits `0.<...buf...> * 10^exp` into the exponential
|
||||
/// form with at least the given number of significant digits. When `upper` is `true`,
|
||||
/// form with at least the given number of fractional digits. When `upper` is `true`,
|
||||
/// the exponent will be prefixed by `E`; otherwise that's `e`. The result is
|
||||
/// stored to the supplied parts array and a slice of written parts is returned.
|
||||
///
|
||||
/// `min_digits` can be less than the number of actual significant digits in `buf`;
|
||||
/// `frac_digits` can be less than the number of actual fractional digits in `buf`;
|
||||
/// it will be ignored and full digits will be printed. It is only used to print
|
||||
/// additional zeroes after rendered digits. Thus, `min_digits == 0` means that
|
||||
/// additional zeroes after rendered digits. Thus, `frac_digits == 0` means that
|
||||
/// it will only print the given digits and nothing else.
|
||||
///
|
||||
/// For example, `buf = b"123", exp = 3, frac_digits = 4` yields the parts for
|
||||
/// `1.2300e2`.
|
||||
fn digits_to_exp_str<'a>(
|
||||
buf: &'a [u8],
|
||||
exp: i16,
|
||||
min_ndigits: usize,
|
||||
frac_digits: usize,
|
||||
upper: bool,
|
||||
parts: &'a mut [MaybeUninit<Part<'a>>],
|
||||
) -> &'a [Part<'a>] {
|
||||
@@ -268,12 +271,22 @@ fn digits_to_exp_str<'a>(
|
||||
parts[n] = MaybeUninit::new(Part::Copy(&buf[..1]));
|
||||
n += 1;
|
||||
|
||||
if buf.len() > 1 || min_ndigits > 1 {
|
||||
// The first generated digit becomes the integral digit, anything after that is already part of
|
||||
// the fractional portion we can emit verbatim.
|
||||
let actual_frac_digits = buf.len() - 1;
|
||||
if actual_frac_digits > 0 || frac_digits > 0 {
|
||||
// Emit a decimal point either when we already have fractional digits or when the requested
|
||||
// precision needs trailing zeroes after the radix point.
|
||||
parts[n] = MaybeUninit::new(Part::Copy(b"."));
|
||||
parts[n + 1] = MaybeUninit::new(Part::Copy(&buf[1..]));
|
||||
n += 2;
|
||||
if min_ndigits > buf.len() {
|
||||
parts[n] = MaybeUninit::new(Part::Zero(min_ndigits - buf.len()));
|
||||
n += 1;
|
||||
if actual_frac_digits > 0 {
|
||||
parts[n] = MaybeUninit::new(Part::Copy(&buf[1..]));
|
||||
n += 1;
|
||||
}
|
||||
if frac_digits > actual_frac_digits {
|
||||
// format_exact exhausted the meaningful digits, so extend the fractional part with
|
||||
// zeroes up to the requested precision.
|
||||
parts[n] = MaybeUninit::new(Part::Zero(frac_digits - actual_frac_digits));
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
@@ -492,7 +505,7 @@ fn estimate_max_buf_len(exp: i16) -> usize {
|
||||
}
|
||||
|
||||
/// Formats given floating point number into the exponential form with
|
||||
/// exactly given number of significant digits. The result is stored to
|
||||
/// exactly given number of fractional digits. The result is stored to
|
||||
/// the supplied parts array while utilizing given byte buffer as a scratch.
|
||||
/// `upper` is used to determine the case of the exponent prefix (`e` or `E`).
|
||||
/// The first part to be rendered is always a `Part::Sign` (which can be
|
||||
@@ -502,8 +515,10 @@ fn estimate_max_buf_len(exp: i16) -> usize {
|
||||
/// It should return the part of the buffer that it initialized.
|
||||
/// You probably would want `strategy::grisu::format_exact` for this.
|
||||
///
|
||||
/// The byte buffer should be at least `ndigits` bytes long unless `ndigits` is
|
||||
/// so large that only the fixed number of digits will be ever written.
|
||||
/// The returned format is `[sign][digit][.fraction?][zero padding?][e|E][exponent]`.
|
||||
///
|
||||
/// The byte buffer should be at least `frac_digits + 1` bytes long unless
|
||||
/// `frac_digits` is so large that only the fixed number of digits will be ever written.
|
||||
/// (The tipping point for `f64` is about 800, so 1000 bytes should be enough.)
|
||||
/// There should be at least 6 parts available, due to the worst case like
|
||||
/// `[+][1][.][2345][e][-][6]`.
|
||||
@@ -511,7 +526,7 @@ pub fn to_exact_exp_str<'a, T, F>(
|
||||
mut format_exact: F,
|
||||
v: T,
|
||||
sign: Sign,
|
||||
ndigits: usize,
|
||||
frac_digits: usize,
|
||||
upper: bool,
|
||||
buf: &'a mut [MaybeUninit<u8>],
|
||||
parts: &'a mut [MaybeUninit<Part<'a>>],
|
||||
@@ -521,7 +536,6 @@ pub fn to_exact_exp_str<'a, T, F>(
|
||||
F: FnMut(&Decoded, &'a mut [MaybeUninit<u8>], i16) -> (&'a [u8], i16),
|
||||
{
|
||||
assert!(parts.len() >= 6);
|
||||
assert!(ndigits > 0);
|
||||
|
||||
let (negative, full_decoded) = decode(v);
|
||||
let sign = determine_sign(sign, &full_decoded, negative);
|
||||
@@ -537,10 +551,10 @@ pub fn to_exact_exp_str<'a, T, F>(
|
||||
Formatted { sign, parts: unsafe { parts[..1].assume_init_ref() } }
|
||||
}
|
||||
FullDecoded::Zero => {
|
||||
if ndigits > 1 {
|
||||
if frac_digits > 0 {
|
||||
// [0.][0000][e0]
|
||||
parts[0] = MaybeUninit::new(Part::Copy(b"0."));
|
||||
parts[1] = MaybeUninit::new(Part::Zero(ndigits - 1));
|
||||
parts[1] = MaybeUninit::new(Part::Zero(frac_digits));
|
||||
parts[2] = MaybeUninit::new(Part::Copy(if upper { b"E0" } else { b"e0" }));
|
||||
Formatted {
|
||||
sign,
|
||||
@@ -558,11 +572,14 @@ pub fn to_exact_exp_str<'a, T, F>(
|
||||
}
|
||||
FullDecoded::Finite(ref decoded) => {
|
||||
let maxlen = estimate_max_buf_len(decoded.exp);
|
||||
assert!(buf.len() >= ndigits || buf.len() >= maxlen);
|
||||
// Scratch space is only needed for the significant digits that `format_exact` can
|
||||
// actually generate. Any remaining requested fractional precision becomes a trailing
|
||||
// `Part::Zero`.
|
||||
let sig_digits = if frac_digits < maxlen { frac_digits + 1 } else { maxlen };
|
||||
assert!(buf.len() >= sig_digits);
|
||||
|
||||
let trunc = if ndigits < maxlen { ndigits } else { maxlen };
|
||||
let (buf, exp) = format_exact(decoded, &mut buf[..trunc], i16::MIN);
|
||||
Formatted { sign, parts: digits_to_exp_str(buf, exp, ndigits, upper, parts) }
|
||||
let (buf, exp) = format_exact(decoded, &mut buf[..sig_digits], i16::MIN);
|
||||
Formatted { sign, parts: digits_to_exp_str(buf, exp, frac_digits, upper, parts) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,9 +68,14 @@ pub struct Formatted<'a> {
|
||||
}
|
||||
|
||||
impl<'a> Formatted<'a> {
|
||||
/// Returns the exact byte length of combined formatted result.
|
||||
/// Returns the byte length of combined formatted result.
|
||||
///
|
||||
/// Saturates at `usize::MAX` if the actual length is larger.
|
||||
///
|
||||
/// This matters on 16-bit targets, where exponential formatting can exceed
|
||||
/// `usize::MAX` by emitting `u16::MAX` trailing zeroes plus `"1."` / `"e0"`.
|
||||
pub fn len(&self) -> usize {
|
||||
self.sign.len() + self.parts.iter().map(|part| part.len()).sum::<usize>()
|
||||
self.parts.iter().fold(self.sign.len(), |len, part| len.saturating_add(part.len()))
|
||||
}
|
||||
|
||||
/// Writes all formatted parts into the supplied buffer.
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use core::fmt::{self, Write};
|
||||
|
||||
#[test]
|
||||
fn test_format_f64() {
|
||||
assert_eq!("1", format!("{:.0}", 1.0f64));
|
||||
@@ -170,6 +172,72 @@ fn test_format_f32_rounds_ties_to_even() {
|
||||
assert_eq!("-1.28E2", format!("{:.2E}", -128.5f32));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_f64_max_precision_exponential() {
|
||||
struct ExactExpWriter {
|
||||
prefix: &'static [u8],
|
||||
zeroes_remaining: u32,
|
||||
suffix: &'static [u8],
|
||||
prefix_pos: usize,
|
||||
suffix_pos: usize,
|
||||
total_len: u32,
|
||||
}
|
||||
|
||||
impl ExactExpWriter {
|
||||
fn new(prefix: &'static str, suffix: &'static str) -> Self {
|
||||
Self {
|
||||
prefix: prefix.as_bytes(),
|
||||
zeroes_remaining: u16::MAX.into(),
|
||||
suffix: suffix.as_bytes(),
|
||||
prefix_pos: 0,
|
||||
suffix_pos: 0,
|
||||
total_len: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(self) {
|
||||
assert_eq!(self.prefix_pos, self.prefix.len());
|
||||
assert_eq!(self.zeroes_remaining, 0);
|
||||
assert_eq!(self.suffix_pos, self.suffix.len());
|
||||
assert_eq!(self.total_len, u32::from(u16::MAX) + 4);
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for ExactExpWriter {
|
||||
fn write_str(&mut self, s: &str) -> fmt::Result {
|
||||
for byte in s.bytes() {
|
||||
self.total_len += 1;
|
||||
|
||||
if self.prefix_pos < self.prefix.len() {
|
||||
assert_eq!(byte, self.prefix[self.prefix_pos]);
|
||||
self.prefix_pos += 1;
|
||||
} else if self.zeroes_remaining > 0 {
|
||||
assert_eq!(byte, b'0');
|
||||
self.zeroes_remaining -= 1;
|
||||
} else {
|
||||
assert!(self.suffix_pos < self.suffix.len());
|
||||
assert_eq!(byte, self.suffix[self.suffix_pos]);
|
||||
self.suffix_pos += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_exact_exp(args: fmt::Arguments<'_>, prefix: &'static str, suffix: &'static str) {
|
||||
let mut writer = ExactExpWriter::new(prefix, suffix);
|
||||
fmt::write(&mut writer, args).unwrap();
|
||||
writer.finish();
|
||||
}
|
||||
|
||||
assert_exact_exp(format_args!("{:.65535e}", 0.0f64), "0.", "e0");
|
||||
assert_exact_exp(format_args!("{:.65535e}", 1.0f64), "1.", "e0");
|
||||
assert_exact_exp(format_args!("{:.65535E}", 0.0f64), "0.", "E0");
|
||||
assert_exact_exp(format_args!("{:.65535E}", 1.0f64), "1.", "E0");
|
||||
assert_exact_exp(format_args!("{:65535.65535e}", 1.0f64), "1.", "e0");
|
||||
}
|
||||
|
||||
fn is_exponential(s: &str) -> bool {
|
||||
s.contains("e") || s.contains("E")
|
||||
}
|
||||
|
||||
@@ -793,7 +793,15 @@ fn to_string<T, F>(f: &mut F, v: T, sign: Sign, ndigits: usize, upper: bool) ->
|
||||
F: for<'a> FnMut(&Decoded, &'a mut [MaybeUninit<u8>], i16) -> (&'a [u8], i16),
|
||||
{
|
||||
to_string_with_parts(|buf, parts| {
|
||||
to_exact_exp_str(|d, b, l| f(d, b, l), v, sign, ndigits, upper, buf, parts)
|
||||
to_exact_exp_str(
|
||||
|d, b, l| f(d, b, l),
|
||||
v,
|
||||
sign,
|
||||
ndigits.saturating_sub(1),
|
||||
upper,
|
||||
buf,
|
||||
parts,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user