Skip to main content

commonware_utils/
time.rs

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