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 > Duration::MAX.as_nanos() {
98            return Duration::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        Duration::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(Duration::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(Duration::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(Duration::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(Duration::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        SystemTime::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
219    #[test]
220    fn test_epoch() {
221        let time = SystemTime::UNIX_EPOCH;
222        assert_eq!(time.epoch(), Duration::from_secs(0));
223
224        let time = SystemTime::UNIX_EPOCH + Duration::from_secs(1) + Duration::from_millis(1);
225        assert_eq!(time.epoch(), Duration::from_millis(1_001));
226    }
227
228    #[test]
229    #[should_panic(expected = "failed to get epoch time")]
230    fn test_epoch_panics() {
231        let time = SystemTime::UNIX_EPOCH - Duration::from_secs(1);
232        time.epoch();
233    }
234
235    #[test]
236    fn test_epoch_millis() {
237        let time = SystemTime::UNIX_EPOCH;
238        assert_eq!(time.epoch_millis(), 0);
239
240        let time = SystemTime::UNIX_EPOCH + Duration::from_secs(1) + Duration::from_millis(1);
241        assert_eq!(time.epoch_millis(), 1_001);
242
243        // Rounds nanoseconds down
244        let time = SystemTime::UNIX_EPOCH + Duration::from_secs(1) + Duration::from_nanos(999_999);
245        assert_eq!(time.epoch_millis(), 1_000);
246
247        // Add 5 minutes
248        let time = SystemTime::UNIX_EPOCH + Duration::from_secs(300);
249        assert_eq!(time.epoch_millis(), 300_000);
250    }
251
252    #[test]
253    #[should_panic(expected = "failed to get epoch time")]
254    fn test_epoch_millis_panics() {
255        let time = SystemTime::UNIX_EPOCH - Duration::from_secs(1);
256        time.epoch_millis();
257    }
258
259    #[test]
260    fn test_from_nanos_saturating() {
261        // Support simple cases
262        assert_eq!(Duration::from_nanos_saturating(0), Duration::new(0, 0));
263        assert_eq!(
264            Duration::from_nanos_saturating(NANOS_PER_SEC - 1),
265            Duration::new(0, (NANOS_PER_SEC - 1) as u32)
266        );
267        assert_eq!(
268            Duration::from_nanos_saturating(NANOS_PER_SEC + 1),
269            Duration::new(1, 1)
270        );
271
272        // Support larger values than `Duration::from_nanos`
273        let std = Duration::from_nanos(u64::MAX);
274        let beyond_std = Duration::from_nanos_saturating(u64::MAX as u128 + 1);
275        assert!(beyond_std > std);
276
277        // Test very large values
278        assert_eq!(
279            Duration::from_nanos_saturating(Duration::MAX.as_nanos()),
280            Duration::MAX
281        );
282
283        // Clamp anything beyond the representable range
284        assert_eq!(
285            Duration::from_nanos_saturating(Duration::MAX.as_nanos() + 1),
286            Duration::MAX
287        );
288        assert_eq!(Duration::from_nanos_saturating(u128::MAX), Duration::MAX);
289    }
290
291    #[test]
292    fn test_add_jittered() {
293        let mut rng = rand::thread_rng();
294        let time = SystemTime::UNIX_EPOCH + Duration::from_secs(1);
295        let jitter = Duration::from_secs(2);
296
297        // Ensure we generate values both below and above the average time.
298        let (mut below, mut above) = (false, false);
299        let avg = time + jitter;
300        for _ in 0..100 {
301            let new_time = time.add_jittered(&mut rng, jitter);
302
303            // Record values higher or lower than the average
304            below |= new_time < avg;
305            above |= new_time > avg;
306
307            // Check bounds
308            assert!(new_time >= time);
309            assert!(new_time <= time + (jitter * 2));
310        }
311        assert!(below && above);
312    }
313
314    #[test]
315    fn check_duration_limit() {
316        // Rollback to limit
317        let result = SystemTime::limit()
318            .checked_add(SYSTEM_TIME_PRECISION - Duration::from_nanos(1))
319            .expect("addition within precision should round down");
320        assert_eq!(result, SystemTime::limit(), "unexpected precision");
321
322        // Exceed limit
323        let result = SystemTime::limit().checked_add(SYSTEM_TIME_PRECISION);
324        assert!(result.is_none(), "able to exceed max duration");
325    }
326
327    #[test]
328    fn system_time_saturating_add() {
329        let max = SystemTime::limit();
330        assert_eq!(max.saturating_add(Duration::from_nanos(1)), max);
331        assert_eq!(max.saturating_add(Duration::from_secs(1)), max);
332    }
333
334    #[test]
335    fn test_duration_parse_milliseconds() {
336        assert_eq!(
337            Duration::parse("500ms").unwrap(),
338            Duration::from_millis(500)
339        );
340        assert_eq!(Duration::parse("0ms").unwrap(), Duration::from_millis(0));
341        assert_eq!(Duration::parse("1ms").unwrap(), Duration::from_millis(1));
342        assert_eq!(
343            Duration::parse("1000ms").unwrap(),
344            Duration::from_millis(1000)
345        );
346        assert_eq!(
347            Duration::parse("250ms").unwrap(),
348            Duration::from_millis(250)
349        );
350    }
351
352    #[test]
353    fn test_duration_parse_seconds() {
354        assert_eq!(Duration::parse("30s").unwrap(), Duration::from_secs(30));
355        assert_eq!(Duration::parse("0s").unwrap(), Duration::from_secs(0));
356        assert_eq!(Duration::parse("1s").unwrap(), Duration::from_secs(1));
357        assert_eq!(Duration::parse("45s").unwrap(), Duration::from_secs(45));
358        assert_eq!(Duration::parse("60s").unwrap(), Duration::from_secs(60));
359        assert_eq!(Duration::parse("3600s").unwrap(), Duration::from_secs(3600));
360    }
361
362    #[test]
363    fn test_duration_parse_minutes() {
364        assert_eq!(Duration::parse("5m").unwrap(), Duration::from_secs(300));
365        assert_eq!(Duration::parse("1m").unwrap(), Duration::from_secs(60));
366        assert_eq!(Duration::parse("0m").unwrap(), Duration::from_secs(0));
367        assert_eq!(Duration::parse("10m").unwrap(), Duration::from_secs(600));
368        assert_eq!(Duration::parse("15m").unwrap(), Duration::from_secs(900));
369        assert_eq!(Duration::parse("30m").unwrap(), Duration::from_secs(1800));
370        assert_eq!(Duration::parse("60m").unwrap(), Duration::from_secs(3600));
371    }
372
373    #[test]
374    fn test_duration_parse_hours() {
375        assert_eq!(Duration::parse("2h").unwrap(), Duration::from_secs(7200));
376        assert_eq!(Duration::parse("1h").unwrap(), Duration::from_secs(3600));
377        assert_eq!(Duration::parse("0h").unwrap(), Duration::from_secs(0));
378        assert_eq!(Duration::parse("3h").unwrap(), Duration::from_secs(10800));
379        assert_eq!(Duration::parse("4h").unwrap(), Duration::from_secs(14400));
380        assert_eq!(Duration::parse("12h").unwrap(), Duration::from_secs(43200));
381        assert_eq!(Duration::parse("24h").unwrap(), Duration::from_secs(86400));
382        assert_eq!(
383            Duration::parse("168h").unwrap(),
384            Duration::from_secs(604800)
385        );
386        // 1 week
387    }
388
389    #[test]
390    fn test_duration_parse_whitespace() {
391        // Should handle whitespace around the input
392        assert_eq!(Duration::parse("  30s  ").unwrap(), Duration::from_secs(30));
393        assert_eq!(
394            Duration::parse("\t500ms\n").unwrap(),
395            Duration::from_millis(500)
396        );
397        assert_eq!(Duration::parse(" 2h ").unwrap(), Duration::from_secs(7200));
398
399        // Should handle whitespace between number and suffix
400        assert_eq!(Duration::parse("30 s").unwrap(), Duration::from_secs(30));
401        assert_eq!(
402            Duration::parse("500 ms").unwrap(),
403            Duration::from_millis(500)
404        );
405        assert_eq!(Duration::parse("2 h").unwrap(), Duration::from_secs(7200));
406        assert_eq!(Duration::parse("5 m").unwrap(), Duration::from_secs(300));
407    }
408
409    #[test]
410    fn test_duration_parse_error_cases() {
411        // Invalid number
412        assert!(Duration::parse("invalid").is_err());
413        assert!(Duration::parse("abc123ms").is_err());
414        assert!(Duration::parse("12.5s").is_err()); // Decimal not supported
415
416        // Invalid suffix
417        assert!(Duration::parse("10x").is_err());
418        assert!(Duration::parse("30days").is_err());
419        assert!(Duration::parse("5y").is_err());
420
421        // Long forms not supported
422        assert!(Duration::parse("5minutes").is_err());
423        assert!(Duration::parse("10seconds").is_err());
424        assert!(Duration::parse("2hours").is_err());
425        assert!(Duration::parse("500millis").is_err());
426        assert!(Duration::parse("30sec").is_err());
427        assert!(Duration::parse("5min").is_err());
428        assert!(Duration::parse("2hr").is_err());
429
430        // No suffix
431        assert!(Duration::parse("60").is_err());
432        assert!(Duration::parse("0").is_err());
433        assert!(Duration::parse("3600").is_err());
434        assert!(Duration::parse("1").is_err());
435
436        // Empty or whitespace only
437        assert!(Duration::parse("").is_err());
438        assert!(Duration::parse("   ").is_err());
439
440        // Negative numbers (should fail because we use u64)
441        assert!(Duration::parse("-5s").is_err());
442        assert!(Duration::parse("-100ms").is_err());
443
444        // Mixed case should not work (we only support lowercase)
445        assert!(Duration::parse("30S").is_err());
446        assert!(Duration::parse("500MS").is_err());
447        assert!(Duration::parse("2H").is_err());
448    }
449
450    #[test]
451    fn test_duration_parse_large_values() {
452        // Large values that don't overflow
453        assert_eq!(
454            Duration::parse("999999999ms").unwrap(),
455            Duration::from_millis(999999999)
456        );
457        assert_eq!(
458            Duration::parse("99999999s").unwrap(),
459            Duration::from_secs(99999999)
460        );
461    }
462
463    #[test]
464    fn test_duration_parse_overflow_cases() {
465        // Test hours overflow
466        let max_safe_hours = u64::MAX / 3600;
467        let overflow_hours = max_safe_hours + 1;
468        assert!(Duration::parse(&format!("{max_safe_hours}h")).is_ok());
469        match Duration::parse(&format!("{overflow_hours}h")) {
470            Err(msg) => assert!(msg.contains("too large (would overflow)")),
471            Ok(_) => panic!("Expected overflow error for large hours value"),
472        }
473        match Duration::parse(&format!("{}h", u64::MAX)) {
474            Err(msg) => assert!(msg.contains("too large (would overflow)")),
475            Ok(_) => panic!("Expected overflow error for u64::MAX hours"),
476        }
477
478        // Test minutes overflow
479        let max_safe_minutes = u64::MAX / 60;
480        let overflow_minutes = max_safe_minutes + 1;
481        assert!(Duration::parse(&format!("{max_safe_minutes}m")).is_ok());
482        match Duration::parse(&format!("{overflow_minutes}m")) {
483            Err(msg) => assert!(msg.contains("too large (would overflow)")),
484            Ok(_) => panic!("Expected overflow error for large minutes value"),
485        }
486        match Duration::parse(&format!("{}m", u64::MAX)) {
487            Err(msg) => assert!(msg.contains("too large (would overflow)")),
488            Ok(_) => panic!("Expected overflow error for u64::MAX minutes"),
489        }
490    }
491}