Skip to main content

dove_core/
duration.rs

1//! Compact share durations like `3d`, `12h`, `30m`, `90s` — how long a share's
2//! link stays valid.
3
4// Consumed by later tasks (`share` / `provision`); allow until they wire it in.
5#![allow(dead_code)]
6
7use anyhow::{anyhow, bail, Result};
8use std::time::Duration;
9
10/// The SigV4 presigned-URL ceiling: a presigned URL can't outlive its signing
11/// credential, and that caps at 7 days. Shares in the simple tier can't exceed
12/// this; longer-lived shares are what the full tier is for.
13pub const PRESIGN_MAX: Duration = Duration::from_secs(7 * 86_400);
14
15/// Parse `<n><unit>` where unit is `d`/`h`/`m`/`s` (e.g. `3d`, `12h`, `90s`).
16/// `n` must be a positive integer.
17pub fn parse(s: &str) -> Result<Duration> {
18    let s = s.trim();
19    let split = s.find(|c: char| !c.is_ascii_digit()).unwrap_or(s.len());
20    let (num, unit) = s.split_at(split);
21    if num.is_empty() {
22        bail!("duration needs a number, e.g. 3d (got {s:?})");
23    }
24    let n: u64 = num
25        .parse()
26        .map_err(|_| anyhow!("duration number out of range in {s:?}"))?;
27    if n == 0 {
28        bail!("duration must be greater than zero (got {s:?})");
29    }
30    let secs = match unit {
31        "d" => n * 86_400,
32        "h" => n * 3_600,
33        "m" => n * 60,
34        "s" => n,
35        "" => bail!("duration needs a unit d/h/m/s, e.g. 3d (got {s:?})"),
36        other => bail!("unknown duration unit {other:?} — use d, h, m, or s"),
37    };
38    Ok(Duration::from_secs(secs))
39}
40
41/// Whether `d` is within the presigned-URL ceiling (≤ 7 days).
42pub fn within_presign_limit(d: Duration) -> bool {
43    d <= PRESIGN_MAX
44}
45
46/// Render a duration back to a compact string, using the largest unit that
47/// divides it evenly (`259200s` → `3d`, `5400s` → `90m`).
48pub fn human(d: Duration) -> String {
49    let s = d.as_secs();
50    if s == 0 {
51        return "0s".to_string();
52    }
53    if s.is_multiple_of(86_400) {
54        format!("{}d", s / 86_400)
55    } else if s.is_multiple_of(3_600) {
56        format!("{}h", s / 3_600)
57    } else if s.is_multiple_of(60) {
58        format!("{}m", s / 60)
59    } else {
60        format!("{s}s")
61    }
62}
63
64/// A friendly, spelled-out duration for status lines: `2 days`, `1 day`,
65/// `12 hours`. Uses the largest unit that divides evenly.
66pub fn human_long(d: Duration) -> String {
67    let s = d.as_secs();
68    let (n, unit) = if s.is_multiple_of(86_400) {
69        (s / 86_400, "day")
70    } else if s.is_multiple_of(3_600) {
71        (s / 3_600, "hour")
72    } else if s.is_multiple_of(60) {
73        (s / 60, "minute")
74    } else {
75        (s, "second")
76    };
77    format!("{n} {unit}{}", if n == 1 { "" } else { "s" })
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn human_long_spells_it_out() {
86        assert_eq!(human_long(parse("2d").unwrap()), "2 days");
87        assert_eq!(human_long(parse("1d").unwrap()), "1 day");
88        assert_eq!(human_long(parse("12h").unwrap()), "12 hours");
89    }
90
91    #[test]
92    fn parses_each_unit() {
93        assert_eq!(parse("3d").unwrap(), Duration::from_secs(3 * 86_400));
94        assert_eq!(parse("12h").unwrap(), Duration::from_secs(12 * 3_600));
95        assert_eq!(parse("30m").unwrap(), Duration::from_secs(30 * 60));
96        assert_eq!(parse("90s").unwrap(), Duration::from_secs(90));
97        assert_eq!(parse("  5d ").unwrap(), Duration::from_secs(5 * 86_400));
98    }
99
100    #[test]
101    fn rejects_bad_input() {
102        assert!(parse("0d").is_err()); // zero
103        assert!(parse("d").is_err()); // no number
104        assert!(parse("5").is_err()); // no unit
105        assert!(parse("5x").is_err()); // bad unit
106        assert!(parse("5days").is_err()); // only single-char units
107        assert!(parse("").is_err());
108    }
109
110    #[test]
111    fn presign_limit_is_seven_days() {
112        assert!(within_presign_limit(parse("7d").unwrap()));
113        assert!(within_presign_limit(parse("168h").unwrap())); // exactly 7d
114        assert!(!within_presign_limit(parse("8d").unwrap()));
115        assert!(!within_presign_limit(parse("169h").unwrap()));
116    }
117
118    #[test]
119    fn human_uses_largest_even_unit() {
120        assert_eq!(human(parse("3d").unwrap()), "3d");
121        assert_eq!(human(parse("12h").unwrap()), "12h");
122        assert_eq!(human(parse("90m").unwrap()), "90m");
123        assert_eq!(human(Duration::from_secs(90)), "90s");
124    }
125}