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