1use rand::{Rng, RngExt as _};
4use std::time::{Duration, SystemTime};
5
6pub const NANOS_PER_SEC: u128 = 1_000_000_000;
8
9pub const MAX_DURATION_SINCE_UNIX_EPOCH: Duration = Duration::new(i64::MAX as u64, 999_999_999);
16
17pub const SYSTEM_TIME_PRECISION: Duration = Duration::from_nanos(1);
19
20pub trait DurationExt {
22 fn as_millis_u64(&self) -> u64;
28
29 fn from_nanos_saturating(ns: u128) -> Duration;
32
33 fn parse(s: &str) -> Result<Duration, String>;
83}
84
85impl DurationExt for Duration {
86 fn as_millis_u64(&self) -> u64 {
87 u64::try_from(self.as_millis()).expect("duration milliseconds should fit in u64")
88 }
89
90 fn from_nanos_saturating(ns: u128) -> Duration {
91 if ns > Self::MAX.as_nanos() {
93 return Self::MAX;
94 }
95
96 let secs = (ns / NANOS_PER_SEC) as u64;
98 let nanos = (ns % NANOS_PER_SEC) as u32;
99 Self::new(secs, nanos)
100 }
101
102 fn parse(s: &str) -> Result<Duration, String> {
103 let s = s.trim();
104
105 if let Some(num_str) = s.strip_suffix("ms") {
107 let millis: u64 = num_str
108 .trim()
109 .parse()
110 .map_err(|_| format!("Invalid milliseconds value: '{num_str}'"))?;
111 return Ok(Self::from_millis(millis));
112 }
113
114 if let Some(num_str) = s.strip_suffix("h") {
116 let hours: u64 = num_str
117 .trim()
118 .parse()
119 .map_err(|_| format!("Invalid hours value: '{num_str}'"))?;
120 let seconds = hours
121 .checked_mul(3600)
122 .ok_or_else(|| format!("Hours value too large (would overflow): '{hours}'"))?;
123 return Ok(Self::from_secs(seconds));
124 }
125
126 if let Some(num_str) = s.strip_suffix("m") {
128 let minutes: u64 = num_str
129 .trim()
130 .parse()
131 .map_err(|_| format!("Invalid minutes value: '{num_str}'"))?;
132 let seconds = minutes
133 .checked_mul(60)
134 .ok_or_else(|| format!("Minutes value too large (would overflow): '{minutes}'"))?;
135 return Ok(Self::from_secs(seconds));
136 }
137
138 if let Some(num_str) = s.strip_suffix("s") {
140 let secs: u64 = num_str
141 .trim()
142 .parse()
143 .map_err(|_| format!("Invalid seconds value: '{num_str}'"))?;
144 return Ok(Self::from_secs(secs));
145 }
146
147 Err(format!(
149 "Invalid duration format: '{s}'. A suffix is required. \
150 Supported formats: '123ms', '30s', '5m', '2h'"
151 ))
152 }
153}
154
155pub trait SystemTimeExt {
157 fn epoch(&self) -> Duration;
161
162 fn epoch_millis(&self) -> u64;
167
168 fn add_jittered(&self, rng: &mut impl Rng, jitter: Duration) -> SystemTime;
172
173 fn limit() -> SystemTime;
175
176 fn saturating_add_ext(&self, delta: Duration) -> SystemTime;
178}
179
180impl SystemTimeExt for SystemTime {
181 fn epoch(&self) -> Duration {
182 self.duration_since(std::time::UNIX_EPOCH)
183 .expect("failed to get epoch time")
184 }
185
186 fn epoch_millis(&self) -> u64 {
187 self.epoch().as_millis().min(u64::MAX as u128) as u64
188 }
189
190 fn add_jittered(&self, rng: &mut impl Rng, jitter: Duration) -> SystemTime {
191 self.saturating_add_ext(rng.random_range(Duration::default()..=jitter * 2))
192 }
193
194 fn limit() -> SystemTime {
195 Self::UNIX_EPOCH
196 .checked_add(MAX_DURATION_SINCE_UNIX_EPOCH)
197 .expect("maximum system time must be representable")
198 }
199
200 fn saturating_add_ext(&self, delta: Duration) -> SystemTime {
201 if delta.is_zero() {
202 return *self;
203 }
204
205 self.checked_add(delta).unwrap_or_else(Self::limit)
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214 use crate::test_rng;
215
216 #[test]
217 fn test_as_millis_u64() {
218 for (duration, expected) in [
219 (Duration::ZERO, 0),
220 (Duration::from_nanos(999_999), 0),
221 (Duration::new(1, 234_567_890), 1_234),
222 (Duration::from_millis(u64::MAX), u64::MAX),
223 (
224 Duration::from_millis(u64::MAX) + Duration::from_nanos(999_999),
225 u64::MAX,
226 ),
227 ] {
228 assert_eq!(duration.as_millis_u64(), expected);
229 }
230 }
231
232 #[test]
233 fn test_as_millis_u64_overflow() {
234 for duration in [
235 Duration::from_millis(u64::MAX) + Duration::from_millis(1),
236 Duration::MAX,
237 ] {
238 assert!(std::panic::catch_unwind(|| duration.as_millis_u64()).is_err());
239 }
240 }
241
242 #[test]
243 fn test_epoch() {
244 let time = SystemTime::UNIX_EPOCH;
245 assert_eq!(time.epoch(), Duration::from_secs(0));
246
247 let time = SystemTime::UNIX_EPOCH + Duration::from_secs(1) + Duration::from_millis(1);
248 assert_eq!(time.epoch(), Duration::from_millis(1_001));
249 }
250
251 #[test]
252 #[should_panic(expected = "failed to get epoch time")]
253 fn test_epoch_panics() {
254 let time = SystemTime::UNIX_EPOCH - Duration::from_secs(1);
255 time.epoch();
256 }
257
258 #[test]
259 fn test_epoch_millis() {
260 let time = SystemTime::UNIX_EPOCH;
261 assert_eq!(time.epoch_millis(), 0);
262
263 let time = SystemTime::UNIX_EPOCH + Duration::from_secs(1) + Duration::from_millis(1);
264 assert_eq!(time.epoch_millis(), 1_001);
265
266 let time = SystemTime::UNIX_EPOCH + Duration::from_secs(1) + Duration::from_nanos(999_999);
268 assert_eq!(time.epoch_millis(), 1_000);
269
270 let time = SystemTime::UNIX_EPOCH + Duration::from_secs(300);
272 assert_eq!(time.epoch_millis(), 300_000);
273 }
274
275 #[test]
276 #[should_panic(expected = "failed to get epoch time")]
277 fn test_epoch_millis_panics() {
278 let time = SystemTime::UNIX_EPOCH - Duration::from_secs(1);
279 time.epoch_millis();
280 }
281
282 #[test]
283 fn test_from_nanos_saturating() {
284 assert_eq!(Duration::from_nanos_saturating(0), Duration::new(0, 0));
286 assert_eq!(
287 Duration::from_nanos_saturating(NANOS_PER_SEC - 1),
288 Duration::new(0, (NANOS_PER_SEC - 1) as u32)
289 );
290 assert_eq!(
291 Duration::from_nanos_saturating(NANOS_PER_SEC + 1),
292 Duration::new(1, 1)
293 );
294
295 let std = Duration::from_nanos(u64::MAX);
297 let beyond_std = Duration::from_nanos_saturating(u64::MAX as u128 + 1);
298 assert!(beyond_std > std);
299
300 assert_eq!(
302 Duration::from_nanos_saturating(Duration::MAX.as_nanos()),
303 Duration::MAX
304 );
305
306 assert_eq!(
308 Duration::from_nanos_saturating(Duration::MAX.as_nanos() + 1),
309 Duration::MAX
310 );
311 assert_eq!(Duration::from_nanos_saturating(u128::MAX), Duration::MAX);
312 }
313
314 #[test]
315 fn test_add_jittered() {
316 let mut rng = test_rng();
317 let time = SystemTime::UNIX_EPOCH + Duration::from_secs(1);
318 let jitter = Duration::from_secs(2);
319
320 let (mut below, mut above) = (false, false);
322 let avg = time + jitter;
323 for _ in 0..100 {
324 let new_time = time.add_jittered(&mut rng, jitter);
325
326 below |= new_time < avg;
328 above |= new_time > avg;
329
330 assert!(new_time >= time);
332 assert!(new_time <= time + (jitter * 2));
333 }
334 assert!(below && above);
335 }
336
337 #[test]
338 fn test_add_jittered_saturates() {
339 let mut rng = test_rng();
340 let time = SystemTime::limit() - Duration::from_secs(1);
341 let jitter = Duration::from_secs(60);
342
343 for _ in 0..100 {
344 let new_time = time.add_jittered(&mut rng, jitter);
345 assert!(new_time >= time);
346 assert!(new_time <= SystemTime::limit());
347 }
348 }
349
350 #[test]
351 fn check_duration_limit() {
352 let result = SystemTime::limit()
354 .checked_add(SYSTEM_TIME_PRECISION - Duration::from_nanos(1))
355 .expect("addition within precision should round down");
356 assert_eq!(result, SystemTime::limit(), "unexpected precision");
357
358 let result = SystemTime::limit().checked_add(SYSTEM_TIME_PRECISION);
360 assert!(result.is_none(), "able to exceed max duration");
361 }
362
363 #[test]
364 fn system_time_saturating_add() {
365 let max = SystemTime::limit();
366 assert_eq!(max.saturating_add_ext(Duration::from_nanos(1)), max);
367 assert_eq!(max.saturating_add_ext(Duration::from_secs(1)), max);
368 }
369
370 #[test]
371 fn test_duration_parse_milliseconds() {
372 assert_eq!(
373 Duration::parse("500ms").unwrap(),
374 Duration::from_millis(500)
375 );
376 assert_eq!(Duration::parse("0ms").unwrap(), Duration::from_millis(0));
377 assert_eq!(Duration::parse("1ms").unwrap(), Duration::from_millis(1));
378 assert_eq!(
379 Duration::parse("1000ms").unwrap(),
380 Duration::from_millis(1000)
381 );
382 assert_eq!(
383 Duration::parse("250ms").unwrap(),
384 Duration::from_millis(250)
385 );
386 }
387
388 #[test]
389 fn test_duration_parse_seconds() {
390 assert_eq!(Duration::parse("30s").unwrap(), Duration::from_secs(30));
391 assert_eq!(Duration::parse("0s").unwrap(), Duration::from_secs(0));
392 assert_eq!(Duration::parse("1s").unwrap(), Duration::from_secs(1));
393 assert_eq!(Duration::parse("45s").unwrap(), Duration::from_secs(45));
394 assert_eq!(Duration::parse("60s").unwrap(), Duration::from_secs(60));
395 assert_eq!(Duration::parse("3600s").unwrap(), Duration::from_secs(3600));
396 }
397
398 #[test]
399 fn test_duration_parse_minutes() {
400 assert_eq!(Duration::parse("5m").unwrap(), Duration::from_secs(300));
401 assert_eq!(Duration::parse("1m").unwrap(), Duration::from_secs(60));
402 assert_eq!(Duration::parse("0m").unwrap(), Duration::from_secs(0));
403 assert_eq!(Duration::parse("10m").unwrap(), Duration::from_secs(600));
404 assert_eq!(Duration::parse("15m").unwrap(), Duration::from_secs(900));
405 assert_eq!(Duration::parse("30m").unwrap(), Duration::from_secs(1800));
406 assert_eq!(Duration::parse("60m").unwrap(), Duration::from_secs(3600));
407 }
408
409 #[test]
410 fn test_duration_parse_hours() {
411 assert_eq!(Duration::parse("2h").unwrap(), Duration::from_secs(7200));
412 assert_eq!(Duration::parse("1h").unwrap(), Duration::from_secs(3600));
413 assert_eq!(Duration::parse("0h").unwrap(), Duration::from_secs(0));
414 assert_eq!(Duration::parse("3h").unwrap(), Duration::from_secs(10800));
415 assert_eq!(Duration::parse("4h").unwrap(), Duration::from_secs(14400));
416 assert_eq!(Duration::parse("12h").unwrap(), Duration::from_secs(43200));
417 assert_eq!(Duration::parse("24h").unwrap(), Duration::from_secs(86400));
418 assert_eq!(
419 Duration::parse("168h").unwrap(),
420 Duration::from_secs(604800)
421 );
422 }
424
425 #[test]
426 fn test_duration_parse_whitespace() {
427 assert_eq!(Duration::parse(" 30s ").unwrap(), Duration::from_secs(30));
429 assert_eq!(
430 Duration::parse("\t500ms\n").unwrap(),
431 Duration::from_millis(500)
432 );
433 assert_eq!(Duration::parse(" 2h ").unwrap(), Duration::from_secs(7200));
434
435 assert_eq!(Duration::parse("30 s").unwrap(), Duration::from_secs(30));
437 assert_eq!(
438 Duration::parse("500 ms").unwrap(),
439 Duration::from_millis(500)
440 );
441 assert_eq!(Duration::parse("2 h").unwrap(), Duration::from_secs(7200));
442 assert_eq!(Duration::parse("5 m").unwrap(), Duration::from_secs(300));
443 }
444
445 #[test]
446 fn test_duration_parse_error_cases() {
447 assert!(Duration::parse("invalid").is_err());
449 assert!(Duration::parse("abc123ms").is_err());
450 assert!(Duration::parse("12.5s").is_err()); assert!(Duration::parse("10x").is_err());
454 assert!(Duration::parse("30days").is_err());
455 assert!(Duration::parse("5y").is_err());
456
457 assert!(Duration::parse("5minutes").is_err());
459 assert!(Duration::parse("10seconds").is_err());
460 assert!(Duration::parse("2hours").is_err());
461 assert!(Duration::parse("500millis").is_err());
462 assert!(Duration::parse("30sec").is_err());
463 assert!(Duration::parse("5min").is_err());
464 assert!(Duration::parse("2hr").is_err());
465
466 assert!(Duration::parse("60").is_err());
468 assert!(Duration::parse("0").is_err());
469 assert!(Duration::parse("3600").is_err());
470 assert!(Duration::parse("1").is_err());
471
472 assert!(Duration::parse("").is_err());
474 assert!(Duration::parse(" ").is_err());
475
476 assert!(Duration::parse("-5s").is_err());
478 assert!(Duration::parse("-100ms").is_err());
479
480 assert!(Duration::parse("30S").is_err());
482 assert!(Duration::parse("500MS").is_err());
483 assert!(Duration::parse("2H").is_err());
484 }
485
486 #[test]
487 fn test_duration_parse_large_values() {
488 assert_eq!(
490 Duration::parse("999999999ms").unwrap(),
491 Duration::from_millis(999999999)
492 );
493 assert_eq!(
494 Duration::parse("99999999s").unwrap(),
495 Duration::from_secs(99999999)
496 );
497 }
498
499 #[test]
500 fn test_duration_parse_overflow_cases() {
501 let max_safe_hours = u64::MAX / 3600;
503 let overflow_hours = max_safe_hours + 1;
504 assert!(Duration::parse(&format!("{max_safe_hours}h")).is_ok());
505 match Duration::parse(&format!("{overflow_hours}h")) {
506 Err(msg) => assert!(msg.contains("too large (would overflow)")),
507 Ok(_) => panic!("Expected overflow error for large hours value"),
508 }
509 match Duration::parse(&format!("{}h", u64::MAX)) {
510 Err(msg) => assert!(msg.contains("too large (would overflow)")),
511 Ok(_) => panic!("Expected overflow error for u64::MAX hours"),
512 }
513
514 let max_safe_minutes = u64::MAX / 60;
516 let overflow_minutes = max_safe_minutes + 1;
517 assert!(Duration::parse(&format!("{max_safe_minutes}m")).is_ok());
518 match Duration::parse(&format!("{overflow_minutes}m")) {
519 Err(msg) => assert!(msg.contains("too large (would overflow)")),
520 Ok(_) => panic!("Expected overflow error for large minutes value"),
521 }
522 match Duration::parse(&format!("{}m", u64::MAX)) {
523 Err(msg) => assert!(msg.contains("too large (would overflow)")),
524 Ok(_) => panic!("Expected overflow error for u64::MAX minutes"),
525 }
526 }
527}