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#[non_exhaustive]
27#[derive(Debug, Clone)]
28pub enum Error {
29 Empty,
31 Unexpected(usize, char),
33 FractionalByte,
35 BadSuffix,
37 BadWhole(core::num::ParseIntError),
39 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 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 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}