commonware_utils/
time.rs

1//! Utility functions for `std::time`.
2
3use rand::Rng;
4use std::time::{Duration, SystemTime};
5
6/// Parse a duration string with time unit suffixes.
7///
8/// This function accepts duration strings with the following suffixes:
9/// - `ms`: Milliseconds (e.g., "500ms", "1000ms")
10/// - `s`: Seconds (e.g., "30s", "5s")
11/// - `m`: Minutes (e.g., "2m", "30m")
12/// - `h`: Hours (e.g., "1h", "24h")
13///
14/// A suffix is required - strings without suffixes will return an error.
15///
16/// # Overflow Protection
17///
18/// The function includes overflow protection for time unit conversions:
19/// - Hours are safely converted to seconds (hours * 3600) with overflow checking
20/// - Minutes are safely converted to seconds (minutes * 60) with overflow checking
21/// - Values that would cause integer overflow return an error
22///
23/// # Arguments
24///
25/// * `s` - A string slice containing the duration with required suffix
26///
27/// # Returns
28///
29/// * `Ok(Duration)` - Successfully parsed duration
30/// * `Err(String)` - Error message describing what went wrong (invalid format, overflow, etc.)
31///
32/// # Examples
33///
34/// ```
35/// # use commonware_utils::parse_duration;
36/// # use std::time::Duration;
37///
38/// // Different time units
39/// assert_eq!(parse_duration("500ms").unwrap(), Duration::from_millis(500));
40/// assert_eq!(parse_duration("30s").unwrap(), Duration::from_secs(30));
41/// assert_eq!(parse_duration("5m").unwrap(), Duration::from_secs(300));
42/// assert_eq!(parse_duration("2h").unwrap(), Duration::from_secs(7200));
43///
44/// // Error cases
45/// assert!(parse_duration("invalid").is_err());
46/// assert!(parse_duration("10x").is_err());
47/// assert!(parse_duration("5minutes").is_err()); // Long forms not supported
48/// assert!(parse_duration("60").is_err()); // No suffix required
49///
50/// // Overflow protection
51/// let max_hours = u64::MAX / 3600;
52/// assert!(parse_duration(&format!("{}h", max_hours)).is_ok());     // At limit
53/// assert!(parse_duration(&format!("{}h", max_hours + 1)).is_err()); // Overflow
54/// ```
55pub fn parse_duration(s: &str) -> Result<Duration, String> {
56    let s = s.trim();
57
58    // Handle milliseconds
59    if let Some(num_str) = s.strip_suffix("ms") {
60        let millis: u64 = num_str
61            .trim()
62            .parse()
63            .map_err(|_| format!("Invalid milliseconds value: '{num_str}'"))?;
64        return Ok(Duration::from_millis(millis));
65    }
66
67    // Handle hours
68    if let Some(num_str) = s.strip_suffix("h") {
69        let hours: u64 = num_str
70            .trim()
71            .parse()
72            .map_err(|_| format!("Invalid hours value: '{num_str}'"))?;
73        let seconds = hours
74            .checked_mul(3600)
75            .ok_or_else(|| format!("Hours value too large (would overflow): '{hours}'"))?;
76        return Ok(Duration::from_secs(seconds));
77    }
78
79    // Handle minutes
80    if let Some(num_str) = s.strip_suffix("m") {
81        let minutes: u64 = num_str
82            .trim()
83            .parse()
84            .map_err(|_| format!("Invalid minutes value: '{num_str}'"))?;
85        let seconds = minutes
86            .checked_mul(60)
87            .ok_or_else(|| format!("Minutes value too large (would overflow): '{minutes}'"))?;
88        return Ok(Duration::from_secs(seconds));
89    }
90
91    // Handle seconds
92    if let Some(num_str) = s.strip_suffix("s") {
93        let secs: u64 = num_str
94            .trim()
95            .parse()
96            .map_err(|_| format!("Invalid seconds value: '{num_str}'"))?;
97        return Ok(Duration::from_secs(secs));
98    }
99
100    // No suffix - return error
101    Err(format!(
102        "Invalid duration format: '{s}'. A suffix is required. \
103         Supported formats: '123ms', '30s', '5m', '2h'"
104    ))
105}
106
107/// Extension trait to add methods to `std::time::SystemTime`
108pub trait SystemTimeExt {
109    /// Returns the duration since the Unix epoch.
110    ///
111    /// Panics if the system time is before the Unix epoch.
112    fn epoch(&self) -> Duration;
113
114    /// Returns the number of milliseconds (rounded down) since the Unix epoch.
115    ///
116    /// Panics if the system time is before the Unix epoch.
117    /// Saturates at `u64::MAX`.
118    fn epoch_millis(&self) -> u64;
119
120    /// Adds a random `Duration` to the current time between `0` and `jitter * 2` and returns the
121    /// resulting `SystemTime`. The random duration is generated using the provided `context`.
122    fn add_jittered(&self, rng: &mut impl Rng, jitter: Duration) -> SystemTime;
123}
124
125impl SystemTimeExt for SystemTime {
126    fn epoch(&self) -> Duration {
127        self.duration_since(std::time::UNIX_EPOCH)
128            .expect("failed to get epoch time")
129    }
130
131    fn epoch_millis(&self) -> u64 {
132        self.epoch().as_millis().min(u64::MAX as u128) as u64
133    }
134
135    fn add_jittered(&self, rng: &mut impl Rng, jitter: Duration) -> SystemTime {
136        *self + rng.gen_range(Duration::default()..=jitter * 2)
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn test_epoch() {
146        let time = SystemTime::UNIX_EPOCH;
147        assert_eq!(time.epoch(), Duration::from_secs(0));
148
149        let time = SystemTime::UNIX_EPOCH + Duration::from_secs(1) + Duration::from_millis(1);
150        assert_eq!(time.epoch(), Duration::from_millis(1_001));
151    }
152
153    #[test]
154    #[should_panic(expected = "failed to get epoch time")]
155    fn test_epoch_panics() {
156        let time = SystemTime::UNIX_EPOCH - Duration::from_secs(1);
157        time.epoch();
158    }
159
160    #[test]
161    fn test_epoch_millis() {
162        let time = SystemTime::UNIX_EPOCH;
163        assert_eq!(time.epoch_millis(), 0);
164
165        let time = SystemTime::UNIX_EPOCH + Duration::from_secs(1) + Duration::from_millis(1);
166        assert_eq!(time.epoch_millis(), 1_001);
167
168        // Rounds nanoseconds down
169        let time = SystemTime::UNIX_EPOCH + Duration::from_secs(1) + Duration::from_nanos(999_999);
170        assert_eq!(time.epoch_millis(), 1_000);
171
172        // Add 5 minutes
173        let time = SystemTime::UNIX_EPOCH + Duration::from_secs(300);
174        assert_eq!(time.epoch_millis(), 300_000);
175    }
176
177    #[test]
178    #[should_panic(expected = "failed to get epoch time")]
179    fn test_epoch_millis_panics() {
180        let time = SystemTime::UNIX_EPOCH - Duration::from_secs(1);
181        time.epoch_millis();
182    }
183
184    #[test]
185    fn test_add_jittered() {
186        let mut rng = rand::thread_rng();
187        let time = SystemTime::UNIX_EPOCH + Duration::from_secs(1);
188        let jitter = Duration::from_secs(2);
189
190        // Ensure we generate values both below and above the average time.
191        let (mut below, mut above) = (false, false);
192        let avg = time + jitter;
193        for _ in 0..100 {
194            let new_time = time.add_jittered(&mut rng, jitter);
195
196            // Record values higher or lower than the average
197            below |= new_time < avg;
198            above |= new_time > avg;
199
200            // Check bounds
201            assert!(new_time >= time);
202            assert!(new_time <= time + (jitter * 2));
203        }
204        assert!(below && above);
205    }
206
207    #[test]
208    fn test_parse_duration_milliseconds() {
209        assert_eq!(parse_duration("500ms").unwrap(), Duration::from_millis(500));
210        assert_eq!(parse_duration("0ms").unwrap(), Duration::from_millis(0));
211        assert_eq!(parse_duration("1ms").unwrap(), Duration::from_millis(1));
212        assert_eq!(
213            parse_duration("1000ms").unwrap(),
214            Duration::from_millis(1000)
215        );
216        assert_eq!(parse_duration("250ms").unwrap(), Duration::from_millis(250));
217    }
218
219    #[test]
220    fn test_parse_duration_seconds() {
221        assert_eq!(parse_duration("30s").unwrap(), Duration::from_secs(30));
222        assert_eq!(parse_duration("0s").unwrap(), Duration::from_secs(0));
223        assert_eq!(parse_duration("1s").unwrap(), Duration::from_secs(1));
224        assert_eq!(parse_duration("45s").unwrap(), Duration::from_secs(45));
225        assert_eq!(parse_duration("60s").unwrap(), Duration::from_secs(60));
226        assert_eq!(parse_duration("3600s").unwrap(), Duration::from_secs(3600));
227    }
228
229    #[test]
230    fn test_parse_duration_minutes() {
231        assert_eq!(parse_duration("5m").unwrap(), Duration::from_secs(300));
232        assert_eq!(parse_duration("1m").unwrap(), Duration::from_secs(60));
233        assert_eq!(parse_duration("0m").unwrap(), Duration::from_secs(0));
234        assert_eq!(parse_duration("10m").unwrap(), Duration::from_secs(600));
235        assert_eq!(parse_duration("15m").unwrap(), Duration::from_secs(900));
236        assert_eq!(parse_duration("30m").unwrap(), Duration::from_secs(1800));
237        assert_eq!(parse_duration("60m").unwrap(), Duration::from_secs(3600));
238    }
239
240    #[test]
241    fn test_parse_duration_hours() {
242        assert_eq!(parse_duration("2h").unwrap(), Duration::from_secs(7200));
243        assert_eq!(parse_duration("1h").unwrap(), Duration::from_secs(3600));
244        assert_eq!(parse_duration("0h").unwrap(), Duration::from_secs(0));
245        assert_eq!(parse_duration("3h").unwrap(), Duration::from_secs(10800));
246        assert_eq!(parse_duration("4h").unwrap(), Duration::from_secs(14400));
247        assert_eq!(parse_duration("12h").unwrap(), Duration::from_secs(43200));
248        assert_eq!(parse_duration("24h").unwrap(), Duration::from_secs(86400));
249        assert_eq!(parse_duration("168h").unwrap(), Duration::from_secs(604800));
250        // 1 week
251    }
252
253    #[test]
254    fn test_parse_duration_whitespace() {
255        // Should handle whitespace around the input
256        assert_eq!(parse_duration("  30s  ").unwrap(), Duration::from_secs(30));
257        assert_eq!(
258            parse_duration("\t500ms\n").unwrap(),
259            Duration::from_millis(500)
260        );
261        assert_eq!(parse_duration(" 2h ").unwrap(), Duration::from_secs(7200));
262
263        // Should handle whitespace between number and suffix
264        assert_eq!(parse_duration("30 s").unwrap(), Duration::from_secs(30));
265        assert_eq!(
266            parse_duration("500 ms").unwrap(),
267            Duration::from_millis(500)
268        );
269        assert_eq!(parse_duration("2 h").unwrap(), Duration::from_secs(7200));
270        assert_eq!(parse_duration("5 m").unwrap(), Duration::from_secs(300));
271    }
272
273    #[test]
274    fn test_parse_duration_error_cases() {
275        // Invalid number
276        assert!(parse_duration("invalid").is_err());
277        assert!(parse_duration("abc123ms").is_err());
278        assert!(parse_duration("12.5s").is_err()); // Decimal not supported
279
280        // Invalid suffix
281        assert!(parse_duration("10x").is_err());
282        assert!(parse_duration("30days").is_err());
283        assert!(parse_duration("5y").is_err());
284
285        // Long forms not supported
286        assert!(parse_duration("5minutes").is_err());
287        assert!(parse_duration("10seconds").is_err());
288        assert!(parse_duration("2hours").is_err());
289        assert!(parse_duration("500millis").is_err());
290        assert!(parse_duration("30sec").is_err());
291        assert!(parse_duration("5min").is_err());
292        assert!(parse_duration("2hr").is_err());
293
294        // No suffix
295        assert!(parse_duration("60").is_err());
296        assert!(parse_duration("0").is_err());
297        assert!(parse_duration("3600").is_err());
298        assert!(parse_duration("1").is_err());
299
300        // Empty or whitespace only
301        assert!(parse_duration("").is_err());
302        assert!(parse_duration("   ").is_err());
303
304        // Negative numbers (should fail because we use u64)
305        assert!(parse_duration("-5s").is_err());
306        assert!(parse_duration("-100ms").is_err());
307
308        // Mixed case should not work (we only support lowercase)
309        assert!(parse_duration("30S").is_err());
310        assert!(parse_duration("500MS").is_err());
311        assert!(parse_duration("2H").is_err());
312    }
313
314    #[test]
315    fn test_parse_duration_large_values() {
316        // Large values that don't overflow
317        assert_eq!(
318            parse_duration("999999999ms").unwrap(),
319            Duration::from_millis(999999999)
320        );
321        assert_eq!(
322            parse_duration("99999999s").unwrap(),
323            Duration::from_secs(99999999)
324        );
325    }
326
327    #[test]
328    fn test_parse_duration_overflow_cases() {
329        // Test hours overflow
330        let max_safe_hours = u64::MAX / 3600;
331        let overflow_hours = max_safe_hours + 1;
332        assert!(parse_duration(&format!("{max_safe_hours}h")).is_ok());
333        match parse_duration(&format!("{overflow_hours}h")) {
334            Err(msg) => assert!(msg.contains("too large (would overflow)")),
335            Ok(_) => panic!("Expected overflow error for large hours value"),
336        }
337        match parse_duration(&format!("{}h", u64::MAX)) {
338            Err(msg) => assert!(msg.contains("too large (would overflow)")),
339            Ok(_) => panic!("Expected overflow error for u64::MAX hours"),
340        }
341
342        // Test minutes overflow
343        let max_safe_minutes = u64::MAX / 60;
344        let overflow_minutes = max_safe_minutes + 1;
345        assert!(parse_duration(&format!("{max_safe_minutes}m")).is_ok());
346        match parse_duration(&format!("{overflow_minutes}m")) {
347            Err(msg) => assert!(msg.contains("too large (would overflow)")),
348            Ok(_) => panic!("Expected overflow error for large minutes value"),
349        }
350        match parse_duration(&format!("{}m", u64::MAX)) {
351            Err(msg) => assert!(msg.contains("too large (would overflow)")),
352            Ok(_) => panic!("Expected overflow error for u64::MAX minutes"),
353        }
354    }
355}