1use chrono::Duration;
30
31pub fn parse_duration(s: &str) -> Result<Duration, String> {
39 let s = s.trim();
40 if s.is_empty() {
41 return Err("duration is empty".into());
42 }
43 let split = s
44 .find(|c: char| !c.is_ascii_digit())
45 .ok_or_else(|| format!("duration '{s}' has no unit suffix (try 30s, 5m, 2h, 7d)"))?;
46 let (num, unit) = s.split_at(split);
47 if num.is_empty() {
48 return Err(format!("duration '{s}': missing number before unit"));
49 }
50 let n: i64 = num
51 .parse()
52 .map_err(|_| format!("duration '{s}': invalid number '{num}'"))?;
53 let unit = unit.trim();
54 let dur = match unit {
55 "ms" => Duration::milliseconds(n),
56 "s" | "sec" | "secs" => Duration::seconds(n),
57 "m" | "min" | "mins" => Duration::minutes(n),
58 "h" | "hr" | "hrs" => Duration::hours(n),
59 "d" | "day" | "days" => Duration::days(n),
60 other => return Err(format!("duration '{s}': unknown unit '{other}'")),
61 };
62 Ok(dur)
63}
64
65pub fn parse_size(s: &str) -> Result<u64, String> {
89 let s = s.trim();
90 if s.is_empty() {
91 return Err("size is empty".into());
92 }
93 let split = match s.find(|c: char| !c.is_ascii_digit()) {
94 None => {
96 return s.parse().map_err(|_| format!("size '{s}': invalid number"));
97 }
98 Some(i) => i,
99 };
100 let (num, unit) = s.split_at(split);
101 if num.is_empty() {
102 return Err(format!("size '{s}': missing number before unit"));
103 }
104 let n: u64 = num
105 .parse()
106 .map_err(|_| format!("size '{s}': invalid number '{num}'"))?;
107 let unit = unit.trim();
108 let mul: u64 = match unit {
109 "B" => 1,
110 "KB" => 1_000,
111 "MB" => 1_000_000,
112 "GB" => 1_000_000_000,
113 "KiB" => 1_024,
114 "MiB" => 1_024 * 1_024,
115 "GiB" => 1_024 * 1_024 * 1_024,
116 other => return Err(format!("size '{s}': unknown unit '{other}'")),
117 };
118 n.checked_mul(mul)
119 .ok_or_else(|| format!("size '{s}': value overflows u64 bytes"))
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn parses_every_unit_and_alias() {
128 assert_eq!(
129 parse_duration("250ms").unwrap(),
130 Duration::milliseconds(250)
131 );
132 assert_eq!(parse_duration("30s").unwrap(), Duration::seconds(30));
133 assert_eq!(parse_duration("30sec").unwrap(), Duration::seconds(30));
134 assert_eq!(parse_duration("30secs").unwrap(), Duration::seconds(30));
135 assert_eq!(parse_duration("5m").unwrap(), Duration::minutes(5));
136 assert_eq!(parse_duration("5min").unwrap(), Duration::minutes(5));
137 assert_eq!(parse_duration("90mins").unwrap(), Duration::minutes(90));
138 assert_eq!(parse_duration("2h").unwrap(), Duration::hours(2));
139 assert_eq!(parse_duration("2hr").unwrap(), Duration::hours(2));
140 assert_eq!(parse_duration("2hrs").unwrap(), Duration::hours(2));
141 assert_eq!(parse_duration("7d").unwrap(), Duration::days(7));
142 assert_eq!(parse_duration("7day").unwrap(), Duration::days(7));
143 assert_eq!(parse_duration("7days").unwrap(), Duration::days(7));
144 }
145
146 #[test]
147 fn trims_surrounding_whitespace() {
148 assert_eq!(parse_duration(" 5m ").unwrap(), Duration::minutes(5));
149 }
150
151 #[test]
152 fn rejects_bare_integer() {
153 assert!(parse_duration("10").is_err());
156 }
157
158 #[test]
159 fn rejects_empty_unit_and_unknown_inputs() {
160 assert!(parse_duration("").is_err());
161 assert!(parse_duration(" ").is_err());
162 assert!(parse_duration("hour").is_err()); assert!(parse_duration("ms").is_err()); assert!(parse_duration("10x").is_err()); assert!(parse_duration("-5m").is_err()); }
167
168 #[test]
169 fn unit_whitespace_is_tolerated() {
170 assert_eq!(parse_duration("5 m").unwrap(), Duration::minutes(5));
173 }
174
175 #[test]
176 fn error_messages_name_the_input() {
177 let err = parse_duration("10x").unwrap_err();
178 assert!(err.contains("10x"), "error should echo the input: {err}");
179 assert!(
180 err.contains("unknown unit"),
181 "error should name the fault: {err}"
182 );
183 }
184
185 #[test]
186 fn parse_size_bare_integer_is_bytes() {
187 assert_eq!(parse_size("0").unwrap(), 0);
188 assert_eq!(parse_size("1024").unwrap(), 1024);
189 }
190
191 #[test]
192 fn parse_size_decimal_si_units() {
193 assert_eq!(parse_size("1B").unwrap(), 1);
194 assert_eq!(parse_size("1KB").unwrap(), 1_000);
195 assert_eq!(parse_size("1MB").unwrap(), 1_000_000);
196 assert_eq!(parse_size("2GB").unwrap(), 2_000_000_000);
197 }
198
199 #[test]
200 fn parse_size_binary_iec_units() {
201 assert_eq!(parse_size("1KiB").unwrap(), 1_024);
202 assert_eq!(parse_size("1MiB").unwrap(), 1_048_576);
203 assert_eq!(parse_size("1GiB").unwrap(), 1_073_741_824);
204 }
205
206 #[test]
207 fn parse_size_trims_surrounding_whitespace() {
208 assert_eq!(parse_size(" 5MB ").unwrap(), 5_000_000);
209 }
210
211 #[test]
212 fn parse_size_rejects_bad_inputs() {
213 assert!(parse_size("").is_err());
214 assert!(parse_size(" ").is_err());
215 assert!(parse_size("abc").is_err()); assert!(parse_size("5x").is_err()); assert!(parse_size("-5MB").is_err()); }
219
220 #[test]
221 fn parse_size_oversized_value_errors_without_panic() {
222 let err = parse_size("99999999999GB").unwrap_err();
224 assert!(
225 err.contains("99999999999GB"),
226 "error should echo input: {err}"
227 );
228 assert!(
229 err.contains("overflow"),
230 "error should name overflow: {err}"
231 );
232 }
233
234 #[test]
235 fn parse_size_error_messages_name_the_input() {
236 let err = parse_size("5x").unwrap_err();
237 assert!(err.contains("5x"), "error should echo the input: {err}");
238 assert!(
239 err.contains("unknown unit"),
240 "error should name the fault: {err}"
241 );
242 }
243}