1pub fn parse_size_in_bytes(raw: &str) -> Option<u64> {
27 let (digits, multiplier) = split_unit(raw.trim());
28 let count = u64::try_from(parse_go_int(trim_zero_decimal(digits))?).ok()?;
29 count.checked_mul(multiplier)
30}
31
32fn split_unit(text: &str) -> (&str, u64) {
36 let bytes = text.as_bytes();
37 let len = bytes.len();
38 if len < 3 || !matches!(bytes[len - 1], b'b' | b'B') {
39 return (text, 1);
40 }
41
42 let (multiplier, suffix_len) = match bytes[len - 2].to_ascii_lowercase() {
43 b'k' => (1 << 10, 2),
44 b'm' => (1 << 20, 2),
45 b'g' => (1 << 30, 2),
46 _ => (1, 1),
47 };
48 (text[..len - suffix_len].trim(), multiplier)
49}
50
51fn trim_zero_decimal(text: &str) -> &str {
57 let mut found_zero = false;
58 for (index, byte) in text.bytes().enumerate().rev() {
59 match byte {
60 b'.' if found_zero => return &text[..index],
61 b'.' => {}
62 b'0' => found_zero = true,
63 _ => return text,
64 }
65 }
66 text
67}
68
69fn parse_go_int(text: &str) -> Option<i64> {
71 let (negative, unsigned) = match text.as_bytes().first() {
72 Some(b'+') => (false, &text[1..]),
73 Some(b'-') => (true, &text[1..]),
74 _ => (false, text),
75 };
76
77 let magnitude = i128::from(parse_go_uint(unsigned)?);
78 i64::try_from(if negative { -magnitude } else { magnitude }).ok()
80}
81
82fn parse_go_uint(text: &str) -> Option<u64> {
84 let bytes = text.as_bytes();
85 let &first = bytes.first()?;
86
87 let (base, digits) = if first == b'0' {
89 match bytes.get(1).map(u8::to_ascii_lowercase) {
90 Some(b'b') if bytes.len() >= 3 => (2, &bytes[2..]),
91 Some(b'o') if bytes.len() >= 3 => (8, &bytes[2..]),
92 Some(b'x') if bytes.len() >= 3 => (16, &bytes[2..]),
93 _ => (8, &bytes[1..]),
94 }
95 } else {
96 (10, bytes)
97 };
98
99 let mut value: u64 = 0;
100 let mut saw_underscore = false;
101 for &byte in digits {
102 let digit = match byte {
103 b'_' => {
104 saw_underscore = true;
105 continue;
106 }
107 b'0'..=b'9' => byte - b'0',
108 b'a'..=b'z' | b'A'..=b'Z' => byte.to_ascii_lowercase() - b'a' + 10,
109 _ => return None,
110 };
111 if digit >= base {
112 return None;
113 }
114 value = value.checked_mul(u64::from(base))?.checked_add(u64::from(digit))?;
115 }
116
117 if saw_underscore && !underscores_ok(bytes) {
118 return None;
119 }
120 Some(value)
121}
122
123fn underscores_ok(bytes: &[u8]) -> bool {
126 #[derive(PartialEq)]
128 enum Previous {
129 Start,
130 Digit,
131 Underscore,
132 Other,
133 }
134
135 let mut previous = Previous::Start;
136 let mut hex = false;
137 let mut rest = bytes;
138 if let [b'0', prefix, tail @ ..] = bytes {
139 if matches!(prefix.to_ascii_lowercase(), b'b' | b'o' | b'x') {
140 previous = Previous::Digit;
142 hex = prefix.eq_ignore_ascii_case(&b'x');
143 rest = tail;
144 }
145 }
146
147 for &byte in rest {
148 if byte.is_ascii_digit() || (hex && byte.is_ascii_hexdigit()) {
149 previous = Previous::Digit;
150 } else if byte == b'_' {
151 if previous != Previous::Digit {
152 return false;
153 }
154 previous = Previous::Underscore;
155 } else if previous == Previous::Underscore {
156 return false;
157 } else {
158 previous = Previous::Other;
159 }
160 }
161 previous != Previous::Underscore
162}
163
164#[cfg(test)]
165mod tests {
166 use super::parse_size_in_bytes;
167
168 #[track_caller]
170 fn assert_sizes(cases: &[(&str, u64)]) {
171 for &(input, expected) in cases {
172 assert_eq!(parse_size_in_bytes(input), Some(expected), "input {input:?}");
173 }
174 }
175
176 #[track_caller]
178 fn assert_unparsed(inputs: &[&str]) {
179 for &input in inputs {
180 assert_eq!(parse_size_in_bytes(input), None, "input {input:?}");
181 }
182 }
183
184 #[test]
192 fn agent_get_size_in_bytes_cases() {
193 assert_sizes(&[
194 ("123", 123),
195 ("1kb", 1024),
196 ("1Mb", 1024 * 1024),
197 ("1 GB", 1024 * 1024 * 1024),
198 ]);
199 assert_unparsed(&["-1", "invalid"]);
200 }
201
202 #[test]
203 fn schema_default_and_common_spellings_are_binary_units() {
204 assert_sizes(&[
205 ("10Mb", 10_485_760),
206 ("10MB", 10_485_760),
207 ("10mb", 10_485_760),
208 ("10 MB", 10_485_760),
209 (" 10MB ", 10_485_760),
210 ("10 MB ", 10_485_760),
211 ("10.0MB", 10_485_760),
212 ("10.00MB", 10_485_760),
213 ("+10MB", 10_485_760),
214 ("5KB", 5_120),
215 ("1GB", 1 << 30),
216 ]);
217 }
218
219 #[test]
220 fn trailing_dot_after_an_all_zero_fraction_is_dropped() {
221 assert_sizes(&[("10.0.MB", 10 << 20), ("1.0.", 1)]);
224 }
225
226 #[test]
227 fn unit_letters_fold_case_independently() {
228 assert_sizes(&[
229 ("1kB", 1 << 10),
230 ("1Kb", 1 << 10),
231 ("1KB", 1 << 10),
232 ("1mB", 1 << 20),
233 ("1Mb", 1 << 20),
234 ("1gb", 1 << 30),
235 ("1gB", 1 << 30),
236 ("1Gb", 1 << 30),
237 ]);
238 }
239
240 #[test]
241 fn plain_numbers_and_bare_byte_suffix_are_literal() {
242 assert_sizes(&[
243 ("100", 100),
244 ("100b", 100),
245 ("100 b", 100),
246 ("3 b", 3),
247 ("0", 0),
248 ("0KB", 0),
249 ("-0", 0),
250 ("10485760", 10_485_760),
251 ]);
252 }
253
254 #[test]
255 fn two_byte_byte_suffix_is_not_stripped() {
256 assert_unparsed(&["1B", "1b", "kb", "KB", "b", "B"]);
259 assert_sizes(&[("10b", 10), ("1 b", 1)]);
260 }
261
262 #[test]
263 fn base_prefixes_follow_go_integer_literals() {
264 assert_sizes(&[
265 ("0x10KB", 16 << 10),
266 ("0X10KB", 16 << 10),
267 ("0xffkb", 255 << 10),
268 ("0x10", 16),
269 ("0o10KB", 8 << 10),
270 ("0O10KB", 8 << 10),
271 ("010KB", 8 << 10),
272 ("010", 8),
273 ("0b10KB", 2 << 10),
274 ("0B10KB", 2 << 10),
275 ("0b1", 1),
276 ("0o7", 7),
277 ("00", 0),
278 ("0x1.0", 1),
279 ]);
280 assert_unparsed(&["08", "0x", "0b", "0o", "0b2", "0o8", "0xg", "0x_"]);
281 }
282
283 #[test]
284 fn underscores_must_separate_digits() {
285 assert_sizes(&[
286 ("1_000KB", 1_000 << 10),
287 ("1_0", 10),
288 ("1_0_0", 100),
289 ("0_7", 7),
290 ("0_10", 8),
291 ("0x_1", 1),
292 ("0x_ff", 255),
293 ("0xf_f", 255),
294 ("0b_1", 1),
295 ("0o_7", 7),
296 ("0b1_0KB", 2 << 10),
297 ]);
298 assert_unparsed(&["_1KB", "1_KB", "1__0", "1__0KB", "1_", "0_x1", "0xf__f", "0b1_"]);
299 }
300
301 #[test]
302 fn units_the_agent_does_not_know_are_unparsed() {
303 assert_unparsed(&[
304 "10MiB", "10K", "10M", "1G", "10TB", "3tb", "1.5MB", "1KBB", "1 K B", "2k b", "MB",
305 ]);
306 }
307
308 #[test]
309 fn negative_empty_and_malformed_numbers_are_unparsed() {
310 assert_unparsed(&[
311 "-1MB", "-1", "", " ", "abc", "+", "-", "+-1", "--1", "1 0", "10.MB", ".0MB", "1.0.0MB", "10 . 0MB",
312 ]);
313 }
314
315 #[test]
316 fn integer_range_is_the_signed_64_bit_range() {
317 assert_sizes(&[
318 ("9223372036854775807", i64::MAX as u64),
319 ("9223372036854775807b", i64::MAX as u64),
320 ]);
321 assert_unparsed(&["9223372036854775808", "-9223372036854775808", "-9223372036854775809"]);
323 assert_unparsed(&["99999999999999999999999"]);
324 }
325
326 #[test]
327 fn product_may_exceed_i64_but_not_u64() {
328 assert_sizes(&[
330 ("8589934592GB", 1 << 63),
331 ("17592186044415MB", 18_446_744_073_708_503_040),
332 ]);
333 assert_unparsed(&["17592186044416MB", "9223372036854775807KB", "9223372036854775807GB"]);
334 }
335
336 #[test]
337 fn whitespace_follows_go_trim_space() {
338 assert_sizes(&[
339 ("10\u{a0}MB", 10 << 20),
340 ("10\u{3000}MB", 10 << 20),
341 ("\u{a0}1\u{a0}kb\u{a0}", 1 << 10),
342 ("1\tKB", 1 << 10),
343 ("1\nKB", 1 << 10),
344 ]);
345 }
346
347 #[test]
348 fn non_ascii_input_is_unparsed_without_panicking() {
349 assert_unparsed(&["é", "10éB", "10MéB", "10Mé", "10MB", "10€b", "ééb", "日本語"]);
351 }
352}