1use std::time::{Duration, SystemTime};
2
3use crate::error::{Result, SyncwebError};
4
5pub fn parse_size_constraint(s: &str) -> Result<(Option<u64>, Option<u64>)> {
19 let trimmed = s.trim();
20 if trimmed.is_empty() {
21 return Ok((None, None));
22 }
23
24 if let Some((size_part, percent_part)) = trimmed.split_once('%') {
25 let base_size = parse_bytes(size_part)?;
26 let percent: u64 = percent_part
27 .parse::<u64>()
28 .map_err(|error| SyncwebError::InvalidConfig(format!("invalid percentage {percent_part:?}: {error}")))?;
29
30 let lower = base_size
31 .saturating_mul(100_u64.saturating_sub(percent))
32 .checked_div(100)
33 .unwrap_or(0);
34 let upper = base_size
35 .saturating_mul(100_u64.saturating_add(percent))
36 .checked_div(100)
37 .unwrap_or(0);
38 return Ok((Some(lower), Some(upper)));
39 }
40
41 let (prefix, value_str) = trimmed
42 .strip_prefix('+')
43 .map(|rest| ("+", rest))
44 .or_else(|| trimmed.strip_prefix('-').map(|rest| ("-", rest)))
45 .or_else(|| trimmed.strip_prefix('>').map(|rest| (">", rest)))
46 .or_else(|| trimmed.strip_prefix('<').map(|rest| ("<", rest)))
47 .or_else(|| trimmed.strip_prefix('=').map(|rest| ("=", rest)))
48 .unwrap_or(("=", trimmed));
49
50 let size = parse_bytes(value_str)?;
51 match prefix {
52 "+" | ">" => Ok((Some(size.saturating_add(1)), None)),
53 "-" | "<" => Ok((None, Some(size.saturating_sub(1)))),
54 _ => Ok((Some(size), Some(size))),
55 }
56}
57
58pub fn parse_bytes(s: &str) -> Result<u64> {
64 let trimmed = s.trim().to_ascii_lowercase();
65 let (number_str, multiplier) = split_size_suffix(&trimmed);
66
67 let parts: Vec<&str> = number_str.split('.').collect();
68 if parts.len() > 2 {
69 return Err(SyncwebError::InvalidConfig(format!("invalid size {s:?}")));
70 }
71
72 let integer_part: u64 = parts
73 .first()
74 .copied()
75 .unwrap_or("0")
76 .parse::<u64>()
77 .map_err(|error| SyncwebError::InvalidConfig(format!("invalid size {s:?}: {error}")))?;
78
79 let mut total = integer_part.saturating_mul(multiplier);
80
81 if parts.len() == 2 {
82 let fraction_str = parts.get(1).copied().unwrap_or("0");
83 let fraction_part: u64 = fraction_str
84 .parse::<u64>()
85 .map_err(|error| SyncwebError::InvalidConfig(format!("invalid size {s:?}: {error}")))?;
86
87 let fraction_divisor = 10_u64.pow(u32::try_from(fraction_str.len()).unwrap_or(0));
88 if fraction_divisor > 0 {
89 let fraction_bytes = multiplier
90 .saturating_mul(fraction_part)
91 .checked_div(fraction_divisor)
92 .unwrap_or(0);
93 total = total.saturating_add(fraction_bytes);
94 }
95 }
96
97 Ok(total)
98}
99
100#[must_use]
102fn split_size_suffix(value: &str) -> (&str, u64) {
103 if let Some(stripped) = value.strip_suffix("pib") {
104 return (stripped, 1024_u64.pow(5));
105 }
106 if let Some(stripped) = value.strip_suffix("tib") {
107 return (stripped, 1024_u64.pow(4));
108 }
109 if let Some(stripped) = value.strip_suffix("gib") {
110 return (stripped, 1024_u64.pow(3));
111 }
112 if let Some(stripped) = value.strip_suffix("mib") {
113 return (stripped, 1024_u64.pow(2));
114 }
115 if let Some(stripped) = value.strip_suffix("kib") {
116 return (stripped, 1024);
117 }
118 if let Some(stripped) = value.strip_suffix("pb") {
119 return (stripped, 1_000_000_000_000_000);
120 }
121 if let Some(stripped) = value.strip_suffix("tb") {
122 return (stripped, 1_000_000_000_000);
123 }
124 if let Some(stripped) = value.strip_suffix("gb") {
125 return (stripped, 1_000_000_000);
126 }
127 if let Some(stripped) = value.strip_suffix("mb") {
128 return (stripped, 1_000_000);
129 }
130 if let Some(stripped) = value.strip_suffix("kb") {
131 return (stripped, 1000);
132 }
133 if let Some(stripped) = value.strip_suffix('b') {
134 return (stripped, 1);
135 }
136 (value, 1)
137}
138
139pub fn parse_duration(s: &str) -> Result<Duration> {
145 let trimmed = s.trim().to_ascii_lowercase();
146 let (number_str, unit) = split_duration_parts(&trimmed);
147
148 let parts: Vec<&str> = number_str.split('.').collect();
149 if parts.len() > 2 {
150 return Err(SyncwebError::InvalidConfig(format!("invalid duration {s:?}")));
151 }
152
153 let integer_part: u64 = parts
154 .first()
155 .copied()
156 .unwrap_or("0")
157 .parse::<u64>()
158 .map_err(|error| SyncwebError::InvalidConfig(format!("invalid duration {s:?}: {error}")))?;
159
160 let multiplier = duration_multiplier(unit)?;
161 let mut total_secs = integer_part.saturating_mul(multiplier);
162
163 if parts.len() == 2 {
164 let fraction_str = parts.get(1).copied().unwrap_or("0");
165 let fraction_part: u64 = fraction_str
166 .parse::<u64>()
167 .map_err(|error| SyncwebError::InvalidConfig(format!("invalid duration {s:?}: {error}")))?;
168
169 let fraction_divisor = 10_u64.pow(u32::try_from(fraction_str.len()).unwrap_or(0));
170 if fraction_divisor > 0 {
171 let fraction_secs = multiplier
172 .saturating_mul(fraction_part)
173 .checked_div(fraction_divisor)
174 .unwrap_or(0);
175 total_secs = total_secs.saturating_add(fraction_secs);
176 }
177 }
178
179 Ok(Duration::from_secs(total_secs))
180}
181
182#[must_use]
184fn split_duration_parts(value: &str) -> (&str, &str) {
185 let unit_start = value
186 .char_indices()
187 .find_map(|(index, character)| character.is_ascii_alphabetic().then_some(index))
188 .unwrap_or(value.len());
189 let (number, unit_str) = value.split_at(unit_start);
190 let unit = unit_str.strip_suffix('s').unwrap_or(unit_str);
191 (number.trim(), unit)
192}
193
194fn duration_multiplier(unit: &str) -> Result<u64> {
196 match unit {
197 "s" | "sec" | "second" => Ok(1),
198 "m" | "min" | "minute" => Ok(60),
199 "h" | "hr" | "hour" => Ok(3600),
200 "d" | "day" => Ok(86400),
201 "w" | "week" => Ok(604_800),
202 "mo" | "mon" | "month" => Ok(2_592_000),
203 "y" | "yr" | "year" => Ok(31_536_000),
204 _ => Err(SyncwebError::InvalidConfig(format!(
205 "unsupported duration unit {unit:?}"
206 ))),
207 }
208}
209
210pub fn parse_relative_time(s: &str) -> Result<SystemTime> {
216 let trimmed = s.trim();
217 let (sign, duration_str) = trimmed.strip_prefix('-').map_or_else(
218 || trimmed.strip_prefix('+').map_or((1_i64, trimmed), |rest| (1_i64, rest)),
219 |rest| (-1_i64, rest),
220 );
221
222 let duration = parse_duration(duration_str)?;
223 let now = SystemTime::now();
224 if sign < 0 {
225 now.checked_sub(duration)
226 .ok_or_else(|| SyncwebError::InvalidConfig("time overflow".to_owned()))
227 } else {
228 now.checked_add(duration)
229 .ok_or_else(|| SyncwebError::InvalidConfig("time overflow".to_owned()))
230 }
231}
232
233#[must_use]
237pub fn parse_depth_constraints(
238 depth_list: &[String],
239 min_depth: usize,
240 max_depth: Option<usize>,
241) -> (usize, Option<usize>) {
242 let mut result_min = min_depth;
243 let mut result_max = max_depth;
244 for s in depth_list {
245 if let Some(rest) = s.strip_prefix('+') {
246 if let Ok(val) = rest.parse::<usize>() {
247 result_min = result_min.max(val);
248 }
249 continue;
250 }
251 if let Some(rest) = s.strip_prefix('-') {
252 if let Ok(val) = rest.parse::<usize>() {
253 result_max = Some(result_max.map_or(val, |current| current.min(val)));
254 }
255 continue;
256 }
257 if let Ok(val) = s.parse::<usize>() {
258 result_min = val;
259 result_max = Some(val);
260 }
261 }
262 (result_min, result_max)
263}
264
265#[must_use]
267pub fn current_unix_secs() -> u64 {
268 SystemTime::now()
269 .duration_since(SystemTime::UNIX_EPOCH)
270 .map_or(0, |duration| duration.as_secs())
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276
277 #[test]
278 fn test_parse_bytes() {
279 assert_eq!(parse_bytes("5GB").unwrap(), 5_000_000_000);
280 assert_eq!(parse_bytes("5Gib").unwrap(), 5 * 1024 * 1024 * 1024);
281 assert_eq!(parse_bytes("500KB").unwrap(), 500_000);
282 assert_eq!(parse_bytes("1024").unwrap(), 1024);
283 assert_eq!(parse_bytes("1024b").unwrap(), 1024);
284 }
285
286 #[test]
287 fn test_parse_size_constraint() {
288 assert_eq!(
289 parse_size_constraint("5GB").unwrap(),
290 (Some(5_000_000_000), Some(5_000_000_000))
291 );
292 assert_eq!(parse_size_constraint("+5GB").unwrap(), (Some(5_000_000_001), None));
293 assert_eq!(parse_size_constraint("-5GB").unwrap(), (None, Some(4_999_999_999)));
294 }
295
296 #[test]
297 fn test_parse_size_constraint_percentage() {
298 let (min, max) = parse_size_constraint("6MB%10").unwrap();
299 assert!(min.is_some());
300 assert!(max.is_some());
301 let min_val = min.unwrap();
302 let max_val = max.unwrap();
303 assert!(min_val < 6_000_000);
304 assert!(max_val > 6_000_000);
305 }
306
307 #[test]
308 fn test_parse_duration() {
309 assert_eq!(parse_duration("3d").unwrap(), Duration::from_hours(72));
310 assert_eq!(parse_duration("2 weeks").unwrap(), Duration::from_hours(336));
311 assert_eq!(parse_duration("1 year").unwrap(), Duration::from_hours(8760));
312 assert_eq!(parse_duration("30 sec").unwrap(), Duration::from_secs(30));
313 }
314
315 #[test]
316 fn test_parse_depth_constraints() {
317 let empty: Vec<String> = vec![];
318 assert_eq!(parse_depth_constraints(&empty, 0, None), (0, None));
319 assert_eq!(parse_depth_constraints(&["+2".to_owned()], 0, None), (2, None));
320 assert_eq!(parse_depth_constraints(&["-3".to_owned()], 0, None), (0, Some(3)));
321 assert_eq!(parse_depth_constraints(&["2".to_owned()], 0, None), (2, Some(2)));
322 assert_eq!(
323 parse_depth_constraints(&["+1".to_owned(), "-3".to_owned()], 0, None),
324 (1, Some(3))
325 );
326 }
327}