ubyte/
parse.rs

1use crate::ByteUnit;
2
3macro_rules! parse_suffix_fn {
4    ($($suffix:ident),*) => (
5        parse_suffix_fn!($($suffix, stringify!($suffix)),*);
6    );
7    ($($suffix:ident, $string:expr),*) => (
8        fn parse_suffix(string: &str) -> Option<ByteUnit> {
9            $(if string.eq_ignore_ascii_case($string) {
10                return Some(ByteUnit::$suffix);
11            })*
12
13            None
14        }
15    );
16}
17
18parse_suffix_fn!(B, kB, KiB, MB, MiB, GB, GiB, TB, TiB, PB, PiB, EB, EiB);
19
20fn is_suffix_char(c: char) -> bool {
21    "begikmpt ".contains(c.to_ascii_lowercase())
22}
23
24/// Parsing error, as returned by
25/// [`ByteUnit::from_str()`](struct.ByteUnit.html#impl-FromStr).
26#[non_exhaustive]
27#[derive(Debug, Clone)]
28pub enum Error {
29    /// The input was empty.
30    Empty,
31    /// Found unexpected character `.1` at byte index `.0`.
32    Unexpected(usize, char),
33    /// A [`ByteUnit::B`] contained a fractional component.
34    FractionalByte,
35    /// The parsed byte unit suffix is unknown.
36    BadSuffix,
37    /// The whole part of the the number (`{whole}.{frac}`) was invalid.
38    BadWhole(core::num::ParseIntError),
39    /// The fractional part of the the number (`{whole}.{frac}`) was invalid.
40    BadFractional(core::num::ParseIntError),
41}
42
43impl core::str::FromStr for ByteUnit {
44    type Err = Error;
45
46    fn from_str(s: &str) -> Result<Self, Self::Err> {
47        if s.is_empty() { return Err(Error::Empty); }
48        let (mut dot, mut suffix) = (None, None);
49        for (i, c) in s.chars().enumerate() {
50            match c {
51                c if c.is_ascii_digit() && suffix.is_none() => continue,
52                '.' if dot.is_none() && suffix.is_none() => dot = Some(i),
53                c if is_suffix_char(c) && suffix.is_none() => suffix = Some(i),
54                c if is_suffix_char(c) => continue,
55                _ => Err(Error::Unexpected(i, c))?
56            }
57        }
58
59        // We can't start with `.` or a suffix character.
60        if dot.map(|i| i == 0).unwrap_or(false) || suffix.map(|i| i == 0).unwrap_or(false) {
61            return Err(Error::Unexpected(0, s.as_bytes()[0] as char));
62        }
63
64        // Parse the suffix. A fractional doesn't make sense for bytes.
65        let suffix_str = suffix.map(|i| s[i..].trim_start()).unwrap_or("b");
66        let unit = parse_suffix(suffix_str).ok_or(Error::BadSuffix)?;
67        if unit == ByteUnit::B && dot.is_some() {
68            return Err(Error::FractionalByte);
69        }
70
71        let num_end = suffix.unwrap_or(s.len());
72        match dot {
73            Some(i) => {
74                let frac_str = &s[(i + 1)..num_end];
75                let whole: u64 = s[..i].parse().map_err(Error::BadWhole)?;
76                let frac: u32 = frac_str.parse().map_err(Error::BadFractional)?;
77                let frac_part = frac as f64 / 10u64.saturating_pow(frac_str.len() as u32) as f64;
78                let frac_unit = (frac_part * unit.as_u64() as f64) as u64;
79                Ok(whole * unit + frac_unit)
80            }
81            None => {
82                let whole: u64 = s[..num_end].parse().map_err(Error::BadWhole)?;
83                Ok(whole * unit)
84            }
85        }
86    }
87}
88
89impl core::fmt::Display for Error {
90    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
91        use Error::*;
92        match self {
93            Empty => write!(f, "the input was empty"),
94            Unexpected(i, c) => write!(f, "unexpected character {:?} at index `{}`", c, i),
95            FractionalByte => write!(f, "unit `B` cannot have a fractional component"),
96            BadSuffix => write!(f, "unknown or malformed byte unit suffix"),
97            BadWhole(e) => write!(f, "whole part failed to parse: {}", e),
98            BadFractional(e) => write!(f, "fractional part failed to parse: {}", e),
99        }
100    }
101}
102
103#[cfg(test)]
104mod parse_tests {
105    use core::str::FromStr;
106    use crate::{ByteUnit, ToByteUnit};
107
108    macro_rules! assert_reject {
109        ($($s:expr),* $(,)?) => ($(
110            let result = ByteUnit::from_str($s);
111            assert!(result.is_err(), "{:?} parsed as {}", $s, result.unwrap());
112        )*)
113    }
114
115    macro_rules! assert_parses {
116        ($($s:expr => $b:expr),* $(,)?) => ($(
117            let result = ByteUnit::from_str($s);
118            assert!(result.is_ok(), "{:?} failed to parse: {}", $s, result.unwrap_err());
119            let actual = result.unwrap();
120            assert_eq!(actual, $b, "expected {}, got {}", $b, actual);
121        )*)
122    }
123
124    #[test]
125    fn reject() {
126        assert_reject!["", "a", "amb", "1.2", ".", ".23KiB", "1.2.3mb", "1.23bcc"];
127        assert_reject!["?mb", "1.2mb.", ".2mb", "99k", "99bk", "1 k b"];
128        assert_reject!["1.2mkb", "1kb2", "1MB ", " 1MB"];
129        assert_reject!["287423890740938348498349344"];
130        assert_reject!["1.kb", "1.", "1. ", "2. kb"];
131    }
132
133    #[test]
134    fn accept() {
135        assert_parses! {
136            "1" => 1.bytes(),
137            "123" => 123.bytes(),
138            "99" => 99.bytes(),
139            "2394394" => 2394394.bytes(),
140            "2874238907409384" => 2874238907409384u64.bytes(),
141        }
142
143        assert_parses! {
144            "1 b" => 1.bytes(),
145            "1 B" => 1.bytes(),
146            "1B" => 1.bytes(),
147            "1b" => 1.bytes(),
148            "1 mb" => 1.megabytes(),
149            "1 mib" => 1.mebibytes(),
150            "1mib" => 1.mebibytes(),
151            "1 kb" => 1.kilobytes(),
152            "1 kB" => 1.kilobytes(),
153            "1 kib" => 1.kibibytes(),
154            "1kib" => 1.kibibytes(),
155            "1KiB" => 1.kibibytes(),
156            "1GB" => 1.gigabytes(),
157
158            "349 b" => 349.bytes(),
159            "13489 mb" => 13489.megabytes(),
160            "349b" => 349.bytes(),
161            "13489mb" => 13489.megabytes(),
162        }
163
164        assert_parses! {
165            "0.5KiB" => 512.bytes(),
166            "0.5KB" => 500.bytes(),
167        }
168
169        assert_parses! {
170            "323kB" => 323.kilobytes(),
171            "3MB" => 3.megabytes(),
172            "3MiB" => 3.mebibytes(),
173            "8GiB" => 8.gibibytes(),
174
175            "5MiB" => 3.mebibytes() + 2.mebibytes(),
176            "7.06GB" => 7.gigabytes() + 60.megabytes(),
177            "7.25GB" => 7.gigabytes() + 250.megabytes(),
178
179            "01MB" => 1.megabytes(),
180            "0001MiB" => 1.mebibytes(),
181        }
182
183        assert_parses! {
184            "9.00000000000000000000MB" => 9.megabytes(),
185            "9.000000000000000000000000000000MB" => 9.megabytes(),
186        }
187    }
188}