prost_types/
timestamp.rs

1use super::*;
2
3impl Timestamp {
4    /// Normalizes the timestamp to a canonical format.
5    ///
6    /// Based on [`google::protobuf::util::CreateNormalized`][1].
7    ///
8    /// [1]: https://github.com/google/protobuf/blob/v3.3.2/src/google/protobuf/util/time_util.cc#L59-L77
9    pub fn normalize(&mut self) {
10        // Make sure nanos is in the range.
11        if self.nanos <= -NANOS_PER_SECOND || self.nanos >= NANOS_PER_SECOND {
12            if let Some(seconds) = self
13                .seconds
14                .checked_add((self.nanos / NANOS_PER_SECOND) as i64)
15            {
16                self.seconds = seconds;
17                self.nanos %= NANOS_PER_SECOND;
18            } else if self.nanos < 0 {
19                // Negative overflow! Set to the earliest normal value.
20                self.seconds = i64::MIN;
21                self.nanos = 0;
22            } else {
23                // Positive overflow! Set to the latest normal value.
24                self.seconds = i64::MAX;
25                self.nanos = 999_999_999;
26            }
27        }
28
29        // For Timestamp nanos should be in the range [0, 999999999].
30        if self.nanos < 0 {
31            if let Some(seconds) = self.seconds.checked_sub(1) {
32                self.seconds = seconds;
33                self.nanos += NANOS_PER_SECOND;
34            } else {
35                // Negative overflow! Set to the earliest normal value.
36                debug_assert_eq!(self.seconds, i64::MIN);
37                self.nanos = 0;
38            }
39        }
40
41        // TODO: should this be checked?
42        // debug_assert!(self.seconds >= -62_135_596_800 && self.seconds <= 253_402_300_799,
43        //               "invalid timestamp: {:?}", self);
44    }
45
46    /// Normalizes the timestamp to a canonical format, returning the original value if it cannot be
47    /// normalized.
48    ///
49    /// Normalization is based on [`google::protobuf::util::CreateNormalized`][1].
50    ///
51    /// [1]: https://github.com/google/protobuf/blob/v3.3.2/src/google/protobuf/util/time_util.cc#L59-L77
52    pub fn try_normalize(mut self) -> Result<Timestamp, Timestamp> {
53        let before = self;
54        self.normalize();
55        // If the seconds value has changed, and is either i64::MIN or i64::MAX, then the timestamp
56        // normalization overflowed.
57        if (self.seconds == i64::MAX || self.seconds == i64::MIN) && self.seconds != before.seconds
58        {
59            Err(before)
60        } else {
61            Ok(self)
62        }
63    }
64
65    /// Return a normalized copy of the timestamp to a canonical format.
66    ///
67    /// Based on [`google::protobuf::util::CreateNormalized`][1].
68    ///
69    /// [1]: https://github.com/google/protobuf/blob/v3.3.2/src/google/protobuf/util/time_util.cc#L59-L77
70    pub fn normalized(&self) -> Self {
71        let mut result = *self;
72        result.normalize();
73        result
74    }
75
76    /// Creates a new `Timestamp` at the start of the provided UTC date.
77    pub fn date(year: i64, month: u8, day: u8) -> Result<Timestamp, TimestampError> {
78        Timestamp::date_time_nanos(year, month, day, 0, 0, 0, 0)
79    }
80
81    /// Creates a new `Timestamp` instance with the provided UTC date and time.
82    pub fn date_time(
83        year: i64,
84        month: u8,
85        day: u8,
86        hour: u8,
87        minute: u8,
88        second: u8,
89    ) -> Result<Timestamp, TimestampError> {
90        Timestamp::date_time_nanos(year, month, day, hour, minute, second, 0)
91    }
92
93    /// Creates a new `Timestamp` instance with the provided UTC date and time.
94    pub fn date_time_nanos(
95        year: i64,
96        month: u8,
97        day: u8,
98        hour: u8,
99        minute: u8,
100        second: u8,
101        nanos: u32,
102    ) -> Result<Timestamp, TimestampError> {
103        let date_time = datetime::DateTime {
104            year,
105            month,
106            day,
107            hour,
108            minute,
109            second,
110            nanos,
111        };
112
113        Timestamp::try_from(date_time)
114    }
115}
116
117impl Name for Timestamp {
118    const PACKAGE: &'static str = PACKAGE;
119    const NAME: &'static str = "Timestamp";
120
121    fn type_url() -> String {
122        type_url_for::<Self>()
123    }
124}
125
126#[cfg(feature = "std")]
127impl From<std::time::SystemTime> for Timestamp {
128    fn from(system_time: std::time::SystemTime) -> Timestamp {
129        let (seconds, nanos) = match system_time.duration_since(std::time::UNIX_EPOCH) {
130            Ok(duration) => {
131                let seconds = i64::try_from(duration.as_secs()).unwrap();
132                (seconds, duration.subsec_nanos() as i32)
133            }
134            Err(error) => {
135                let duration = error.duration();
136                let seconds = i64::try_from(duration.as_secs()).unwrap();
137                let nanos = duration.subsec_nanos() as i32;
138                if nanos == 0 {
139                    (-seconds, 0)
140                } else {
141                    (-seconds - 1, 1_000_000_000 - nanos)
142                }
143            }
144        };
145        Timestamp { seconds, nanos }
146    }
147}
148
149/// A timestamp handling error.
150#[derive(Debug, PartialEq)]
151#[non_exhaustive]
152pub enum TimestampError {
153    /// Indicates that a [`Timestamp`] could not be converted to
154    /// [`SystemTime`][std::time::SystemTime] because it is out of range.
155    ///
156    /// The range of times that can be represented by `SystemTime` depends on the platform. All
157    /// `Timestamp`s are likely representable on 64-bit Unix-like platforms, but other platforms,
158    /// such as Windows and 32-bit Linux, may not be able to represent the full range of
159    /// `Timestamp`s.
160    OutOfSystemRange(Timestamp),
161
162    /// An error indicating failure to parse a timestamp in RFC-3339 format.
163    ParseFailure,
164
165    /// Indicates an error when constructing a timestamp due to invalid date or time data.
166    InvalidDateTime,
167}
168
169impl fmt::Display for TimestampError {
170    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171        match self {
172            TimestampError::OutOfSystemRange(timestamp) => {
173                write!(
174                    f,
175                    "{} is not representable as a `SystemTime` because it is out of range",
176                    timestamp
177                )
178            }
179            TimestampError::ParseFailure => {
180                write!(f, "failed to parse RFC-3339 formatted timestamp")
181            }
182            TimestampError::InvalidDateTime => {
183                write!(f, "invalid date or time")
184            }
185        }
186    }
187}
188
189#[cfg(feature = "std")]
190impl std::error::Error for TimestampError {}
191
192#[cfg(feature = "std")]
193impl TryFrom<Timestamp> for std::time::SystemTime {
194    type Error = TimestampError;
195
196    fn try_from(mut timestamp: Timestamp) -> Result<std::time::SystemTime, Self::Error> {
197        let orig_timestamp = timestamp;
198        timestamp.normalize();
199
200        let system_time = if timestamp.seconds >= 0 {
201            std::time::UNIX_EPOCH.checked_add(time::Duration::from_secs(timestamp.seconds as u64))
202        } else {
203            std::time::UNIX_EPOCH.checked_sub(time::Duration::from_secs(
204                timestamp
205                    .seconds
206                    .checked_neg()
207                    .ok_or(TimestampError::OutOfSystemRange(timestamp))? as u64,
208            ))
209        };
210
211        let system_time = system_time.and_then(|system_time| {
212            system_time.checked_add(time::Duration::from_nanos(timestamp.nanos as u64))
213        });
214
215        system_time.ok_or(TimestampError::OutOfSystemRange(orig_timestamp))
216    }
217}
218
219impl FromStr for Timestamp {
220    type Err = TimestampError;
221
222    fn from_str(s: &str) -> Result<Timestamp, TimestampError> {
223        datetime::parse_timestamp(s).ok_or(TimestampError::ParseFailure)
224    }
225}
226
227impl fmt::Display for Timestamp {
228    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229        datetime::DateTime::from(*self).fmt(f)
230    }
231}
232
233#[cfg(kani)]
234mod proofs {
235    use super::*;
236
237    #[cfg(feature = "std")]
238    #[kani::proof]
239    #[kani::unwind(3)]
240    fn check_timestamp_roundtrip_via_system_time() {
241        let seconds = kani::any();
242        let nanos = kani::any();
243
244        let mut timestamp = Timestamp { seconds, nanos };
245        timestamp.normalize();
246
247        if let Ok(system_time) = std::time::SystemTime::try_from(timestamp) {
248            assert_eq!(Timestamp::from(system_time), timestamp);
249        }
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256
257    #[cfg(feature = "std")]
258    use proptest::prelude::*;
259    #[cfg(feature = "std")]
260    use std::time::{self, SystemTime, UNIX_EPOCH};
261
262    #[cfg(feature = "std")]
263    proptest! {
264        #[test]
265        fn check_system_time_roundtrip(
266            system_time in SystemTime::arbitrary(),
267        ) {
268            prop_assert_eq!(SystemTime::try_from(Timestamp::from(system_time)).unwrap(), system_time);
269        }
270    }
271
272    #[cfg(feature = "std")]
273    #[test]
274    fn check_timestamp_negative_seconds() {
275        // Representative tests for the case of timestamps before the UTC Epoch time:
276        // validate the expected behaviour that "negative second values with fractions
277        // must still have non-negative nanos values that count forward in time"
278        // https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#google.protobuf.Timestamp
279        //
280        // To ensure cross-platform compatibility, all nanosecond values in these
281        // tests are in minimum 100 ns increments.  This does not affect the general
282        // character of the behaviour being tested, but ensures that the tests are
283        // valid for both POSIX (1 ns precision) and Windows (100 ns precision).
284        assert_eq!(
285            Timestamp::from(UNIX_EPOCH - time::Duration::new(1_001, 0)),
286            Timestamp {
287                seconds: -1_001,
288                nanos: 0
289            }
290        );
291        assert_eq!(
292            Timestamp::from(UNIX_EPOCH - time::Duration::new(0, 999_999_900)),
293            Timestamp {
294                seconds: -1,
295                nanos: 100
296            }
297        );
298        assert_eq!(
299            Timestamp::from(UNIX_EPOCH - time::Duration::new(2_001_234, 12_300)),
300            Timestamp {
301                seconds: -2_001_235,
302                nanos: 999_987_700
303            }
304        );
305        assert_eq!(
306            Timestamp::from(UNIX_EPOCH - time::Duration::new(768, 65_432_100)),
307            Timestamp {
308                seconds: -769,
309                nanos: 934_567_900
310            }
311        );
312    }
313
314    #[cfg(all(unix, feature = "std"))]
315    #[test]
316    fn check_timestamp_negative_seconds_1ns() {
317        // UNIX-only test cases with 1 ns precision
318        assert_eq!(
319            Timestamp::from(UNIX_EPOCH - time::Duration::new(0, 999_999_999)),
320            Timestamp {
321                seconds: -1,
322                nanos: 1
323            }
324        );
325        assert_eq!(
326            Timestamp::from(UNIX_EPOCH - time::Duration::new(1_234_567, 123)),
327            Timestamp {
328                seconds: -1_234_568,
329                nanos: 999_999_877
330            }
331        );
332        assert_eq!(
333            Timestamp::from(UNIX_EPOCH - time::Duration::new(890, 987_654_321)),
334            Timestamp {
335                seconds: -891,
336                nanos: 12_345_679
337            }
338        );
339    }
340
341    #[cfg(feature = "std")]
342    #[test]
343    fn check_timestamp_normalize() {
344        // Make sure that `Timestamp::normalize` behaves correctly on and near overflow.
345        #[rustfmt::skip] // Don't mangle the table formatting.
346        let cases = [
347            // --- Table of test cases ---
348            //        test seconds      test nanos  expected seconds  expected nanos
349            (line!(),            0,              0,                0,              0),
350            (line!(),            1,              1,                1,              1),
351            (line!(),           -1,             -1,               -2,    999_999_999),
352            (line!(),            0,    999_999_999,                0,    999_999_999),
353            (line!(),            0,   -999_999_999,               -1,              1),
354            (line!(),            0,  1_000_000_000,                1,              0),
355            (line!(),            0, -1_000_000_000,               -1,              0),
356            (line!(),            0,  1_000_000_001,                1,              1),
357            (line!(),            0, -1_000_000_001,               -2,    999_999_999),
358            (line!(),           -1,              1,               -1,              1),
359            (line!(),            1,             -1,                0,    999_999_999),
360            (line!(),           -1,  1_000_000_000,                0,              0),
361            (line!(),            1, -1_000_000_000,                0,              0),
362            (line!(), i64::MIN    ,              0,     i64::MIN    ,              0),
363            (line!(), i64::MIN + 1,              0,     i64::MIN + 1,              0),
364            (line!(), i64::MIN    ,              1,     i64::MIN    ,              1),
365            (line!(), i64::MIN    ,  1_000_000_000,     i64::MIN + 1,              0),
366            (line!(), i64::MIN    , -1_000_000_000,     i64::MIN    ,              0),
367            (line!(), i64::MIN + 1, -1_000_000_000,     i64::MIN    ,              0),
368            (line!(), i64::MIN + 2, -1_000_000_000,     i64::MIN + 1,              0),
369            (line!(), i64::MIN    , -1_999_999_998,     i64::MIN    ,              0),
370            (line!(), i64::MIN + 1, -1_999_999_998,     i64::MIN    ,              0),
371            (line!(), i64::MIN + 2, -1_999_999_998,     i64::MIN    ,              2),
372            (line!(), i64::MIN    , -1_999_999_999,     i64::MIN    ,              0),
373            (line!(), i64::MIN + 1, -1_999_999_999,     i64::MIN    ,              0),
374            (line!(), i64::MIN + 2, -1_999_999_999,     i64::MIN    ,              1),
375            (line!(), i64::MIN    , -2_000_000_000,     i64::MIN    ,              0),
376            (line!(), i64::MIN + 1, -2_000_000_000,     i64::MIN    ,              0),
377            (line!(), i64::MIN + 2, -2_000_000_000,     i64::MIN    ,              0),
378            (line!(), i64::MIN    ,   -999_999_998,     i64::MIN    ,              0),
379            (line!(), i64::MIN + 1,   -999_999_998,     i64::MIN    ,              2),
380            (line!(), i64::MAX    ,              0,     i64::MAX    ,              0),
381            (line!(), i64::MAX - 1,              0,     i64::MAX - 1,              0),
382            (line!(), i64::MAX    ,             -1,     i64::MAX - 1,    999_999_999),
383            (line!(), i64::MAX    ,  1_000_000_000,     i64::MAX    ,    999_999_999),
384            (line!(), i64::MAX - 1,  1_000_000_000,     i64::MAX    ,              0),
385            (line!(), i64::MAX - 2,  1_000_000_000,     i64::MAX - 1,              0),
386            (line!(), i64::MAX    ,  1_999_999_998,     i64::MAX    ,    999_999_999),
387            (line!(), i64::MAX - 1,  1_999_999_998,     i64::MAX    ,    999_999_998),
388            (line!(), i64::MAX - 2,  1_999_999_998,     i64::MAX - 1,    999_999_998),
389            (line!(), i64::MAX    ,  1_999_999_999,     i64::MAX    ,    999_999_999),
390            (line!(), i64::MAX - 1,  1_999_999_999,     i64::MAX    ,    999_999_999),
391            (line!(), i64::MAX - 2,  1_999_999_999,     i64::MAX - 1,    999_999_999),
392            (line!(), i64::MAX    ,  2_000_000_000,     i64::MAX    ,    999_999_999),
393            (line!(), i64::MAX - 1,  2_000_000_000,     i64::MAX    ,    999_999_999),
394            (line!(), i64::MAX - 2,  2_000_000_000,     i64::MAX    ,              0),
395            (line!(), i64::MAX    ,    999_999_998,     i64::MAX    ,    999_999_998),
396            (line!(), i64::MAX - 1,    999_999_998,     i64::MAX - 1,    999_999_998),
397        ];
398
399        for case in cases.iter() {
400            let test_timestamp = crate::Timestamp {
401                seconds: case.1,
402                nanos: case.2,
403            };
404
405            assert_eq!(
406                test_timestamp.normalized(),
407                crate::Timestamp {
408                    seconds: case.3,
409                    nanos: case.4,
410                },
411                "test case on line {} doesn't match",
412                case.0,
413            );
414        }
415    }
416
417    #[cfg(feature = "arbitrary")]
418    #[test]
419    fn check_timestamp_implements_arbitrary() {
420        use arbitrary::{Arbitrary, Unstructured};
421
422        let mut unstructured = Unstructured::new(&[]);
423
424        assert_eq!(
425            Timestamp::arbitrary(&mut unstructured),
426            Ok(Timestamp {
427                seconds: 0,
428                nanos: 0
429            })
430        );
431    }
432}