From dc1e40b52933906f7f73b4b3b100ab4bb14fd3c5 Mon Sep 17 00:00:00 2001 From: Alexander Kireev Date: Fri, 3 Jul 2026 08:55:33 +0700 Subject: [PATCH] fix: ideal_unit_std can disagree with the no_std path near power-of-unit boundaries f64::ln() isn't precise enough to trust right at a 1024^k or 1000^k boundary, so the std Display path could pick a different unit exponent than the no_std loop for the exact same byte count (e.g. 1125899906842623 bytes prints as 1.0 PiB with std but 1024.0 TiB without it), and in the worst case the assert on the ln()-derived exponent could fail and panic (see #142). Nudges the ln() approximation to the exact boundary with a couple of integer-exact powi checks instead of trusting it outright. Added tests covering the boundary directly. --- src/display.rs | 86 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 78 insertions(+), 8 deletions(-) diff --git a/src/display.rs b/src/display.rs index 0cb2e62e..e0854dda 100644 --- a/src/display.rs +++ b/src/display.rs @@ -136,7 +136,7 @@ impl fmt::Display for Display { let size = bytes as f64; #[cfg(feature = "std")] - let exp = ideal_unit_std(size, unit_base); + let exp = ideal_unit_std(size, unit, unit_base); #[cfg(not(feature = "std"))] let exp = ideal_unit_no_std(size, unit); @@ -175,13 +175,25 @@ fn ideal_unit_no_std(size: f64, unit: u64) -> usize { #[cfg(feature = "std")] #[allow(dead_code)] // used in std contexts -fn ideal_unit_std(size: f64, unit_base: f64) -> usize { - assert!(size.ln() >= unit_base, "only called when bytes >= unit"); +fn ideal_unit_std(size: f64, unit: u64, unit_base: f64) -> usize { + assert!(size >= unit as f64, "only called when bytes >= unit"); + + // `ln()` is a fast approximation, but it's not precise enough to trust at power-of-`unit` + // boundaries: `f64::ln` can round such that `size.ln() / unit_base` lands one exponent above + // or below the correct value (see #142), which previously could underflow `exp - 1` and panic, + // or silently pick the wrong unit prefix. Nudge the approximation to the exact boundary using + // integer-exact `powi` checks, matching the (slower but exact) loop in `ideal_unit_no_std`. + let unit = unit as f64; + let mut exp = ((size.ln() / unit_base) as isize).max(1); - match (size.ln() / unit_base) as usize { - 0 => unreachable!(), - e => e, + while exp > 1 && size / unit.powi(exp as i32 - 1) < unit { + exp -= 1; + } + while size / unit.powi(exp as i32) >= unit { + exp += 1; } + + exp as usize } #[cfg(test)] @@ -200,7 +212,7 @@ mod tests { let size = bytes.0 as f64; - ideal_unit_std(size, crate::LN_KIB) == ideal_unit_no_std(size, crate::KIB) + ideal_unit_std(size, crate::KIB, crate::LN_KIB) == ideal_unit_no_std(size, crate::KIB) } #[test] @@ -211,10 +223,68 @@ mod tests { let size = bytes.0 as f64; - ideal_unit_std(size, crate::LN_KB) == ideal_unit_no_std(size, crate::KB) + ideal_unit_std(size, crate::KB, crate::LN_KB) == ideal_unit_no_std(size, crate::KB) + } + } + + // Regression test for #142 / the `std` vs `no_std` display divergence: `f64::ln()` isn't + // precise enough to trust right at a power-of-`unit` boundary, so `ideal_unit_std` used to + // disagree with the exact, loop-based `ideal_unit_no_std` for sizes just below 1024^5 bytes + // (and the equivalent 1000^5 boundary for SI units) — previously "1.0 PiB" under the `std` + // feature vs "1024.0 TiB" without it, for the exact same byte count. + #[cfg(feature = "std")] + #[test] + fn ideal_unit_std_matches_no_std_near_pebi_boundary() { + for bytes in [ + 1_125_899_906_842_621u64, // 1024^5 - 3 + 1_125_899_906_842_622, // 1024^5 - 2 + 1_125_899_906_842_623, // 1024^5 - 1 + 1_125_899_906_842_624, // 1024^5 exactly + ] { + let size = bytes as f64; + assert_eq!( + ideal_unit_std(size, crate::KIB, crate::LN_KIB), + ideal_unit_no_std(size, crate::KIB), + "mismatch at {bytes} bytes (IEC)", + ); + } + + for bytes in [ + 999_999_999_999_996u64, // 1000^5 - 4 + 999_999_999_999_997, // 1000^5 - 3 + 999_999_999_999_998, // 1000^5 - 2 + 999_999_999_999_999, // 1000^5 - 1 + 1_000_000_000_000_000, // 1000^5 exactly + ] { + let size = bytes as f64; + assert_eq!( + ideal_unit_std(size, crate::KB, crate::LN_KB), + ideal_unit_no_std(size, crate::KB), + "mismatch at {bytes} bytes (SI)", + ); } } + #[test] + fn display_matches_just_below_pebi_boundary() { + assert_eq!( + "1024.0 TiB", + Display { + byte_size: ByteSize(1_125_899_906_842_623), + format: Format::Iec, + } + .to_string() + ); + assert_eq!( + "1.0 PiB", + Display { + byte_size: ByteSize(1_125_899_906_842_624), + format: Format::Iec, + } + .to_string() + ); + } + #[test] fn to_string_iec() { let display = Display {