Skip to main content

objectstore_types/
time.rs

1//! Second-precision timestamps for object creation, access, and expiration.
2//!
3//! [`Timestamp`] represents object creation times, expiration deadlines, and the access times
4//! used to check or renew them. Fractional timestamps round up to the next second. Event
5//! timestamps, metrics, and multipart modification times retain their own precision.
6//!
7//! The default serde representation preserves the `SystemTime` metadata format. Use
8//! [`Timestamp::as_rfc3339`] for HTTP headers and JSON fields containing RFC3339 strings.
9
10use std::fmt;
11use std::ops::{Add, Sub};
12use std::str::FromStr;
13use std::time::{Duration, SystemTime};
14
15use humantime::TimestampError;
16use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
17use thiserror::Error;
18
19/// A whole-second Unix timestamp used for access time and expiration.
20///
21/// Also used for object creation times. Values range from the Unix epoch through
22/// `9999-12-31T23:59:59Z`. When constructed with fractional seconds, the timestamp rounds up to
23/// the next whole second. Values outside this range are rejected.
24///
25/// Serializes in the same format as `SystemTime`, with `secs_since_epoch` and `nanos_since_epoch`
26/// fields. The nanoseconds field is always zero when serialized. Deserialization accepts legacy
27/// fractional timestamps and rounds them upward.
28///
29/// ```
30/// use std::time::Duration;
31/// use objectstore_types::time::Timestamp;
32///
33/// let deadline = Timestamp::from_unix_micros(1_700_000_000_123_456)?;
34/// assert_eq!(deadline.as_rfc3339().to_string(), "2023-11-14T22:13:21Z");
35/// let extended = deadline + Duration::from_secs(60);
36/// assert_eq!(extended.as_secs(), deadline.as_secs() + 60);
37/// # Ok::<(), objectstore_types::time::InvalidTimestamp>(())
38/// ```
39#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
40pub struct Timestamp(u64);
41
42impl Timestamp {
43    /// The Unix epoch.
44    pub const UNIX_EPOCH: Self = Self(0);
45
46    /// The maximum supported timestamp, `9999-12-31T23:59:59Z`.
47    const MAX: u64 = 253_402_300_799;
48
49    /// Captures the current wall-clock time, rounded up to a whole second.
50    ///
51    /// # Panics
52    /// Panics if the system clock is outside the supported timestamp range.
53    pub fn now() -> Self {
54        Self::try_from(SystemTime::now()).expect("system clock outside timestamp range")
55    }
56
57    /// Constructs a timestamp from whole Unix seconds.
58    pub fn from_unix_secs(seconds: u64) -> Result<Self, InvalidTimestamp> {
59        if seconds <= Self::MAX {
60            Ok(Self(seconds))
61        } else {
62            Err(InvalidTimestamp)
63        }
64    }
65
66    /// Constructs a timestamp from Unix microseconds, rounding fractional seconds upward.
67    pub fn from_unix_micros(micros: i64) -> Result<Self, InvalidTimestamp> {
68        let micros = u64::try_from(micros).map_err(|_| InvalidTimestamp)?;
69        Self::from_unix_secs(micros.div_ceil(1_000_000))
70    }
71
72    /// Parses an RFC3339 timestamp, rounding fractional seconds upward.
73    ///
74    /// Returns an error if the input is invalid or outside the supported timestamp range.
75    pub fn from_rfc3339(value: &str) -> Result<Self, TimestampError> {
76        let time = humantime::parse_rfc3339(value)?;
77        Self::try_from(time).map_err(|_| TimestampError::OutOfRange)
78    }
79
80    /// Returns the timestamp in whole Unix seconds.
81    pub fn as_secs(self) -> u64 {
82        self.0
83    }
84
85    /// Returns the second-aligned timestamp in Unix microseconds.
86    pub fn as_micros(self) -> u64 {
87        self.0 * 1_000_000
88    }
89
90    /// Returns a copy that displays and serializes as an RFC3339 string.
91    pub fn as_rfc3339(self) -> Rfc3339Timestamp {
92        Rfc3339Timestamp(self)
93    }
94
95    /// Adds a duration, rounding upward, or returns `None` if the result is out of range.
96    pub fn checked_add(self, duration: Duration) -> Option<Self> {
97        let seconds = self.0.checked_add(duration.as_secs())?;
98        let seconds = seconds.checked_add(u64::from(duration.subsec_nanos() != 0))?;
99        Self::from_unix_secs(seconds).ok()
100    }
101
102    /// Subtracts a duration, rounding upward, or returns `None` if the result precedes the epoch.
103    pub fn checked_sub(self, duration: Duration) -> Option<Self> {
104        // Ceiling a whole timestamp minus a duration subtracts only the whole seconds.
105        // Reject an unrounded result before the epoch, just as construction does.
106        if duration > Duration::from_secs(self.0) {
107            return None;
108        }
109        Some(Self(self.0 - duration.as_secs()))
110    }
111
112    /// Returns the elapsed duration, or `None` if `earlier` is later than this timestamp.
113    pub fn checked_duration_since(self, earlier: Self) -> Option<Duration> {
114        self.0.checked_sub(earlier.0).map(Duration::from_secs)
115    }
116}
117
118impl TryFrom<SystemTime> for Timestamp {
119    type Error = InvalidTimestamp;
120
121    fn try_from(time: SystemTime) -> Result<Self, Self::Error> {
122        let duration = time
123            .duration_since(SystemTime::UNIX_EPOCH)
124            .map_err(|_| InvalidTimestamp)?;
125        let seconds = duration
126            .as_secs()
127            .checked_add(u64::from(duration.subsec_nanos() != 0))
128            .ok_or(InvalidTimestamp)?;
129        Self::from_unix_secs(seconds)
130    }
131}
132
133impl From<Timestamp> for SystemTime {
134    fn from(time: Timestamp) -> Self {
135        Self::UNIX_EPOCH + Duration::from_secs(time.0)
136    }
137}
138
139impl Add<Duration> for Timestamp {
140    type Output = Self;
141
142    /// Adds a duration, rounding up. Panics if the result is outside the supported range.
143    fn add(self, duration: Duration) -> Self {
144        self.checked_add(duration)
145            .expect("timestamp addition out of range")
146    }
147}
148
149impl Sub<Duration> for Timestamp {
150    type Output = Self;
151
152    /// Subtracts a duration, rounding up. Panics if the result precedes the epoch.
153    fn sub(self, duration: Duration) -> Self {
154        self.checked_sub(duration)
155            .expect("timestamp subtraction out of range")
156    }
157}
158
159impl Serialize for Timestamp {
160    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
161        SystemTime::from(*self).serialize(serializer)
162    }
163}
164
165impl<'de> Deserialize<'de> for Timestamp {
166    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
167        Self::try_from(SystemTime::deserialize(deserializer)?).map_err(de::Error::custom)
168    }
169}
170
171/// A timestamp is outside the supported range.
172#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
173#[error("timestamp is outside the Unix epoch through year 9999")]
174pub struct InvalidTimestamp;
175
176/// An owned timestamp view that displays and serializes as a whole-second RFC3339 string.
177///
178/// Deserialization accepts fractional seconds and rounds upward, like [`Timestamp`].
179#[derive(Clone, Copy, Debug, Eq, PartialEq)]
180pub struct Rfc3339Timestamp(Timestamp);
181
182impl Rfc3339Timestamp {
183    /// Returns the underlying timestamp.
184    pub fn into_inner(self) -> Timestamp {
185        self.0
186    }
187}
188
189impl fmt::Display for Rfc3339Timestamp {
190    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
191        humantime::format_rfc3339_seconds(self.0.into()).fmt(f)
192    }
193}
194
195impl FromStr for Rfc3339Timestamp {
196    type Err = TimestampError;
197
198    fn from_str(value: &str) -> Result<Self, Self::Err> {
199        Timestamp::from_rfc3339(value).map(Self)
200    }
201}
202
203impl Serialize for Rfc3339Timestamp {
204    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
205        serializer.collect_str(self)
206    }
207}
208
209impl<'de> Deserialize<'de> for Rfc3339Timestamp {
210    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
211        let value = String::deserialize(deserializer)?;
212        value.parse().map_err(de::Error::custom)
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    #[test]
221    fn rounding_and_arithmetic() {
222        let epoch = SystemTime::UNIX_EPOCH;
223        for (nanos, seconds) in [(0, 0), (1, 1), (999_999_999, 1), (1_000_000_000, 1)] {
224            let time = Timestamp::try_from(epoch + Duration::from_nanos(nanos)).unwrap();
225            assert_eq!(time.as_secs(), seconds);
226            assert_eq!(time.as_micros(), seconds * 1_000_000);
227            assert_eq!(Timestamp::try_from(SystemTime::from(time)).unwrap(), time);
228        }
229        let time = Timestamp::from_unix_secs(10).unwrap();
230        assert_eq!((time + Duration::from_millis(1500)).as_secs(), 12);
231        assert_eq!((time - Duration::from_millis(1500)).as_secs(), 9);
232        assert_eq!(time.checked_duration_since(time), Some(Duration::ZERO));
233        assert!(Timestamp::try_from(epoch - Duration::from_nanos(1)).is_err());
234        assert!(Timestamp::from_unix_micros(-1).is_err());
235        assert_eq!(Timestamp::from_unix_micros(1).unwrap().as_secs(), 1);
236        assert!(Timestamp::from_unix_micros(i64::MAX).is_err());
237        assert!(
238            Timestamp::UNIX_EPOCH
239                .checked_sub(Duration::from_nanos(1))
240                .is_none()
241        );
242        let max = Timestamp::from_unix_secs(Timestamp::MAX).unwrap();
243        assert!(max.checked_add(Duration::from_nanos(1)).is_none());
244        assert!(max.checked_add(Duration::MAX).is_none());
245        assert_eq!(max.as_rfc3339().to_string(), "9999-12-31T23:59:59Z");
246    }
247
248    #[test]
249    fn serialization_formats() {
250        let legacy = r#"{"secs_since_epoch":1700000000,"nanos_since_epoch":1}"#;
251        let time: Timestamp = serde_json::from_str(legacy).unwrap();
252        assert_eq!(time.as_secs(), 1_700_000_001);
253        let json = serde_json::to_string(&time).unwrap();
254        assert_eq!(
255            json,
256            r#"{"secs_since_epoch":1700000001,"nanos_since_epoch":0}"#
257        );
258        assert_eq!(serde_json::from_str::<Timestamp>(&json).unwrap(), time);
259        assert_eq!(
260            serde_json::from_str::<SystemTime>(&json).unwrap(),
261            time.into()
262        );
263        let rfc: Rfc3339Timestamp =
264            serde_json::from_str(r#""2023-11-14T22:13:20.000001Z""#).unwrap();
265        assert_eq!(rfc.into_inner(), time);
266        assert_eq!(
267            serde_json::to_string(&rfc).unwrap(),
268            r#""2023-11-14T22:13:21Z""#
269        );
270        assert_eq!(rfc.to_string(), "2023-11-14T22:13:21Z");
271        assert!(
272            "9999-12-31T23:59:59.1Z"
273                .parse::<Rfc3339Timestamp>()
274                .is_err()
275        );
276    }
277}