Skip to main content

commonware_utils/
time.rs

1//! Utility functions for `std::time`.
2
3use rand::{Rng, RngExt as _};
4use std::time::{Duration, SystemTime};
5
6/// Number of nanoseconds in a second.
7pub const NANOS_PER_SEC: u128 = 1_000_000_000;
8
9/// Maximum duration that can be safely added to [`SystemTime::UNIX_EPOCH`] without overflow on the
10/// current platform.
11///
12/// Source: `SystemTime` on Unix stores seconds in a signed 64-bit integer; see
13/// [`std::sys::pal::unix::time`](https://github.com/rust-lang/rust/blob/master/library/std/src/sys/pal/unix/time.rs),
14/// which bounds additions at `i64::MAX` seconds plus 999_999_999 nanoseconds.
15pub const MAX_DURATION_SINCE_UNIX_EPOCH: Duration = Duration::new(i64::MAX as u64, 999_999_999);
16
17/// The precision of [`SystemTime`] on supported platforms.
18pub const SYSTEM_TIME_PRECISION: Duration = Duration::from_nanos(1);
19
20/// Extension trait providing additional functionality for [`Duration`].
21pub trait DurationExt {
22    /// Creates a duration from nanoseconds represented as a `u128`. Saturates anything beyond the
23    /// representable range.
24    fn from_nanos_saturating(ns: u128) -> Duration;
25
26    /// Parse a duration string with time unit suffixes.
27    ///
28    /// This function accepts duration strings with the following suffixes:
29    /// - `ms`: Milliseconds (e.g., "500ms", "1000ms")
30    /// - `s`: Seconds (e.g., "30s", "5s")
31    /// - `m`: Minutes (e.g., "2m", "30m")
32    /// - `h`: Hours (e.g., "1h", "24h")
33    ///
34    /// A suffix is required - strings without suffixes will return an error.
35    ///
36    /// # Overflow Protection
37    ///
38    /// The function includes overflow protection for time unit conversions:
39    /// - Hours are safely converted to seconds (hours * 3600) with overflow checking
40    /// - Minutes are safely converted to seconds (minutes * 60) with overflow checking
41    /// - Values that would cause integer overflow return an error
42    ///
43    /// # Arguments
44    ///
45    /// * `s` - A string slice containing the duration with required suffix
46    ///
47    /// # Returns
48    ///
49    /// * `Ok(Duration)` - Successfully parsed duration
50    /// * `Err(String)` - Error message describing what went wrong (invalid format, overflow, etc.)
51    ///
52    /// # Examples
53    ///
54    /// ```
55    /// # use commonware_utils::DurationExt;
56    /// # use std::time::Duration;
57    ///
58    /// // Different time units
59    /// assert_eq!(Duration::parse("500ms").unwrap(), Duration::from_millis(500));
60    /// assert_eq!(Duration::parse("30s").unwrap(), Duration::from_secs(30));
61    /// assert_eq!(Duration::parse("5m").unwrap(), Duration::from_secs(300));
62    /// assert_eq!(Duration::parse("2h").unwrap(), Duration::from_secs(7200));
63    ///
64    /// // Error cases
65    /// assert!(Duration::parse("invalid").is_err());
66    /// assert!(Duration::parse("10x").is_err());
67    /// assert!(Duration::parse("5minutes").is_err()); // Long forms not supported
68    /// assert!(Duration::parse("60").is_err()); // No suffix required
69    ///
70    /// // Overflow protection
71    /// let max_hours = u64::MAX / 3600;
72    /// assert!(Duration::parse(&format!("{}h", max_hours)).is_ok());      // At limit
73    /// assert!(Duration::parse(&format!("{}h", max_hours + 1)).is_err()); // Overflow
74    /// ```
75    fn parse(s: &str) -> Result<Duration, String>;
76}
77
78impl DurationExt for Duration {
79    fn from_nanos_saturating(ns: u128) -> Duration {
80        // Clamp anything beyond the representable range
81        if ns > Self::MAX.as_nanos() {
82            return Self::MAX;
83        }
84
85        // Convert to `Duration`
86        let secs = (ns / NANOS_PER_SEC) as u64;
87        let nanos = (ns % NANOS_PER_SEC) as u32;
88        Self::new(secs, nanos)
89    }
90
91    fn parse(s: &str) -> Result<Duration, String> {
92        let s = s.trim();
93
94        // Handle milliseconds
95        if let Some(num_str) = s.strip_suffix("ms") {
96            let millis: u64 = num_str
97                .trim()
98                .parse()
99                .map_err(|_| format!("Invalid milliseconds value: '{num_str}'"))?;
100            return Ok(Self::from_millis(millis));
101        }
102
103        // Handle hours
104        if let Some(num_str) = s.strip_suffix("h") {
105            let hours: u64 = num_str
106                .trim()
107                .parse()
108                .map_err(|_| format!("Invalid hours value: '{num_str}'"))?;
109            let seconds = hours
110                .checked_mul(3600)
111                .ok_or_else(|| format!("Hours value too large (would overflow): '{hours}'"))?;
112            return Ok(Self::from_secs(seconds));
113        }
114
115        // Handle minutes
116        if let Some(num_str) = s.strip_suffix("m") {
117            let minutes: u64 = num_str
118                .trim()
119                .parse()
120                .map_err(|_| format!("Invalid minutes value: '{num_str}'"))?;
121            let seconds = minutes
122                .checked_mul(60)
123                .ok_or_else(|| format!("Minutes value too large (would overflow): '{minutes}'"))?;
124            return Ok(Self::from_secs(seconds));
125        }
126
127        // Handle seconds
128        if let Some(num_str) = s.strip_suffix("s") {
129            let secs: u64 = num_str
130                .trim()
131                .parse()
132                .map_err(|_| format!("Invalid seconds value: '{num_str}'"))?;
133            return Ok(Self::from_secs(secs));
134        }
135
136        // No suffix - return error
137        Err(format!(
138            "Invalid duration format: '{s}'. A suffix is required. \
139         Supported formats: '123ms', '30s', '5m', '2h'"
140        ))
141    }
142}
143
144/// Extension trait to add methods to `std::time::SystemTime`
145pub trait SystemTimeExt {
146    /// Returns the duration since the Unix epoch.
147    ///
148    /// Panics if the system time is before the Unix epoch.
149    fn epoch(&self) -> Duration;
150
151    /// Returns the number of milliseconds (rounded down) since the Unix epoch.
152    ///
153    /// Panics if the system time is before the Unix epoch.
154    /// Saturates at `u64::MAX`.
155    fn epoch_millis(&self) -> u64;
156
157    /// Adds a random `Duration` to the current time between `0` and `jitter * 2` and returns the
158    /// resulting `SystemTime`. The random duration is generated using the provided `context`.
159    fn add_jittered(&self, rng: &mut impl Rng, jitter: Duration) -> SystemTime;
160
161    /// Returns the maximum representable [SystemTime] on this platform.
162    fn limit() -> SystemTime;
163
164    /// Adds `delta` to the current time, saturating at the platform maximum instead of overflowing.
165    fn saturating_add_ext(&self, delta: Duration) -> SystemTime;
166}
167
168impl SystemTimeExt for SystemTime {
169    fn epoch(&self) -> Duration {
170        self.duration_since(std::time::UNIX_EPOCH)
171            .expect("failed to get epoch time")
172    }
173
174    fn epoch_millis(&self) -> u64 {
175        self.epoch().as_millis().min(u64::MAX as u128) as u64
176    }
177
178    fn add_jittered(&self, rng: &mut impl Rng, jitter: Duration) -> SystemTime {
179        *self + rng.random_range(Duration::default()..=jitter * 2)
180    }
181
182    fn limit() -> SystemTime {
183        Self::UNIX_EPOCH
184            .checked_add(MAX_DURATION_SINCE_UNIX_EPOCH)
185            .expect("maximum system time must be representable")
186    }
187
188    fn saturating_add_ext(&self, delta: Duration) -> SystemTime {
189        if delta.is_zero() {
190            return *self;
191        }
192
193        // When adding less than SYSTEM_TIME_PRECISION, this may actually not fail but simply
194        // round down to the nearest representable value
195        self.checked_add(delta).unwrap_or_else(Self::limit)
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202    use crate::test_rng;
203
204    #[test]
205    fn test_epoch() {
206        let time = SystemTime::UNIX_EPOCH;
207        assert_eq!(time.epoch(), Duration::from_secs(0));
208
209        let time = SystemTime::UNIX_EPOCH + Duration::from_secs(1) + Duration::from_millis(1);
210        assert_eq!(time.epoch(), Duration::from_millis(1_001));
211    }
212
213    #[test]
214    #[should_panic(expected = "failed to get epoch time")]
215    fn test_epoch_panics() {
216        let time = SystemTime::UNIX_EPOCH - Duration::from_secs(1);
217        time.epoch();
218    }
219
220    #[test]
221    fn test_epoch_millis() {
222        let time = SystemTime::UNIX_EPOCH;
223        assert_eq!(time.epoch_millis(), 0);
224
225        let time = SystemTime::UNIX_EPOCH + Duration::from_secs(1) + Duration::from_millis(1);
226        assert_eq!(time.epoch_millis(), 1_001);
227
228        // Rounds nanoseconds down
229        let time = SystemTime::UNIX_EPOCH + Duration::from_secs(1) + Duration::from_nanos(999_999);
230        assert_eq!(time.epoch_millis(), 1_000);
231
232        // Add 5 minutes
233        let time = SystemTime::UNIX_EPOCH + Duration::from_secs(300);
234        assert_eq!(time.epoch_millis(), 300_000);
235    }
236
237    #[test]
238    #[should_panic(expected = "failed to get epoch time")]
239    fn test_epoch_millis_panics() {
240        let time = SystemTime::UNIX_EPOCH - Duration::from_secs(1);
241        time.epoch_millis();
242    }
243
244    #[test]
245    fn test_from_nanos_saturating() {
246        // Support simple cases
247        assert_eq!(Duration::from_nanos_saturating(0), Duration::new(0, 0));
248        assert_eq!(
249            Duration::from_nanos_saturating(NANOS_PER_SEC - 1),
250            Duration::new(0, (NANOS_PER_SEC - 1) as u32)
251        );
252        assert_eq!(
253            Duration::from_nanos_saturating(NANOS_PER_SEC + 1),
254            Duration::new(1, 1)
255        );
256
257        // Support larger values than `Duration::from_nanos`
258        let std = Duration::from_nanos(u64::MAX);
259        let beyond_std = Duration::from_nanos_saturating(u64::MAX as u128 + 1);
260        assert!(beyond_std > std);
261
262        // Test very large values
263        assert_eq!(
264            Duration::from_nanos_saturating(Duration::MAX.as_nanos()),
265            Duration::MAX
266        );
267
268        // Clamp anything beyond the representable range
269        assert_eq!(
270            Duration::from_nanos_saturating(Duration::MAX.as_nanos() + 1),
271            Duration::MAX
272        );
273        assert_eq!(Duration::from_nanos_saturating(u128::MAX), Duration::MAX);
274    }
275
276    #[test]
277    fn test_add_jittered() {
278        let mut rng = test_rng();
279        let time = SystemTime::UNIX_EPOCH + Duration::from_secs(1);
280        let jitter = Duration::from_secs(2);
281
282        // Ensure we generate values both below and above the average time.
283        let (mut below, mut above) = (false, false);
284        let avg = time + jitter;
285        for _ in 0..100 {
286            let new_time = time.add_jittered(&mut rng, jitter);
287
288            // Record values higher or lower than the average
289            below |= new_time < avg;
290            above |= new_time > avg;
291
292            // Check bounds
293            assert!(new_time >= time);
294            assert!(new_time <= time + (jitter * 2));
295        }
296        assert!(below && above);
297    }
298
299    #[test]
300    fn check_duration_limit() {
301        // Rollback to limit
302        let result = SystemTime::limit()
303            .checked_add(SYSTEM_TIME_PRECISION - Duration::from_nanos(1))
304            .expect("addition within precision should round down");
305        assert_eq!(result, SystemTime::limit(), "unexpected precision");
306
307        // Exceed limit
308        let result = SystemTime::limit().checked_add(SYSTEM_TIME_PRECISION);
309        assert!(result.is_none(), "able to exceed max duration");
310    }
311
312    #[test]
313    fn system_time_saturating_add() {
314        let max = SystemTime::limit();
315        assert_eq!(max.saturating_add_ext(Duration::from_nanos(1)), max);
316        assert_eq!(max.saturating_add_ext(Duration::from_secs(1)), max);
317    }
318
319    #[test]
320    fn test_duration_parse_milliseconds() {
321        assert_eq!(
322            Duration::parse("500ms").unwrap(),
323            Duration::from_millis(500)
324        );
325        assert_eq!(Duration::parse("0ms").unwrap(), Duration::from_millis(0));
326        assert_eq!(Duration::parse("1ms").unwrap(), Duration::from_millis(1));
327        assert_eq!(
328            Duration::parse("1000ms").unwrap(),
329            Duration::from_millis(1000)
330        );
331        assert_eq!(
332            Duration::parse("250ms").unwrap(),
333            Duration::from_millis(250)
334        );
335    }
336
337    #[test]
338    fn test_duration_parse_seconds() {
339        assert_eq!(Duration::parse("30s").unwrap(), Duration::from_secs(30));
340        assert_eq!(Duration::parse("0s").unwrap(), Duration::from_secs(0));
341        assert_eq!(Duration::parse("1s").unwrap(), Duration::from_secs(1));
342        assert_eq!(Duration::parse("45s").unwrap(), Duration::from_secs(45));
343        assert_eq!(Duration::parse("60s").unwrap(), Duration::from_secs(60));
344        assert_eq!(Duration::parse("3600s").unwrap(), Duration::from_secs(3600));
345    }
346
347    #[test]
348    fn test_duration_parse_minutes() {
349        assert_eq!(Duration::parse("5m").unwrap(), Duration::from_secs(300));
350        assert_eq!(Duration::parse("1m").unwrap(), Duration::from_secs(60));
351        assert_eq!(Duration::parse("0m").unwrap(), Duration::from_secs(0));
352        assert_eq!(Duration::parse("10m").unwrap(), Duration::from_secs(600));
353        assert_eq!(Duration::parse("15m").unwrap(), Duration::from_secs(900));
354        assert_eq!(Duration::parse("30m").unwrap(), Duration::from_secs(1800));
355        assert_eq!(Duration::parse("60m").unwrap(), Duration::from_secs(3600));
356    }
357
358    #[test]
359    fn test_duration_parse_hours() {
360        assert_eq!(Duration::parse("2h").unwrap(), Duration::from_secs(7200));
361        assert_eq!(Duration::parse("1h").unwrap(), Duration::from_secs(3600));
362        assert_eq!(Duration::parse("0h").unwrap(), Duration::from_secs(0));
363        assert_eq!(Duration::parse("3h").unwrap(), Duration::from_secs(10800));
364        assert_eq!(Duration::parse("4h").unwrap(), Duration::from_secs(14400));
365        assert_eq!(Duration::parse("12h").unwrap(), Duration::from_secs(43200));
366        assert_eq!(Duration::parse("24h").unwrap(), Duration::from_secs(86400));
367        assert_eq!(
368            Duration::parse("168h").unwrap(),
369            Duration::from_secs(604800)
370        );
371        // 1 week
372    }
373
374    #[test]
375    fn test_duration_parse_whitespace() {
376        // Should handle whitespace around the input
377        assert_eq!(Duration::parse("  30s  ").unwrap(), Duration::from_secs(30));
378        assert_eq!(
379            Duration::parse("\t500ms\n").unwrap(),
380            Duration::from_millis(500)
381        );
382        assert_eq!(Duration::parse(" 2h ").unwrap(), Duration::from_secs(7200));
383
384        // Should handle whitespace between number and suffix
385        assert_eq!(Duration::parse("30 s").unwrap(), Duration::from_secs(30));
386        assert_eq!(
387            Duration::parse("500 ms").unwrap(),
388            Duration::from_millis(500)
389        );
390        assert_eq!(Duration::parse("2 h").unwrap(), Duration::from_secs(7200));
391        assert_eq!(Duration::parse("5 m").unwrap(), Duration::from_secs(300));
392    }
393
394    #[test]
395    fn test_duration_parse_error_cases() {
396        // Invalid number
397        assert!(Duration::parse("invalid").is_err());
398        assert!(Duration::parse("abc123ms").is_err());
399        assert!(Duration::parse("12.5s").is_err()); // Decimal not supported
400
401        // Invalid suffix
402        assert!(Duration::parse("10x").is_err());
403        assert!(Duration::parse("30days").is_err());
404        assert!(Duration::parse("5y").is_err());
405
406        // Long forms not supported
407        assert!(Duration::parse("5minutes").is_err());
408        assert!(Duration::parse("10seconds").is_err());
409        assert!(Duration::parse("2hours").is_err());
410        assert!(Duration::parse("500millis").is_err());
411        assert!(Duration::parse("30sec").is_err());
412        assert!(Duration::parse("5min").is_err());
413        assert!(Duration::parse("2hr").is_err());
414
415        // No suffix
416        assert!(Duration::parse("60").is_err());
417        assert!(Duration::parse("0").is_err());
418        assert!(Duration::parse("3600").is_err());
419        assert!(Duration::parse("1").is_err());
420
421        // Empty or whitespace only
422        assert!(Duration::parse("").is_err());
423        assert!(Duration::parse("   ").is_err());
424
425        // Negative numbers (should fail because we use u64)
426        assert!(Duration::parse("-5s").is_err());
427        assert!(Duration::parse("-100ms").is_err());
428
429        // Mixed case should not work (we only support lowercase)
430        assert!(Duration::parse("30S").is_err());
431        assert!(Duration::parse("500MS").is_err());
432        assert!(Duration::parse("2H").is_err());
433    }
434
435    #[test]
436    fn test_duration_parse_large_values() {
437        // Large values that don't overflow
438        assert_eq!(
439            Duration::parse("999999999ms").unwrap(),
440            Duration::from_millis(999999999)
441        );
442        assert_eq!(
443            Duration::parse("99999999s").unwrap(),
444            Duration::from_secs(99999999)
445        );
446    }
447
448    #[test]
449    fn test_duration_parse_overflow_cases() {
450        // Test hours overflow
451        let max_safe_hours = u64::MAX / 3600;
452        let overflow_hours = max_safe_hours + 1;
453        assert!(Duration::parse(&format!("{max_safe_hours}h")).is_ok());
454        match Duration::parse(&format!("{overflow_hours}h")) {
455            Err(msg) => assert!(msg.contains("too large (would overflow)")),
456            Ok(_) => panic!("Expected overflow error for large hours value"),
457        }
458        match Duration::parse(&format!("{}h", u64::MAX)) {
459            Err(msg) => assert!(msg.contains("too large (would overflow)")),
460            Ok(_) => panic!("Expected overflow error for u64::MAX hours"),
461        }
462
463        // Test minutes overflow
464        let max_safe_minutes = u64::MAX / 60;
465        let overflow_minutes = max_safe_minutes + 1;
466        assert!(Duration::parse(&format!("{max_safe_minutes}m")).is_ok());
467        match Duration::parse(&format!("{overflow_minutes}m")) {
468            Err(msg) => assert!(msg.contains("too large (would overflow)")),
469            Ok(_) => panic!("Expected overflow error for large minutes value"),
470        }
471        match Duration::parse(&format!("{}m", u64::MAX)) {
472            Err(msg) => assert!(msg.contains("too large (would overflow)")),
473            Ok(_) => panic!("Expected overflow error for u64::MAX minutes"),
474        }
475    }
476}