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/// Implements the unstable/naive version of `Eq`: a basic equality check on the internal fields of the `Timestamp`.
127/// This implies that `normalized_ts != non_normalized_ts` even if `normalized_ts == non_normalized_ts.normalized()`.
128#[cfg(feature = "std")]
129impl Eq for Timestamp {}
130
131#[cfg(feature = "std")]
132impl std::hash::Hash for Timestamp {
133    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
134        self.seconds.hash(state);
135        self.nanos.hash(state);
136    }
137}
138
139#[cfg(feature = "std")]
140impl From<std::time::SystemTime> for Timestamp {
141    fn from(system_time: std::time::SystemTime) -> Timestamp {
142        let (seconds, nanos) = match system_time.duration_since(std::time::UNIX_EPOCH) {
143            Ok(duration) => {
144                let seconds = i64::try_from(duration.as_secs()).unwrap();
145                (seconds, duration.subsec_nanos() as i32)
146            }
147            Err(error) => {
148                let duration = error.duration();
149                let seconds = i64::try_from(duration.as_secs()).unwrap();
150                let nanos = duration.subsec_nanos() as i32;
151                if nanos == 0 {
152                    (-seconds, 0)
153                } else {
154                    (-seconds - 1, 1_000_000_000 - nanos)
155                }
156            }
157        };
158        Timestamp { seconds, nanos }
159    }
160}
161
162/// A timestamp handling error.
163#[derive(Debug, PartialEq)]
164#[non_exhaustive]
165pub enum TimestampError {
166    /// Indicates that a [`Timestamp`] could not be converted to
167    /// [`SystemTime`][std::time::SystemTime] because it is out of range.
168    ///
169    /// The range of times that can be represented by `SystemTime` depends on the platform. All
170    /// `Timestamp`s are likely representable on 64-bit Unix-like platforms, but other platforms,
171    /// such as Windows and 32-bit Linux, may not be able to represent the full range of
172    /// `Timestamp`s.
173    OutOfSystemRange(Timestamp),
174
175    /// An error indicating failure to parse a timestamp in RFC-3339 format.
176    ParseFailure,
177
178    /// Indicates an error when constructing a timestamp due to invalid date or time data.
179    InvalidDateTime,
180}
181
182impl fmt::Display for TimestampError {
183    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184        match self {
185            TimestampError::OutOfSystemRange(timestamp) => {
186                write!(
187                    f,
188                    "{} is not representable as a `SystemTime` because it is out of range",
189                    timestamp
190                )
191            }
192            TimestampError::ParseFailure => {
193                write!(f, "failed to parse RFC-3339 formatted timestamp")
194            }
195            TimestampError::InvalidDateTime => {
196                write!(f, "invalid date or time")
197            }
198        }
199    }
200}
201
202#[cfg(feature = "std")]
203impl std::error::Error for TimestampError {}
204
205#[cfg(feature = "std")]
206impl TryFrom<Timestamp> for std::time::SystemTime {
207    type Error = TimestampError;
208
209    fn try_from(mut timestamp: Timestamp) -> Result<std::time::SystemTime, Self::Error> {
210        let orig_timestamp = timestamp;
211        timestamp.normalize();
212
213        let system_time = if timestamp.seconds >= 0 {
214            std::time::UNIX_EPOCH.checked_add(time::Duration::from_secs(timestamp.seconds as u64))
215        } else {
216            std::time::UNIX_EPOCH.checked_sub(time::Duration::from_secs(
217                timestamp
218                    .seconds
219                    .checked_neg()
220                    .ok_or(TimestampError::OutOfSystemRange(timestamp))? as u64,
221            ))
222        };
223
224        let system_time = system_time.and_then(|system_time| {
225            system_time.checked_add(time::Duration::from_nanos(timestamp.nanos as u64))
226        });
227
228        system_time.ok_or(TimestampError::OutOfSystemRange(orig_timestamp))
229    }
230}
231
232impl FromStr for Timestamp {
233    type Err = TimestampError;
234
235    fn from_str(s: &str) -> Result<Timestamp, TimestampError> {
236        datetime::parse_timestamp(s).ok_or(TimestampError::ParseFailure)
237    }
238}
239
240impl fmt::Display for Timestamp {
241    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
242        datetime::DateTime::from(*self).fmt(f)
243    }
244}
245
246#[cfg(kani)]
247mod proofs {
248    use super::*;
249
250    #[cfg(feature = "std")]
251    #[kani::proof]
252    #[kani::unwind(3)]
253    fn check_timestamp_roundtrip_via_system_time() {
254        let seconds = kani::any();
255        let nanos = kani::any();
256
257        let mut timestamp = Timestamp { seconds, nanos };
258        timestamp.normalize();
259
260        if let Ok(system_time) = std::time::SystemTime::try_from(timestamp) {
261            assert_eq!(Timestamp::from(system_time), timestamp);
262        }
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269
270    #[cfg(feature = "std")]
271    use proptest::prelude::*;
272    #[cfg(feature = "std")]
273    use std::time::{self, SystemTime, UNIX_EPOCH};
274
275    #[cfg(feature = "std")]
276    proptest! {
277        #[test]
278        fn check_system_time_roundtrip(
279            system_time in SystemTime::arbitrary(),
280        ) {
281            prop_assert_eq!(SystemTime::try_from(Timestamp::from(system_time)).unwrap(), system_time);
282        }
283    }
284
285    #[cfg(feature = "std")]
286    #[test]
287    fn check_timestamp_negative_seconds() {
288        // Representative tests for the case of timestamps before the UTC Epoch time:
289        // validate the expected behaviour that "negative second values with fractions
290        // must still have non-negative nanos values that count forward in time"
291        // https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#google.protobuf.Timestamp
292        //
293        // To ensure cross-platform compatibility, all nanosecond values in these
294        // tests are in minimum 100 ns increments.  This does not affect the general
295        // character of the behaviour being tested, but ensures that the tests are
296        // valid for both POSIX (1 ns precision) and Windows (100 ns precision).
297        assert_eq!(
298            Timestamp::from(UNIX_EPOCH - time::Duration::new(1_001, 0)),
299            Timestamp {
300                seconds: -1_001,
301                nanos: 0
302            }
303        );
304        assert_eq!(
305            Timestamp::from(UNIX_EPOCH - time::Duration::new(0, 999_999_900)),
306            Timestamp {
307                seconds: -1,
308                nanos: 100
309            }
310        );
311        assert_eq!(
312            Timestamp::from(UNIX_EPOCH - time::Duration::new(2_001_234, 12_300)),
313            Timestamp {
314                seconds: -2_001_235,
315                nanos: 999_987_700
316            }
317        );
318        assert_eq!(
319            Timestamp::from(UNIX_EPOCH - time::Duration::new(768, 65_432_100)),
320            Timestamp {
321                seconds: -769,
322                nanos: 934_567_900
323            }
324        );
325    }
326
327    #[cfg(all(unix, feature = "std"))]
328    #[test]
329    fn check_timestamp_negative_seconds_1ns() {
330        // UNIX-only test cases with 1 ns precision
331        assert_eq!(
332            Timestamp::from(UNIX_EPOCH - time::Duration::new(0, 999_999_999)),
333            Timestamp {
334                seconds: -1,
335                nanos: 1
336            }
337        );
338        assert_eq!(
339            Timestamp::from(UNIX_EPOCH - time::Duration::new(1_234_567, 123)),
340            Timestamp {
341                seconds: -1_234_568,
342                nanos: 999_999_877
343            }
344        );
345        assert_eq!(
346            Timestamp::from(UNIX_EPOCH - time::Duration::new(890, 987_654_321)),
347            Timestamp {
348                seconds: -891,
349                nanos: 12_345_679
350            }
351        );
352    }
353
354    #[cfg(feature = "std")]
355    #[test]
356    fn check_timestamp_normalize() {
357        // Make sure that `Timestamp::normalize` behaves correctly on and near overflow.
358        #[rustfmt::skip] // Don't mangle the table formatting.
359        let cases = [
360            // --- Table of test cases ---
361            //        test seconds      test nanos  expected seconds  expected nanos
362            (line!(),            0,              0,                0,              0),
363            (line!(),            1,              1,                1,              1),
364            (line!(),           -1,             -1,               -2,    999_999_999),
365            (line!(),            0,    999_999_999,                0,    999_999_999),
366            (line!(),            0,   -999_999_999,               -1,              1),
367            (line!(),            0,  1_000_000_000,                1,              0),
368            (line!(),            0, -1_000_000_000,               -1,              0),
369            (line!(),            0,  1_000_000_001,                1,              1),
370            (line!(),            0, -1_000_000_001,               -2,    999_999_999),
371            (line!(),           -1,              1,               -1,              1),
372            (line!(),            1,             -1,                0,    999_999_999),
373            (line!(),           -1,  1_000_000_000,                0,              0),
374            (line!(),            1, -1_000_000_000,                0,              0),
375            (line!(), i64::MIN    ,              0,     i64::MIN    ,              0),
376            (line!(), i64::MIN + 1,              0,     i64::MIN + 1,              0),
377            (line!(), i64::MIN    ,              1,     i64::MIN    ,              1),
378            (line!(), i64::MIN    ,  1_000_000_000,     i64::MIN + 1,              0),
379            (line!(), i64::MIN    , -1_000_000_000,     i64::MIN    ,              0),
380            (line!(), i64::MIN + 1, -1_000_000_000,     i64::MIN    ,              0),
381            (line!(), i64::MIN + 2, -1_000_000_000,     i64::MIN + 1,              0),
382            (line!(), i64::MIN    , -1_999_999_998,     i64::MIN    ,              0),
383            (line!(), i64::MIN + 1, -1_999_999_998,     i64::MIN    ,              0),
384            (line!(), i64::MIN + 2, -1_999_999_998,     i64::MIN    ,              2),
385            (line!(), i64::MIN    , -1_999_999_999,     i64::MIN    ,              0),
386            (line!(), i64::MIN + 1, -1_999_999_999,     i64::MIN    ,              0),
387            (line!(), i64::MIN + 2, -1_999_999_999,     i64::MIN    ,              1),
388            (line!(), i64::MIN    , -2_000_000_000,     i64::MIN    ,              0),
389            (line!(), i64::MIN + 1, -2_000_000_000,     i64::MIN    ,              0),
390            (line!(), i64::MIN + 2, -2_000_000_000,     i64::MIN    ,              0),
391            (line!(), i64::MIN    ,   -999_999_998,     i64::MIN    ,              0),
392            (line!(), i64::MIN + 1,   -999_999_998,     i64::MIN    ,              2),
393            (line!(), i64::MAX    ,              0,     i64::MAX    ,              0),
394            (line!(), i64::MAX - 1,              0,     i64::MAX - 1,              0),
395            (line!(), i64::MAX    ,             -1,     i64::MAX - 1,    999_999_999),
396            (line!(), i64::MAX    ,  1_000_000_000,     i64::MAX    ,    999_999_999),
397            (line!(), i64::MAX - 1,  1_000_000_000,     i64::MAX    ,              0),
398            (line!(), i64::MAX - 2,  1_000_000_000,     i64::MAX - 1,              0),
399            (line!(), i64::MAX    ,  1_999_999_998,     i64::MAX    ,    999_999_999),
400            (line!(), i64::MAX - 1,  1_999_999_998,     i64::MAX    ,    999_999_998),
401            (line!(), i64::MAX - 2,  1_999_999_998,     i64::MAX - 1,    999_999_998),
402            (line!(), i64::MAX    ,  1_999_999_999,     i64::MAX    ,    999_999_999),
403            (line!(), i64::MAX - 1,  1_999_999_999,     i64::MAX    ,    999_999_999),
404            (line!(), i64::MAX - 2,  1_999_999_999,     i64::MAX - 1,    999_999_999),
405            (line!(), i64::MAX    ,  2_000_000_000,     i64::MAX    ,    999_999_999),
406            (line!(), i64::MAX - 1,  2_000_000_000,     i64::MAX    ,    999_999_999),
407            (line!(), i64::MAX - 2,  2_000_000_000,     i64::MAX    ,              0),
408            (line!(), i64::MAX    ,    999_999_998,     i64::MAX    ,    999_999_998),
409            (line!(), i64::MAX - 1,    999_999_998,     i64::MAX - 1,    999_999_998),
410        ];
411
412        for case in cases.iter() {
413            let test_timestamp = crate::Timestamp {
414                seconds: case.1,
415                nanos: case.2,
416            };
417
418            assert_eq!(
419                test_timestamp.normalized(),
420                crate::Timestamp {
421                    seconds: case.3,
422                    nanos: case.4,
423                },
424                "test case on line {} doesn't match",
425                case.0,
426            );
427        }
428    }
429
430    #[cfg(feature = "arbitrary")]
431    #[test]
432    fn check_timestamp_implements_arbitrary() {
433        use arbitrary::{Arbitrary, Unstructured};
434
435        let mut unstructured = Unstructured::new(&[]);
436
437        assert_eq!(
438            Timestamp::arbitrary(&mut unstructured),
439            Ok(Timestamp {
440                seconds: 0,
441                nanos: 0
442            })
443        );
444    }
445}