Skip to main content

pretty/
sizes.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//! Rust implementation of sizes formatting and parsing, matching zircon/system/ulib/pretty.
6
7use core::fmt::Write;
8
9/// Units for formatting byte sizes.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11#[repr(u8)]
12pub enum SizeUnit {
13    Auto = 0, // Automatically select an appropriate unit.
14    Bytes = b'B',
15    KiB = b'K',
16    MiB = b'M',
17    GiB = b'G',
18    TiB = b'T',
19    PiB = b'P',
20    EiB = b'E',
21}
22
23impl TryFrom<u8> for SizeUnit {
24    type Error = ();
25    fn try_from(value: u8) -> Result<Self, Self::Error> {
26        match value.to_ascii_uppercase() {
27            b'B' => Ok(SizeUnit::Bytes),
28            b'K' => Ok(SizeUnit::KiB),
29            b'M' => Ok(SizeUnit::MiB),
30            b'G' => Ok(SizeUnit::GiB),
31            b'T' => Ok(SizeUnit::TiB),
32            b'P' => Ok(SizeUnit::PiB),
33            b'E' => Ok(SizeUnit::EiB),
34            0 => Ok(SizeUnit::Auto),
35            _ => Err(()),
36        }
37    }
38}
39
40impl TryFrom<char> for SizeUnit {
41    type Error = ();
42    fn try_from(value: char) -> Result<Self, Self::Error> {
43        if value.is_ascii() { Self::try_from(value as u8) } else { Err(()) }
44    }
45}
46
47impl SizeUnit {
48    pub fn to_str(self) -> &'static str {
49        match self {
50            SizeUnit::Auto => "",
51            SizeUnit::Bytes => "B",
52            SizeUnit::KiB => "K",
53            SizeUnit::MiB => "M",
54            SizeUnit::GiB => "G",
55            SizeUnit::TiB => "T",
56            SizeUnit::PiB => "P",
57            SizeUnit::EiB => "E",
58        }
59    }
60}
61
62// A buffer size (including trailing NUL in C, but here just capacity)
63// that's large enough for any value formatted by format_size_fixed().
64// In C++: sizeof("18446744073709551616B") = 22.
65pub const MAX_FORMAT_SIZE_LEN: usize = 22;
66
67struct SliceWriter<'a> {
68    slice: &'a mut [u8],
69    cursor: usize,
70}
71
72impl<'a> SliceWriter<'a> {
73    fn new(slice: &'a mut [u8]) -> Self {
74        Self { slice, cursor: 0 }
75    }
76
77    fn into_str(self) -> &'a str {
78        core::str::from_utf8(&self.slice[..self.cursor]).unwrap_or("")
79    }
80}
81
82impl<'a> core::fmt::Write for SliceWriter<'a> {
83    fn write_str(&mut self, s: &str) -> core::fmt::Result {
84        let bytes = s.as_bytes();
85        let keep = core::cmp::min(bytes.len(), self.slice.len() - self.cursor);
86        self.slice[self.cursor..self.cursor + keep].copy_from_slice(&bytes[..keep]);
87        self.cursor += keep;
88        Ok(())
89    }
90}
91
92// Calculate "n / d" as an integer, rounding any fractional part.
93//
94// The often-used expression "(n + (d / 2)) / d" can't be used due to
95// potential overflow.
96fn rounding_divide(n: usize, d: usize) -> usize {
97    // If `n` is half way to the next multiple of `d`, we want to round up.
98    // Otherwise, we truncate.
99    let round_up = (n % d) >= (d / 2);
100    n / d + if round_up { 1 } else { 0 }
101}
102
103/// Formats |bytes| as a human-readable string like "123.4k".
104/// Units are in powers of 1024, so "K" is technically "kiB", etc.
105/// Values smaller than "K" have the suffix "B".
106///
107/// Exact multiples of a unit are displayed without a decimal;
108/// e.g., "17K" means the value is exactly 17 * 1024.
109///
110/// Otherwise, a decimal is present; e.g., "17.0K" means the value
111/// is (17 * 1024) +/- epsilon.
112///
113/// |unit| is the unit to use, as a u8 character (e.g. b'B', b'K', etc.).
114/// If zero, picks a natural unit for the size, ensuring at most four whole decimal places.
115/// If |unit| is unknown, the output will have a '?' prefix but otherwise
116/// behave the same as |unit==0|.
117pub fn format_size_fixed_rs(buf: &mut [u8], bytes: usize, mut unit: u8) -> &str {
118    if buf.is_empty() {
119        return "";
120    }
121
122    let mut writer = SliceWriter::new(buf);
123    let units = b"BKMGTPE";
124    let num_units = units.len();
125
126    let orig_bytes = bytes;
127    let mut prepended_question = false;
128
129    let mut bytes = bytes;
130
131    loop {
132        let mut ui = 0;
133        let mut divisor = 1;
134
135        // If we have a fixed (non-zero) unit, divide until we hit it.
136        //
137        // Otherwise, divide until we reach a unit that can express the value
138        // with 4 or fewer whole digits.
139        // - If we can express the value without a fraction (it's a whole
140        //   kibi/mebi/gibibyte), use the largest possible unit (e.g., favor
141        //   "1M" over "1024K").
142        // - Otherwise, favor more whole digits to retain precision (e.g.,
143        //   favor "1025K" or "1025.0K" over "1.0M").
144        while if unit != 0 {
145            ui < num_units && units[ui] != unit
146        } else {
147            bytes >= 10000 || (bytes != 0 && (bytes & 1023) == 0)
148        } {
149            ui += 1;
150            if ui >= num_units {
151                // We probably got an unknown unit. Fall back to a natural unit,
152                // but leave a hint that something's wrong.
153                if !prepended_question {
154                    let _ = writer.write_char('?');
155                    prepended_question = true;
156                }
157                unit = 0;
158                bytes = orig_bytes;
159                break;
160            }
161            bytes /= 1024;
162            divisor *= 1024;
163        }
164
165        if ui < num_units {
166            // If the chosen divisor divides the input value evenly, don't print out a
167            // fractional part.
168            if orig_bytes.is_multiple_of(divisor) {
169                let _ = core::write!(&mut writer, "{}{}", bytes, units[ui] as char);
170            } else {
171                // We don't have an exact number, so print one unit of precision.
172                //
173                // Ideally we could just calculate:
174                //
175                //   sprintf("%0.1f\n", (double)orig_bytes / divisor)
176                //
177                // but want to avoid floating point. Instead, we separately calculate the
178                // two parts using integer arithmetic.
179                let mut int_part = orig_bytes / divisor;
180                let mut fractional_part = rounding_divide((orig_bytes % divisor) * 10, divisor);
181                if fractional_part >= 10 {
182                    // the fractional part rounded up to 10: carry it over to the integer part.
183                    fractional_part = 0;
184                    int_part += 1;
185                }
186                let _ = core::write!(
187                    &mut writer,
188                    "{}.{}{}",
189                    int_part,
190                    fractional_part,
191                    units[ui] as char
192                );
193            }
194            break;
195        }
196    }
197
198    let s = writer.into_str();
199    assert!(s.len() <= MAX_FORMAT_SIZE_LEN);
200    s
201}
202
203/// Calls format_size_fixed_rs() with unit=0, picking a natural unit for the size.
204pub fn format_size_rs(buf: &mut [u8], bytes: usize) -> &str {
205    format_size_fixed_rs(buf, bytes, 0)
206}
207
208/// FormattedBytes is an inline buffer suitable for containing formatted byte sizes.
209///
210/// Matches C++ `pretty::FormattedBytes`.
211#[derive(Clone, Copy)]
212pub struct FormattedBytes {
213    buf: [u8; MAX_FORMAT_SIZE_LEN],
214    len: usize,
215}
216
217impl Default for FormattedBytes {
218    fn default() -> Self {
219        Self::empty()
220    }
221}
222
223impl FormattedBytes {
224    /// Construct an empty formatted byte size buffer.
225    pub const fn empty() -> Self {
226        Self { buf: [0u8; MAX_FORMAT_SIZE_LEN], len: 0 }
227    }
228
229    /// Construct a string representing the given size, choosing an appropriate unit automatically.
230    pub fn new(bytes: usize) -> Self {
231        let mut res = Self::empty();
232        res.set_size(bytes);
233        res
234    }
235
236    /// Construct a string representing the given size, using the given unit.
237    pub fn with_unit(bytes: usize, unit: SizeUnit) -> Self {
238        let mut res = Self::empty();
239        res.set_size_with_unit(bytes, unit);
240        res
241    }
242
243    /// Update the string to the given size, choosing an appropriate unit automatically.
244    pub fn set_size(&mut self, bytes: usize) -> &mut Self {
245        let s = format_size_rs(&mut self.buf, bytes);
246        self.len = s.len();
247        self
248    }
249
250    /// Update the string to the given size, using the given unit.
251    pub fn set_size_with_unit(&mut self, bytes: usize, unit: SizeUnit) -> &mut Self {
252        let s = format_size_fixed_rs(&mut self.buf, bytes, unit as u8);
253        self.len = s.len();
254        self
255    }
256
257    /// Return the formatted string slice.
258    pub fn as_str(&self) -> &str {
259        core::str::from_utf8(&self.buf[..self.len]).unwrap_or("")
260    }
261
262    /// Returns the formatted magnitude as a string slice (without unit suffix).
263    pub fn magnitude(&self) -> &str {
264        let s = self.as_str();
265        if s.is_empty() { s } else { &s[..s.len() - 1] }
266    }
267
268    /// Returns the associated `SizeUnit`. In the case of an empty string, `SizeUnit::Auto` is returned.
269    pub fn unit(&self) -> SizeUnit {
270        let s = self.as_str().as_bytes();
271        match s.last() {
272            Some(&last) => SizeUnit::try_from(last).unwrap_or(SizeUnit::Auto),
273            None => SizeUnit::Auto,
274        }
275    }
276}
277
278impl core::ops::Deref for FormattedBytes {
279    type Target = str;
280
281    fn deref(&self) -> &str {
282        self.as_str()
283    }
284}
285
286impl AsRef<str> for FormattedBytes {
287    fn as_ref(&self) -> &str {
288        self.as_str()
289    }
290}
291
292impl core::fmt::Display for FormattedBytes {
293    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
294        f.write_str(self.as_str())
295    }
296}
297
298impl core::fmt::Debug for FormattedBytes {
299    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
300        core::fmt::Debug::fmt(self.as_str(), f)
301    }
302}
303
304impl kprint::backend::AsKPrintStr for FormattedBytes {
305    #[inline(always)]
306    fn kprint_len(&self) -> core::ffi::c_int {
307        self.len as core::ffi::c_int
308    }
309
310    #[inline(always)]
311    fn kprint_ptr(&self) -> *const core::ffi::c_char {
312        self.buf.as_ptr() as *const core::ffi::c_char
313    }
314}
315
316impl kprint::backend::AsKPrintStr for &FormattedBytes {
317    #[inline(always)]
318    fn kprint_len(&self) -> core::ffi::c_int {
319        self.len as core::ffi::c_int
320    }
321
322    #[inline(always)]
323    fn kprint_ptr(&self) -> *const core::ffi::c_char {
324        self.buf.as_ptr() as *const core::ffi::c_char
325    }
326}
327
328#[derive(Debug, Clone, Copy, PartialEq, Eq)]
329struct EncodedSize<'a> {
330    // All numbers before the first '.'.
331    integral: &'a str,
332
333    // All numbers after the first '.'.
334    fractional: Option<&'a str>,
335
336    unit: SizeUnit,
337
338    scale: u64,
339}
340
341fn process_formatted_string(mut formatted: &str) -> Option<EncodedSize<'_>> {
342    if formatted.is_empty() {
343        return None;
344    }
345
346    let mut unit = SizeUnit::Bytes;
347    let mut scale = 1u64;
348
349    if let Some(last_char) = formatted.chars().next_back() {
350        if !last_char.is_ascii_digit() {
351            unit = SizeUnit::try_from(last_char).ok()?;
352            formatted = &formatted[..formatted.len() - last_char.len_utf8()];
353
354            // Look for the unit.
355            match unit {
356                SizeUnit::Bytes => scale = 1,
357                SizeUnit::KiB => scale = 1 << 10,
358                SizeUnit::MiB => scale = 1 << 20,
359                SizeUnit::GiB => scale = 1 << 30,
360                SizeUnit::TiB => scale = 1 << 40,
361                SizeUnit::PiB => scale = 1 << 50,
362                SizeUnit::EiB => scale = 1 << 60,
363                _ => return None,
364            }
365        }
366    }
367
368    let split_at = formatted.find('.');
369    let (integral, fractional) = if let Some(split_at) = split_at {
370        let integral = &formatted[..split_at];
371        let fractional = &formatted[split_at + 1..];
372        // "A.[Unit]" with A being digit is still invalid.
373        if fractional.is_empty() {
374            return None;
375        }
376        (integral, Some(fractional))
377    } else {
378        (formatted, None)
379    };
380
381    if integral.is_empty() {
382        return None;
383    }
384
385    Some(EncodedSize { integral, fractional, unit, scale })
386}
387
388/// Returns the number of bytes represented by a human readable string
389/// like "123.4k", 123.4 * 1024 bytes encoded in |formatted_bytes|.
390///
391/// If |formatted_bytes| is not correctly formatted then |None| is returned.
392///
393/// This is a reverse function of |format_size| input |bytes|. Except that it considers
394/// absence of unit (e.g. "123") to be in bytes(implicit B).
395pub fn parse_size_bytes(formatted_bytes: &str) -> Option<u64> {
396    let encoded_size = process_formatted_string(formatted_bytes)?;
397
398    let mut integral: u64 = 0;
399    let mut base_10: u64 = 1;
400
401    for c in encoded_size.integral.chars().rev() {
402        if !c.is_ascii_digit() {
403            return None;
404        }
405        let val = c.to_digit(10)? as u64;
406        let scaled_val = val.checked_mul(base_10)?.checked_mul(encoded_size.scale)?;
407
408        integral = integral.checked_add(scaled_val)?;
409        base_10 = base_10.checked_mul(10)?;
410    }
411
412    let mut fractional: u64 = 0;
413    if let Some(frac_str) = encoded_size.fractional {
414        let mut frac_base_10: u64 = 1;
415        let mut carry: u64 = 0;
416
417        // This loop provides software division, because for the larger
418        // units its is quite possible to overflow when doing the scaling
419        // of the mantissa.
420        // If one were to use the naive approach:
421        //  * let m be the mantissa as an integer.
422        //  * k the length of the mantissa.
423        //  * u the scaling factor of the provided unit.
424        //
425        // The number of bytes encoded in the mantissa, can be calculated as:
426        //     |m * u / 10^(k)|
427        // The problem arises when |m| * |u| exceeds the capacity of 64 bits.
428        for c in frac_str.chars() {
429            if !c.is_ascii_digit() {
430                return None;
431            }
432            let val = c.to_digit(10)? as u64;
433            frac_base_10 = frac_base_10.checked_mul(10)?;
434            let scaled_value = val.checked_mul(encoded_size.scale)?;
435
436            // Calculate how many bytes does this digit of the mantissa contributes.
437            let contrib = scaled_value / frac_base_10;
438            fractional = fractional.checked_add(contrib)?;
439
440            // Bring the carry from 10^-(i - 1) bytes to 10^-(i) bytes.
441            carry = carry.checked_mul(10)?.checked_add(scaled_value % frac_base_10)?;
442
443            // Try to consume any part of the accumulated carry.
444            let consumed_carry = carry / frac_base_10;
445            fractional = fractional.checked_add(consumed_carry)?;
446
447            // Adjust the units back again.
448            carry %= frac_base_10;
449        }
450
451        // At this point there should be no carry left, unless we were given
452        // a value that is not byte aligned (Y.X bytes) where X is non zero,
453        // after applying the proper scaling.
454        if carry != 0 {
455            return None;
456        }
457    }
458
459    integral.checked_add(fractional)
460}
461
462#[cfg(test)]
463mod tests {
464    use super::*;
465
466    const KILO: usize = 1024;
467    const MEGA: usize = KILO * 1024;
468    const GIGA: usize = MEGA * 1024;
469    const TERA: usize = GIGA * 1024;
470    const PETA: usize = TERA * 1024;
471    const EXA: usize = PETA * 1024;
472
473    struct FormatSizeTestCase {
474        input: usize,
475        unit: u8,
476        expected_output: &'static str,
477    }
478
479    const FORMAT_SIZE_TEST_CASES: &[FormatSizeTestCase] = &[
480        // Whole multiples don't print decimals,
481        // and always round up to their largest unit.
482        FormatSizeTestCase { input: 0, unit: 0, expected_output: "0B" },
483        FormatSizeTestCase { input: 1, unit: 0, expected_output: "1B" },
484        // Favor the largest unit when it loses no precision
485        // (e.g., "1K" not "1024B").
486        // Larger values may still use a smaller unit
487        // (e.g., "1K" + 1 == "1025B") to preserve precision.
488        FormatSizeTestCase { input: KILO - 1, unit: 0, expected_output: "1023B" },
489        FormatSizeTestCase { input: KILO, unit: 0, expected_output: "1K" },
490        FormatSizeTestCase { input: KILO + 1, unit: 0, expected_output: "1025B" },
491        FormatSizeTestCase { input: KILO * 9, unit: 0, expected_output: "9K" },
492        FormatSizeTestCase { input: KILO * 9 + 1, unit: 0, expected_output: "9217B" },
493        FormatSizeTestCase { input: KILO * 10, unit: 0, expected_output: "10K" },
494        // Same demonstration for the next unit.
495        FormatSizeTestCase { input: MEGA - KILO, unit: 0, expected_output: "1023K" },
496        FormatSizeTestCase { input: MEGA, unit: 0, expected_output: "1M" },
497        FormatSizeTestCase { input: MEGA + KILO, unit: 0, expected_output: "1025K" },
498        FormatSizeTestCase { input: MEGA * 9, unit: 0, expected_output: "9M" },
499        FormatSizeTestCase { input: MEGA * 9 + KILO, unit: 0, expected_output: "9217K" },
500        FormatSizeTestCase { input: MEGA * 10, unit: 0, expected_output: "10M" },
501        // Sanity checks for remaining units.
502        FormatSizeTestCase { input: MEGA, unit: 0, expected_output: "1M" },
503        FormatSizeTestCase { input: GIGA, unit: 0, expected_output: "1G" },
504        FormatSizeTestCase { input: TERA, unit: 0, expected_output: "1T" },
505        FormatSizeTestCase { input: PETA, unit: 0, expected_output: "1P" },
506        FormatSizeTestCase { input: EXA, unit: 0, expected_output: "1E" },
507        // Non-whole multiples print decimals, and favor more whole digits
508        // (e.g., "1024.0K" not "1.0M") to retain precision.
509        FormatSizeTestCase { input: MEGA - 1, unit: 0, expected_output: "1024.0K" },
510        FormatSizeTestCase { input: MEGA + MEGA / 3, unit: 0, expected_output: "1365.3K" },
511        FormatSizeTestCase { input: GIGA - 1, unit: 0, expected_output: "1024.0M" },
512        FormatSizeTestCase { input: TERA - 1, unit: 0, expected_output: "1024.0G" },
513        FormatSizeTestCase { input: PETA - 1, unit: 0, expected_output: "1024.0T" },
514        FormatSizeTestCase { input: EXA - 1, unit: 0, expected_output: "1024.0P" },
515        FormatSizeTestCase { input: usize::MAX, unit: 0, expected_output: "16.0E" },
516        // Never show more than four whole digits,
517        // to make the values easier to eyeball.
518        FormatSizeTestCase { input: 9999, unit: 0, expected_output: "9999B" },
519        FormatSizeTestCase { input: 10000, unit: 0, expected_output: "9.8K" },
520        FormatSizeTestCase { input: KILO * 9999, unit: 0, expected_output: "9999K" },
521        FormatSizeTestCase { input: KILO * 9999 + 1, unit: 0, expected_output: "9999.0K" },
522        FormatSizeTestCase { input: KILO * 10000, unit: 0, expected_output: "9.8M" },
523        // Ensure values are correctly rounded.
524        FormatSizeTestCase { input: 10700, unit: 0, expected_output: "10.4K" },
525        FormatSizeTestCase { input: 10701, unit: 0, expected_output: "10.5K" },
526        FormatSizeTestCase { input: 69887590, unit: 0, expected_output: "66.6M" },
527        FormatSizeTestCase { input: 69887591, unit: 0, expected_output: "66.7M" },
528        FormatSizeTestCase { input: 18389097998479209267, unit: 0, expected_output: "15.9E" },
529        FormatSizeTestCase { input: 18389097998479209268, unit: 0, expected_output: "16.0E" },
530        // When fixed, we can see a lot more digits.
531        FormatSizeTestCase {
532            input: usize::MAX,
533            unit: b'B',
534            expected_output: "18446744073709551615B",
535        },
536        FormatSizeTestCase {
537            input: usize::MAX,
538            unit: b'K',
539            expected_output: "18014398509481984.0K",
540        },
541        FormatSizeTestCase { input: usize::MAX, unit: b'M', expected_output: "17592186044416.0M" },
542        FormatSizeTestCase { input: usize::MAX, unit: b'G', expected_output: "17179869184.0G" },
543        FormatSizeTestCase { input: usize::MAX, unit: b'T', expected_output: "16777216.0T" },
544        FormatSizeTestCase { input: usize::MAX, unit: b'P', expected_output: "16384.0P" },
545        FormatSizeTestCase { input: usize::MAX, unit: b'E', expected_output: "16.0E" },
546        // Smaller than natural fixed unit.
547        FormatSizeTestCase { input: GIGA, unit: b'K', expected_output: "1048576K" },
548        // Larger than natural fixed unit.
549        FormatSizeTestCase { input: MEGA / 10, unit: b'M', expected_output: "0.1M" },
550        // Unknown units fall back to natural, but add a '?' prefix.
551        FormatSizeTestCase { input: GIGA, unit: b'q', expected_output: "?1G" },
552        FormatSizeTestCase { input: KILO, unit: b'q', expected_output: "?1K" },
553        FormatSizeTestCase { input: GIGA + 1, unit: b'#', expected_output: "?1.0G" },
554        FormatSizeTestCase { input: KILO + 1, unit: b'#', expected_output: "?1025B" },
555    ];
556
557    #[test]
558    fn test_format_size_fixed() {
559        let mut buf = [0u8; MAX_FORMAT_SIZE_LEN];
560        for (i, tc) in FORMAT_SIZE_TEST_CASES.iter().enumerate() {
561            buf.fill(0);
562            let res = format_size_fixed_rs(&mut buf, tc.input, tc.unit);
563            assert_eq!(
564                res, tc.expected_output,
565                "case {}, input={}, unit={}",
566                i, tc.input, tc.unit as char
567            );
568        }
569    }
570
571    #[test]
572    fn test_format_size_short_buf_truncates() {
573        let input = 1023 * KILO + 1;
574        let expected_output = "1023.0K";
575
576        let mut buf = [0x55u8; MAX_FORMAT_SIZE_LEN * 2];
577        for str_size in 0..=expected_output.len() {
578            buf.fill(0x55);
579            let res = format_size_rs(&mut buf[..str_size], input);
580            assert_eq!(res, &expected_output[..str_size]);
581            assert_eq!(buf[str_size], 0x55);
582        }
583    }
584
585    #[test]
586    fn test_format_size_bad_unit_short_buf_truncates() {
587        let mut buf = [0x55u8; MAX_FORMAT_SIZE_LEN];
588
589        // Size zero should not touch the buffer.
590        buf.fill(0x55);
591        let res = format_size_fixed_rs(&mut buf[..0], GIGA, b'q');
592        assert_eq!(res, "");
593        assert_eq!(buf[0], 0x55);
594
595        // Size 1 should just be the warning '?'.
596        buf.fill(0x55);
597        let res = format_size_fixed_rs(&mut buf[..1], GIGA, b'q');
598        assert_eq!(res, "?");
599        assert_eq!(buf[1], 0x55);
600
601        // Then just the number without units.
602        buf.fill(0x55);
603        let res = format_size_fixed_rs(&mut buf[..2], GIGA, b'q');
604        assert_eq!(res, "?1");
605        assert_eq!(buf[2], 0x55);
606
607        // Then the whole thing.
608        buf.fill(0x55);
609        let res = format_size_fixed_rs(&mut buf[..3], GIGA, b'q');
610        assert_eq!(res, "?1G");
611        assert_eq!(buf[3], 0x55);
612    }
613
614    #[test]
615    fn test_cpp_to_string() {
616        assert_eq!(SizeUnit::Auto.to_str(), "");
617        assert_eq!(SizeUnit::Bytes.to_str(), "B");
618        assert_eq!(SizeUnit::KiB.to_str(), "K");
619        assert_eq!(SizeUnit::MiB.to_str(), "M");
620        assert_eq!(SizeUnit::GiB.to_str(), "G");
621        assert_eq!(SizeUnit::TiB.to_str(), "T");
622        assert_eq!(SizeUnit::PiB.to_str(), "P");
623        assert_eq!(SizeUnit::EiB.to_str(), "E");
624    }
625
626    struct ParseTestCase {
627        expected_bytes: u64,
628        input: &'static str,
629    }
630
631    const PARSE_TEST_CASES: &[ParseTestCase] = &[
632        // Integral
633        ParseTestCase { expected_bytes: 1234, input: "1234" },
634        ParseTestCase { expected_bytes: 1234, input: "1234b" },
635        ParseTestCase { expected_bytes: 1234, input: "1234B" },
636        ParseTestCase { expected_bytes: 1234 * 1024, input: "1234k" },
637        ParseTestCase { expected_bytes: 1234 * 1024, input: "1234K" },
638        ParseTestCase { expected_bytes: 1234 * 1024 * 1024, input: "1234m" },
639        ParseTestCase { expected_bytes: 1234 * 1024 * 1024, input: "1234M" },
640        ParseTestCase { expected_bytes: 1234 * 1024 * 1024 * 1024, input: "1234g" },
641        ParseTestCase { expected_bytes: 1234 * 1024 * 1024 * 1024, input: "1234G" },
642        ParseTestCase { expected_bytes: 1234 * 1024 * 1024 * 1024 * 1024, input: "1234t" },
643        ParseTestCase { expected_bytes: 1234 * 1024 * 1024 * 1024 * 1024, input: "1234T" },
644        ParseTestCase { expected_bytes: 5 * 1024 * 1024 * 1024 * 1024 * 1024, input: "5p" },
645        ParseTestCase { expected_bytes: 5 * 1024 * 1024 * 1024 * 1024 * 1024, input: "5P" },
646        ParseTestCase { expected_bytes: 2 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024, input: "2e" },
647        ParseTestCase { expected_bytes: 2 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024, input: "2E" },
648        // Fractional
649        ParseTestCase { expected_bytes: 10700, input: "10.4492187500k" },
650        ParseTestCase { expected_bytes: 10700, input: "10.4492187500K" },
651        ParseTestCase { expected_bytes: 10700 * 1024, input: "10.4492187500m" },
652        ParseTestCase { expected_bytes: 10700 * 1024, input: "10.4492187500M" },
653        ParseTestCase { expected_bytes: 10700 * 1024 * 1024, input: "10.4492187500g" },
654        ParseTestCase { expected_bytes: 10700 * 1024 * 1024, input: "10.4492187500G" },
655        ParseTestCase { expected_bytes: 10700 * 1024 * 1024 * 1024, input: "10.4492187500t" },
656        ParseTestCase { expected_bytes: 10700 * 1024 * 1024 * 1024, input: "10.4492187500T" },
657        ParseTestCase {
658            expected_bytes: 10700 * 1024 * 1024 * 1024 * 1024,
659            input: "10.4492187500p",
660        },
661        ParseTestCase {
662            expected_bytes: 10700 * 1024 * 1024 * 1024 * 1024,
663            input: "10.4492187500P",
664        },
665        ParseTestCase { expected_bytes: 1441151880758558720, input: "1.25e" },
666        ParseTestCase { expected_bytes: 1441151880758558720, input: "1.25E" },
667    ];
668
669    #[test]
670    fn test_parse_size_bytes() {
671        for tc in PARSE_TEST_CASES {
672            let res = parse_size_bytes(tc.input);
673            assert_eq!(res, Some(tc.expected_bytes), "input: {}", tc.input);
674        }
675    }
676
677    const INVALID_INPUTS: &[&str] = &["", "1..1", "1w", "b", "AM", "1.AM", "A.1M"];
678
679    #[test]
680    fn test_parse_size_bytes_invalid() {
681        for input in INVALID_INPUTS {
682            let res = parse_size_bytes(input);
683            assert_eq!(res, None, "input: {}", input);
684        }
685    }
686
687    #[test]
688    fn test_formatted_bytes() {
689        let empty = FormattedBytes::empty();
690        assert_eq!(empty.as_str(), "");
691        assert_eq!(empty.magnitude(), "");
692        assert_eq!(empty.unit(), SizeUnit::Auto);
693
694        let fb = FormattedBytes::new(1024 * 1024);
695        assert_eq!(fb.as_str(), "1M");
696        assert_eq!(fb.magnitude(), "1");
697        assert_eq!(fb.unit(), SizeUnit::MiB);
698
699        let fb_unit = FormattedBytes::with_unit(1024 * 1024, SizeUnit::KiB);
700        assert_eq!(fb_unit.as_str(), "1024K");
701        assert_eq!(fb_unit.magnitude(), "1024");
702        assert_eq!(fb_unit.unit(), SizeUnit::KiB);
703    }
704}