@@ -47,6 +47,7 @@ macroweave::repeat!(Ty in [u8, u16, u32, u64, usize] {
4747 impl FromStr for ByteSize <Ty > {
4848 type Err = ParseError ;
4949
50+ #[ inline]
5051 fn from_str( s: & str ) -> Result <Self , Self :: Err > {
5152 bsize_from_u64( parse_size( s. as_bytes( ) ) ?)
5253 }
6263 . map_err ( |_| ParseError :: Overflow )
6364}
6465
66+ // Half-ceil transitions occur at (2n + 1) / (2 * multiplier). Supported multipliers factor only
67+ // into 2s and 5s, so every transition terminates in decimal; 2^60 is the worst case at 61 digits.
68+ const MAX_RELEVANT_FRACTION_DIGITS : usize = 61 ;
69+
70+ #[ inline]
6571fn parse_size ( mut src : & [ u8 ] ) -> Result < u64 , ParseError > {
6672 // trim starting and trailing spaces
6773 while let [ b' ' , init @ ..] = src {
@@ -146,13 +152,20 @@ fn parse_size(mut src: &[u8]) -> Result<u64, ParseError> {
146152 let integer_bytes = integer. checked_mul ( multiplier) ;
147153
148154 if let Some ( start) = fraction_start {
155+ let fraction = & src[ start..] ;
156+ let fraction = if fraction. len ( ) > MAX_RELEVANT_FRACTION_DIGITS {
157+ relevant_fraction_prefix ( fraction, multiplier) ?
158+ } else {
159+ fraction
160+ } ;
161+
149162 // Multiply the fraction by the unit multiplier from right to left in base 10.
150- // Once all fractional digits are consumed, carry is the integral byte count and
151- // the last remainder digit determines rounding to the nearest byte.
163+ // Once all digits are consumed, carry is the integral byte count and the last remainder
164+ // digit determines rounding to the nearest byte.
152165 debug_assert ! ( multiplier <= u64 :: MAX / 10 ) ;
153166 let mut carry = 0u64 ;
154167 let mut rounding_digit = 0u64 ;
155- for b in src [ start.. ] . iter ( ) . copied ( ) . rev ( ) {
168+ for b in fraction . iter ( ) . copied ( ) . rev ( ) {
156169 match b {
157170 b'0' ..=b'9' => {
158171 let product = u64:: from ( b - b'0' ) * multiplier + carry;
@@ -174,9 +187,36 @@ fn parse_size(mut src: &[u8]) -> Result<u64, ParseError> {
174187 integer_bytes. ok_or ( ParseError :: Overflow )
175188}
176189
190+ fn relevant_fraction_prefix ( src : & [ u8 ] , multiplier : u64 ) -> Result < & [ u8 ] , ParseError > {
191+ // A half-byte boundary has a terminating decimal representation for every supported unit.
192+ // Digits after that representation cannot change half-ceil rounding, so validate them without
193+ // including them in the multiplication loop below.
194+ let relevant_digits = if multiplier == 1 || !multiplier. is_power_of_two ( ) {
195+ multiplier. ilog10 ( ) as usize + 1
196+ } else {
197+ multiplier. ilog2 ( ) as usize + 1
198+ } ;
199+ let mut digits = 0usize ;
200+ let mut end = src. len ( ) ;
201+ for ( index, b) in src. iter ( ) . copied ( ) . enumerate ( ) {
202+ match b {
203+ b'0' ..=b'9' => {
204+ digits += 1 ;
205+ if digits == relevant_digits {
206+ end = index + 1 ;
207+ }
208+ }
209+ b'_' => { }
210+ _ => return Err ( ParseError :: Malformed ) ,
211+ }
212+ }
213+ Ok ( & src[ ..end] )
214+ }
215+
177216#[ cfg( test) ]
178217mod tests {
179218 use alloc:: format;
219+ use alloc:: string:: String ;
180220 use alloc:: string:: ToString ;
181221
182222 use super :: * ;
@@ -280,6 +320,14 @@ mod tests {
280320 "1.3 42.0 B" ,
281321 "1.3 ... B" ,
282322 "19.aE" ,
323+ concat ! (
324+ "1.0000000000000000000000000000000000000000000000000000000000000000" ,
325+ "x EB" ,
326+ ) ,
327+ concat ! (
328+ "0.0004882812500000000000000000000000000000000000000000000000000000" ,
329+ "x KiB" ,
330+ ) ,
283331 "IB" ,
284332 "iB" ,
285333 "1iB" ,
@@ -312,7 +360,79 @@ mod tests {
312360 assert_eq ! ( "4GiB" . parse:: <ByteSize <u32 >>( ) , Err ( ParseError :: Overflow ) ) ;
313361 }
314362
363+ #[ test]
364+ fn ignores_fractional_digits_that_cannot_affect_half_ceil ( ) {
365+ assert_parse_ok ( "0.499999999999999999999999999999 B" , 0 ) ;
366+ assert_parse_ok ( "0.500000000000000000000000000001 B" , 1 ) ;
367+ assert_parse_ok ( "0.000488281249999999999999999999 KiB" , 0 ) ;
368+ assert_parse_ok ( "0.000488281250000000000000000001 KiB" , 1 ) ;
369+ assert_parse_ok (
370+ "0.000000000000000000433680868994201773602981120347976684570312499999999 EiB" ,
371+ 0 ,
372+ ) ;
373+ assert_parse_ok (
374+ "0.000000000000000000433680868994201773602981120347976684570312500000001 EiB" ,
375+ 1 ,
376+ ) ;
377+ }
378+
379+ fn scale_fraction_reference ( src : & [ u8 ] , multiplier : u64 ) -> u64 {
380+ let mut carry = 0u64 ;
381+ let mut rounding_digit = 0u64 ;
382+ for b in src. iter ( ) . copied ( ) . rev ( ) {
383+ match b {
384+ b'0' ..=b'9' => {
385+ let product = u64:: from ( b - b'0' ) * multiplier + carry;
386+ rounding_digit = product % 10 ;
387+ carry = product / 10 ;
388+ }
389+ b'_' => { }
390+ _ => unreachable ! ( ) ,
391+ }
392+ }
393+ carry + u64:: from ( rounding_digit >= 5 )
394+ }
395+
315396 quickcheck:: quickcheck! {
397+ fn long_fractions_match_full_precision_reference(
398+ unit_index: u8 ,
399+ digits: alloc:: vec:: Vec <u8 >
400+ ) -> bool {
401+ const UNITS : [ ( & str , u64 ) ; 13 ] = [
402+ ( "B" , 1 ) ,
403+ ( "kB" , 1_000 ) ,
404+ ( "MB" , 1_000_000 ) ,
405+ ( "GB" , 1_000_000_000 ) ,
406+ ( "TB" , 1_000_000_000_000 ) ,
407+ ( "PB" , 1_000_000_000_000_000 ) ,
408+ ( "EB" , 1_000_000_000_000_000_000 ) ,
409+ ( "KiB" , 1 << 10 ) ,
410+ ( "MiB" , 1 << 20 ) ,
411+ ( "GiB" , 1 << 30 ) ,
412+ ( "TiB" , 1 << 40 ) ,
413+ ( "PiB" , 1 << 50 ) ,
414+ ( "EiB" , 1 << 60 ) ,
415+ ] ;
416+
417+ let ( unit, multiplier) = UNITS [ usize :: from( unit_index) % UNITS . len( ) ] ;
418+ let mut fraction = String :: with_capacity( 146 ) ;
419+ for index in 0 ..128 {
420+ if index > 0 && index % 7 == 0 {
421+ fraction. push( '_' ) ;
422+ }
423+ let digit = digits
424+ . get( index % digits. len( ) . max( 1 ) )
425+ . copied( )
426+ . unwrap_or( index as u8 )
427+ % 10 ;
428+ fraction. push( char :: from( b'0' + digit) ) ;
429+ }
430+
431+ let expected = scale_fraction_reference( fraction. as_bytes( ) , multiplier) ;
432+ let input = format!( "0.{fraction} {unit}" ) ;
433+ input. parse:: <ByteSize <u64 >>( ) == Ok ( ByteSize :: b( expected) )
434+ }
435+
316436 fn parses_eib_fractions_exactly( whole: u8 , fraction: u64 ) -> bool {
317437 const MULTIPLIER : u128 = 1 << 60 ;
318438 const SCALE : u128 = 1_000_000_000_000_000_000 ;
0 commit comments