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