1pub fn parse_duration(s: &str) -> Result<u64, String> {
5 let s = s.trim();
6 if s.is_empty() {
7 return Err("Empty duration string".to_string());
8 }
9
10 if let Some(n) = s.strip_suffix("ms") {
11 n.parse::<u64>()
12 .map_err(|_| format!("Invalid milliseconds: {n}"))
13 } else if let Some(n) = s.strip_suffix('s') {
14 n.parse::<u64>()
15 .map(|v| v * 1000)
16 .map_err(|_| format!("Invalid seconds: {n}"))
17 } else if let Some(n) = s.strip_suffix('m') {
18 n.parse::<u64>()
19 .map(|v| v * 60 * 1000)
20 .map_err(|_| format!("Invalid minutes: {n}"))
21 } else if let Some(n) = s.strip_suffix('h') {
22 n.parse::<u64>()
23 .map(|v| v * 3600 * 1000)
24 .map_err(|_| format!("Invalid hours: {n}"))
25 } else if let Some(n) = s.strip_suffix('d') {
26 n.parse::<u64>()
27 .map(|v| v * 86400 * 1000)
28 .map_err(|_| format!("Invalid days: {n}"))
29 } else {
30 s.parse::<u64>()
32 .map_err(|_| format!("Invalid duration: {s}. Use suffixes: ms, s, m, h, d"))
33 }
34}
35
36pub fn validate_cron(expr: &str) -> Result<(), String> {
38 let fields: Vec<&str> = expr.split_whitespace().collect();
39 if fields.len() != 5 {
40 return Err(format!(
41 "Cron expression must have 5 fields (min hour dom month dow), got {}",
42 fields.len()
43 ));
44 }
45
46 let ranges = [(0, 59), (0, 23), (1, 31), (1, 12), (0, 7)];
47 let names = ["minute", "hour", "day-of-month", "month", "day-of-week"];
48
49 for (i, field) in fields.iter().enumerate() {
50 validate_cron_field(field, ranges[i].0, ranges[i].1, names[i])?;
51 }
52
53 Ok(())
54}
55
56fn validate_cron_field(field: &str, min: u32, max: u32, name: &str) -> Result<(), String> {
57 if field == "*" {
58 return Ok(());
59 }
60
61 if let Some((range_part, step_part)) = field.split_once('/') {
63 if range_part != "*" {
64 validate_cron_range(range_part, min, max, name)?;
65 }
66 step_part
67 .parse::<u32>()
68 .map_err(|_| format!("Invalid step in {name} field: {step_part}"))?;
69 return Ok(());
70 }
71
72 for part in field.split(',') {
74 validate_cron_range(part, min, max, name)?;
75 }
76
77 Ok(())
78}
79
80fn validate_cron_range(part: &str, min: u32, max: u32, name: &str) -> Result<(), String> {
81 if let Some((start_s, end_s)) = part.split_once('-') {
82 let start: u32 = start_s
83 .parse()
84 .map_err(|_| format!("Invalid range start in {name}: {start_s}"))?;
85 let end: u32 = end_s
86 .parse()
87 .map_err(|_| format!("Invalid range end in {name}: {end_s}"))?;
88 if start < min || end > max || start > end {
89 return Err(format!(
90 "Range {start}-{end} out of bounds for {name} ({min}-{max})"
91 ));
92 }
93 } else {
94 let val: u32 = part
95 .parse()
96 .map_err(|_| format!("Invalid value in {name}: {part}"))?;
97 if val < min || val > max {
98 return Err(format!(
99 "Value {val} out of bounds for {name} ({min}-{max})"
100 ));
101 }
102 }
103 Ok(())
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 #[test]
111 fn test_parse_duration_ms() {
112 assert_eq!(parse_duration("100ms").unwrap(), 100);
113 }
114
115 #[test]
116 fn test_parse_duration_seconds() {
117 assert_eq!(parse_duration("30s").unwrap(), 30_000);
118 }
119
120 #[test]
121 fn test_parse_duration_minutes() {
122 assert_eq!(parse_duration("5m").unwrap(), 300_000);
123 }
124
125 #[test]
126 fn test_parse_duration_hours() {
127 assert_eq!(parse_duration("2h").unwrap(), 7_200_000);
128 }
129
130 #[test]
131 fn test_parse_duration_days() {
132 assert_eq!(parse_duration("1d").unwrap(), 86_400_000);
133 }
134
135 #[test]
136 fn test_parse_duration_plain_ms() {
137 assert_eq!(parse_duration("5000").unwrap(), 5000);
138 }
139
140 #[test]
141 fn test_parse_duration_invalid() {
142 assert!(parse_duration("abc").is_err());
143 assert!(parse_duration("").is_err());
144 }
145
146 #[test]
147 fn test_validate_cron_basic() {
148 assert!(validate_cron("0 0 * * *").is_ok()); assert!(validate_cron("*/5 * * * *").is_ok()); assert!(validate_cron("0 9 * * 1-5").is_ok()); assert!(validate_cron("30 14 1 * *").is_ok()); }
153
154 #[test]
155 fn test_validate_cron_invalid() {
156 assert!(validate_cron("0 0 *").is_err()); assert!(validate_cron("60 0 * * *").is_err()); assert!(validate_cron("0 25 * * *").is_err()); }
160}