Skip to main content

artifactize_core/auth/codex/
timestamp.rs

1//! OAuth timestamps have integer-second precision and the full saved u64 domain.
2//! Represent the instant as elapsed time since the Unix epoch, rather than narrowing
3//! older credential files to a calendar library's supported year range.
4
5use std::time::{Duration, SystemTime, UNIX_EPOCH};
6
7use serde::{Deserialize, Deserializer, Serialize, Serializer};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
10pub struct Timestamp(Duration);
11
12impl Timestamp {
13    pub fn now() -> Result<Self, String> {
14        SystemTime::now()
15            .duration_since(UNIX_EPOCH)
16            .map(|elapsed| Self::from_seconds(elapsed.as_secs()))
17            .map_err(|_| "The system clock is before the Unix epoch.".into())
18    }
19
20    pub fn from_seconds(seconds: u64) -> Self {
21        Self(Duration::from_secs(seconds))
22    }
23
24    pub fn seconds(self) -> u64 {
25        self.0.as_secs()
26    }
27
28    pub fn checked_add(self, duration: Duration) -> Option<Self> {
29        self.0
30            .checked_add(duration)
31            .map(|elapsed| Self::from_seconds(elapsed.as_secs()))
32    }
33
34    pub fn saturating_add(self, duration: Duration) -> Self {
35        Self::from_seconds(self.0.saturating_add(duration).as_secs())
36    }
37
38    /// How long after `earlier` this is; zero when it is not later.
39    pub fn saturating_since(self, earlier: Self) -> Duration {
40        self.0.saturating_sub(earlier.0)
41    }
42}
43
44impl Serialize for Timestamp {
45    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
46        seconds::serialize(self, serializer)
47    }
48}
49impl<'de> Deserialize<'de> for Timestamp {
50    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
51        seconds::deserialize(deserializer)
52    }
53}
54
55pub(super) mod seconds {
56    use super::*;
57    pub fn serialize<S: Serializer>(time: &Timestamp, serializer: S) -> Result<S::Ok, S::Error> {
58        time.seconds().serialize(serializer)
59    }
60    pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Timestamp, D::Error> {
61        u64::deserialize(deserializer).map(Timestamp::from_seconds)
62    }
63}
64
65pub(super) mod optional_duration {
66    use super::*;
67    pub fn deserialize<'de, D: Deserializer<'de>>(
68        deserializer: D,
69    ) -> Result<Option<Duration>, D::Error> {
70        Option::<u64>::deserialize(deserializer).map(|value| value.map(Duration::from_secs))
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn integer_seconds_keep_the_entire_saved_domain() {
80        for seconds in [0, 1, 10_000, u64::MAX] {
81            let wire = seconds.to_string();
82            let time: Timestamp = serde_json::from_str(&wire).unwrap();
83            assert_eq!(time.seconds(), seconds);
84            assert_eq!(serde_json::to_string(&time).unwrap(), wire);
85        }
86        for wire in ["-1", "1.5", "\"100\"", "null"] {
87            assert!(serde_json::from_str::<Timestamp>(wire).is_err());
88        }
89    }
90
91    #[test]
92    fn timestamp_margins_preserve_checked_and_saturating_overflow() {
93        let time = Timestamp::from_seconds(10_000);
94        assert_eq!(
95            time.checked_add(Duration::from_secs(3600))
96                .unwrap()
97                .seconds(),
98            13_600
99        );
100        let last = Timestamp::from_seconds(u64::MAX);
101        assert!(last.checked_add(Duration::from_secs(1)).is_none());
102        assert_eq!(last.saturating_add(Duration::from_secs(300)), last);
103    }
104}