1#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum ParameterUnit {
12 Bytes,
13 Kilobytes,
14 Megabytes,
15 Milliseconds,
16 Seconds,
17 Minutes,
18}
19
20const MEMORY_UNITS_HINT: &str =
21 "Valid units for this parameter are \"B\", \"kB\", \"MB\", \"GB\", and \"TB\".";
22const TIME_UNITS_HINT: &str =
23 "Valid units for this parameter are \"us\", \"ms\", \"s\", \"min\", \"h\", and \"d\".";
24const INTEGER_RANGE_HINT: &str = "Value exceeds integer range.";
25const MAX_UNIT_LENGTH: usize = 3;
26const KIB: f64 = 1024.0;
27
28impl ParameterUnit {
29 pub const fn name(self) -> &'static str {
31 match self {
32 Self::Bytes => "B",
33 Self::Kilobytes => "kB",
34 Self::Megabytes => "MB",
35 Self::Milliseconds => "ms",
36 Self::Seconds => "s",
37 Self::Minutes => "min",
38 }
39 }
40
41 const fn hint(self) -> &'static str {
42 match self {
43 Self::Bytes | Self::Kilobytes | Self::Megabytes => MEMORY_UNITS_HINT,
44 Self::Milliseconds | Self::Seconds | Self::Minutes => TIME_UNITS_HINT,
45 }
46 }
47
48 fn conversions(self) -> [(&'static str, f64); 6] {
50 match self {
51 Self::Bytes => [
52 ("TB", KIB * KIB * KIB * KIB),
53 ("GB", KIB * KIB * KIB),
54 ("MB", KIB * KIB),
55 ("kB", KIB),
56 ("B", 1.0),
57 ("", 0.0),
58 ],
59 Self::Kilobytes => [
60 ("TB", KIB * KIB * KIB),
61 ("GB", KIB * KIB),
62 ("MB", KIB),
63 ("kB", 1.0),
64 ("B", 1.0 / KIB),
65 ("", 0.0),
66 ],
67 Self::Megabytes => [
68 ("TB", KIB * KIB),
69 ("GB", KIB),
70 ("MB", 1.0),
71 ("kB", 1.0 / KIB),
72 ("B", 1.0 / (KIB * KIB)),
73 ("", 0.0),
74 ],
75 Self::Milliseconds => [
76 ("d", 86_400_000.0),
77 ("h", 3_600_000.0),
78 ("min", 60_000.0),
79 ("s", 1000.0),
80 ("ms", 1.0),
81 ("us", 1.0 / 1000.0),
82 ],
83 Self::Seconds => [
84 ("d", 86_400.0),
85 ("h", 3600.0),
86 ("min", 60.0),
87 ("s", 1.0),
88 ("ms", 1.0 / 1000.0),
89 ("us", 1.0 / (1000.0 * 1000.0)),
90 ],
91 Self::Minutes => [
92 ("d", 1440.0),
93 ("h", 60.0),
94 ("min", 1.0),
95 ("s", 1.0 / 60.0),
96 ("ms", 1.0 / (1000.0 * 60.0)),
97 ("us", 1.0 / (1000.0 * 1000.0 * 60.0)),
98 ],
99 }
100 }
101}
102
103pub(super) fn parse_integer(
105 raw: &str,
106 unit: Option<ParameterUnit>,
107) -> Result<i32, Option<&'static str>> {
108 let bytes = raw.as_bytes();
109 let (integer, integer_end, overflow) = c_strtol(bytes);
110 let (mut value, mut end) = (integer as f64, integer_end);
111 if overflow || matches!(bytes.get(integer_end), Some(b'.' | b'e' | b'E')) {
112 let (double, double_end, out_of_range) = c_strtod(bytes);
113 if double_end == 0 || out_of_range {
114 return Err(None);
115 }
116 (value, end) = (double, double_end);
117 } else if integer_end == 0 {
118 return Err(None);
119 }
120 if value.is_nan() {
121 return Err(None);
122 }
123 end = skip_c_space(bytes, end);
124 if end < bytes.len() {
125 let unit = unit.ok_or(None)?;
126 value = convert_to_base_unit(value, &bytes[end..], unit).ok_or(Some(unit.hint()))?;
127 }
128 let value = value.round_ties_even();
129 if value > f64::from(i32::MAX) || value < f64::from(i32::MIN) {
130 return Err(Some(INTEGER_RANGE_HINT));
131 }
132 Ok(value as i32)
133}
134
135pub(super) fn show_integer(value: i64, unit: Option<ParameterUnit>) -> String {
137 let Some(unit) = unit.filter(|_| value > 0) else {
138 return value.to_string();
139 };
140 for (name, multiplier) in unit.conversions() {
141 if name.is_empty() {
142 break;
143 }
144 if multiplier <= 1.0 || value % (multiplier as i64) == 0 {
145 let converted = (value as f64 / multiplier).round_ties_even() as i64;
146 return format!("{converted}{name}");
147 }
148 }
149 unreachable!("every unit lists its base unit")
150}
151
152fn convert_to_base_unit(value: f64, text: &[u8], unit: ParameterUnit) -> Option<f64> {
154 let length = text
155 .iter()
156 .take(MAX_UNIT_LENGTH)
157 .take_while(|byte| !is_c_space(**byte))
158 .count();
159 if skip_c_space(text, length) != text.len() {
160 return None;
161 }
162 let conversions = unit.conversions();
163 let index = conversions
164 .iter()
165 .position(|(name, _)| !name.is_empty() && name.as_bytes() == &text[..length])?;
166 let mut converted = value * conversions[index].1;
167 if let Some((next, multiplier)) = conversions.get(index + 1) {
168 if !next.is_empty() {
169 converted = (converted / multiplier).round_ties_even() * multiplier;
170 }
171 }
172 Some(converted)
173}
174
175pub(super) fn is_c_space(byte: u8) -> bool {
177 matches!(byte, b' ' | b'\t' | b'\n' | 0x0b | 0x0c | b'\r')
178}
179
180fn skip_c_space(bytes: &[u8], mut index: usize) -> usize {
181 while bytes.get(index).copied().is_some_and(is_c_space) {
182 index += 1;
183 }
184 index
185}
186
187fn c_strtol(bytes: &[u8]) -> (i64, usize, bool) {
189 let mut index = skip_c_space(bytes, 0);
190 let negative = match bytes.get(index) {
191 Some(b'-') => {
192 index += 1;
193 true
194 }
195 Some(b'+') => {
196 index += 1;
197 false
198 }
199 _ => false,
200 };
201 let hexadecimal = matches!(bytes.get(index..index + 2), Some(b"0x" | b"0X"))
202 && bytes.get(index + 2).is_some_and(u8::is_ascii_hexdigit);
203 let radix = if hexadecimal {
204 index += 2;
205 16
206 } else if bytes.get(index) == Some(&b'0') {
207 8
208 } else {
209 10
210 };
211 let start = index;
212 let mut magnitude: u128 = 0;
213 let mut overflow = false;
214 while let Some(digit) = bytes
215 .get(index)
216 .and_then(|byte| char::from(*byte).to_digit(radix))
217 {
218 magnitude = magnitude * u128::from(radix) + u128::from(digit);
219 if magnitude > u128::from(i64::MAX.unsigned_abs()) + 1 {
220 magnitude = u128::from(i64::MAX.unsigned_abs()) + 1;
221 overflow = true;
222 }
223 index += 1;
224 }
225 if index == start {
226 return (0, 0, false);
227 }
228 let value = if negative {
229 i64::try_from(-(magnitude as i128)).unwrap_or_else(|_| {
230 overflow = true;
231 i64::MIN
232 })
233 } else {
234 i64::try_from(magnitude).unwrap_or_else(|_| {
235 overflow = true;
236 i64::MAX
237 })
238 };
239 (value, index, overflow)
240}
241
242pub(super) fn c_strtod(bytes: &[u8]) -> (f64, usize, bool) {
244 let mut index = skip_c_space(bytes, 0);
245 let sign_start = index;
246 let negative = match bytes.get(index) {
247 Some(b'-') => {
248 index += 1;
249 true
250 }
251 Some(b'+') => {
252 index += 1;
253 false
254 }
255 _ => false,
256 };
257 let signed = |value: f64| if negative { -value } else { value };
258 let rest = &bytes[index..];
259 let starts_with =
260 |word: &[u8]| rest.len() >= word.len() && rest[..word.len()].eq_ignore_ascii_case(word);
261 if starts_with(b"infinity") {
262 return (signed(f64::INFINITY), index + 8, false);
263 }
264 if starts_with(b"inf") {
265 return (signed(f64::INFINITY), index + 3, false);
266 }
267 if starts_with(b"nan") {
268 let mut end = index + 3;
269 if bytes.get(end) == Some(&b'(') {
270 let close = bytes[end + 1..]
271 .iter()
272 .position(|byte| !(byte.is_ascii_alphanumeric() || *byte == b'_'));
273 if let Some(offset) = close {
274 if bytes[end + 1 + offset] == b')' {
275 end += offset + 2;
276 }
277 }
278 }
279 return (f64::NAN, end, false);
280 }
281 if matches!(rest.get(..2), Some(b"0x" | b"0X")) {
282 if let Some((value, end, nonzero)) = hexadecimal_float(bytes, index + 2) {
283 return finish_strtod(signed(value), end, nonzero);
284 }
285 }
286 let mut end = index;
287 let mut nonzero = false;
288 let mut digits = 0;
289 while let Some(byte) = bytes.get(end).filter(|byte| byte.is_ascii_digit()) {
290 nonzero |= *byte != b'0';
291 digits += 1;
292 end += 1;
293 }
294 if bytes.get(end) == Some(&b'.') {
295 end += 1;
296 while let Some(byte) = bytes.get(end).filter(|byte| byte.is_ascii_digit()) {
297 nonzero |= *byte != b'0';
298 digits += 1;
299 end += 1;
300 }
301 }
302 if digits == 0 {
303 return (0.0, 0, false);
304 }
305 if matches!(bytes.get(end), Some(b'e' | b'E')) {
306 let mut exponent = end + 1;
307 if matches!(bytes.get(exponent), Some(b'+' | b'-')) {
308 exponent += 1;
309 }
310 if bytes.get(exponent).is_some_and(u8::is_ascii_digit) {
311 end = exponent;
312 while bytes.get(end).is_some_and(u8::is_ascii_digit) {
313 end += 1;
314 }
315 }
316 }
317 let text = std::str::from_utf8(&bytes[sign_start..end]).expect("ASCII number");
318 let value = text.parse::<f64>().expect("C decimal number syntax");
319 finish_strtod(value, end, nonzero)
320}
321
322fn finish_strtod(value: f64, end: usize, nonzero: bool) -> (f64, usize, bool) {
323 let out_of_range = value.is_infinite() || (nonzero && value.abs() < f64::MIN_POSITIVE);
324 (value, end, out_of_range)
325}
326
327fn hexadecimal_float(bytes: &[u8], start: usize) -> Option<(f64, usize, bool)> {
329 let mut index = start;
330 let mut mantissa: u64 = 0;
331 let mut exponent: i64 = 0;
332 let mut digits = 0;
333 let mut nonzero = false;
334 let mut accumulate = |digit: u32, fraction: bool, exponent: &mut i64| {
335 nonzero |= digit != 0;
336 if mantissa >> 60 == 0 {
337 mantissa = mantissa << 4 | u64::from(digit);
338 if fraction {
339 *exponent -= 4;
340 }
341 } else if !fraction {
342 *exponent += 4;
343 }
344 };
345 while let Some(digit) = bytes
346 .get(index)
347 .and_then(|byte| char::from(*byte).to_digit(16))
348 {
349 accumulate(digit, false, &mut exponent);
350 digits += 1;
351 index += 1;
352 }
353 if bytes.get(index) == Some(&b'.') {
354 let mut fraction = index + 1;
355 let mut fraction_digits = 0;
356 while let Some(digit) = bytes
357 .get(fraction)
358 .and_then(|byte| char::from(*byte).to_digit(16))
359 {
360 accumulate(digit, true, &mut exponent);
361 fraction_digits += 1;
362 fraction += 1;
363 }
364 if digits + fraction_digits > 0 {
365 index = fraction;
366 digits += fraction_digits;
367 }
368 }
369 if digits == 0 {
370 return None;
371 }
372 if matches!(bytes.get(index), Some(b'p' | b'P')) {
373 let mut cursor = index + 1;
374 let negative = match bytes.get(cursor) {
375 Some(b'-') => {
376 cursor += 1;
377 true
378 }
379 Some(b'+') => {
380 cursor += 1;
381 false
382 }
383 _ => false,
384 };
385 if bytes.get(cursor).is_some_and(u8::is_ascii_digit) {
386 let mut binary: i64 = 0;
387 while let Some(byte) = bytes.get(cursor).filter(|byte| byte.is_ascii_digit()) {
388 binary = binary
389 .saturating_mul(10)
390 .saturating_add(i64::from(byte - b'0'));
391 cursor += 1;
392 }
393 exponent = exponent.saturating_add(if negative { -binary } else { binary });
394 index = cursor;
395 }
396 }
397 let mut remaining = exponent.clamp(-2200, 2200);
399 let mut value = mantissa as f64;
400 while remaining > 1000 {
401 value *= 2f64.powi(1000);
402 remaining -= 1000;
403 }
404 while remaining < -1000 {
405 value *= 2f64.powi(-1000);
406 remaining += 1000;
407 }
408 let remaining = i32::try_from(remaining).expect("exponent within one step");
409 Some((value * 2f64.powi(remaining), index, nonzero))
410}
411
412#[cfg(test)]
413mod tests;