prost_types/
duration.rs

1use super::*;
2
3impl Duration {
4    /// Normalizes the duration 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#L79-L100
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 least normal value.
20                self.seconds = i64::MIN;
21                self.nanos = -NANOS_MAX;
22            } else {
23                // Positive overflow! Set to the greatest normal value.
24                self.seconds = i64::MAX;
25                self.nanos = NANOS_MAX;
26            }
27        }
28
29        // nanos should have the same sign as seconds.
30        if self.seconds < 0 && self.nanos > 0 {
31            if let Some(seconds) = self.seconds.checked_add(1) {
32                self.seconds = seconds;
33                self.nanos -= NANOS_PER_SECOND;
34            } else {
35                // Positive overflow! Set to the greatest normal value.
36                debug_assert_eq!(self.seconds, i64::MAX);
37                self.nanos = NANOS_MAX;
38            }
39        } else if self.seconds > 0 && self.nanos < 0 {
40            if let Some(seconds) = self.seconds.checked_sub(1) {
41                self.seconds = seconds;
42                self.nanos += NANOS_PER_SECOND;
43            } else {
44                // Negative overflow! Set to the least normal value.
45                debug_assert_eq!(self.seconds, i64::MIN);
46                self.nanos = -NANOS_MAX;
47            }
48        }
49        // TODO: should this be checked?
50        // debug_assert!(self.seconds >= -315_576_000_000 && self.seconds <= 315_576_000_000,
51        //               "invalid duration: {:?}", self);
52    }
53
54    /// Returns a normalized copy of the duration to a canonical format.
55    ///
56    /// Based on [`google::protobuf::util::CreateNormalized`][1].
57    ///
58    /// [1]: https://github.com/google/protobuf/blob/v3.3.2/src/google/protobuf/util/time_util.cc#L79-L100
59    pub fn normalized(&self) -> Self {
60        let mut result = *self;
61        result.normalize();
62        result
63    }
64}
65
66impl Name for Duration {
67    const PACKAGE: &'static str = PACKAGE;
68    const NAME: &'static str = "Duration";
69
70    fn type_url() -> String {
71        type_url_for::<Self>()
72    }
73}
74
75impl TryFrom<time::Duration> for Duration {
76    type Error = DurationError;
77
78    /// Converts a `std::time::Duration` to a `Duration`, failing if the duration is too large.
79    fn try_from(duration: time::Duration) -> Result<Duration, DurationError> {
80        let seconds = i64::try_from(duration.as_secs()).map_err(|_| DurationError::OutOfRange)?;
81        let nanos = duration.subsec_nanos() as i32;
82
83        let duration = Duration { seconds, nanos };
84        Ok(duration.normalized())
85    }
86}
87
88impl TryFrom<Duration> for time::Duration {
89    type Error = DurationError;
90
91    /// Converts a `Duration` to a `std::time::Duration`, failing if the duration is negative.
92    fn try_from(mut duration: Duration) -> Result<time::Duration, DurationError> {
93        duration.normalize();
94        if duration.seconds >= 0 && duration.nanos >= 0 {
95            Ok(time::Duration::new(
96                duration.seconds as u64,
97                duration.nanos as u32,
98            ))
99        } else {
100            Err(DurationError::NegativeDuration(time::Duration::new(
101                (-duration.seconds) as u64,
102                (-duration.nanos) as u32,
103            )))
104        }
105    }
106}
107
108impl fmt::Display for Duration {
109    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110        let d = self.normalized();
111        if self.seconds < 0 || self.nanos < 0 {
112            write!(f, "-")?;
113        }
114        write!(f, "{}", d.seconds.abs())?;
115
116        // Format subseconds to either nothing, millis, micros, or nanos.
117        let nanos = d.nanos.abs();
118        if nanos == 0 {
119            write!(f, "s")
120        } else if nanos % 1_000_000 == 0 {
121            write!(f, ".{:03}s", nanos / 1_000_000)
122        } else if nanos % 1_000 == 0 {
123            write!(f, ".{:06}s", nanos / 1_000)
124        } else {
125            write!(f, ".{nanos:09}s")
126        }
127    }
128}
129
130/// A duration handling error.
131#[derive(Debug, PartialEq)]
132#[non_exhaustive]
133pub enum DurationError {
134    /// Indicates failure to parse a [`Duration`] from a string.
135    ///
136    /// The [`Duration`] string format is specified in the [Protobuf JSON mapping specification][1].
137    ///
138    /// [1]: https://protobuf.dev/programming-guides/proto3/#json
139    ParseFailure,
140
141    /// Indicates failure to convert a `prost_types::Duration` to a `std::time::Duration` because
142    /// the duration is negative. The included `std::time::Duration` matches the magnitude of the
143    /// original negative `prost_types::Duration`.
144    NegativeDuration(time::Duration),
145
146    /// Indicates failure to convert a `std::time::Duration` to a `prost_types::Duration`.
147    ///
148    /// Converting a `std::time::Duration` to a `prost_types::Duration` fails if the magnitude
149    /// exceeds that representable by `prost_types::Duration`.
150    OutOfRange,
151}
152
153impl fmt::Display for DurationError {
154    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155        match self {
156            DurationError::ParseFailure => write!(f, "failed to parse duration"),
157            DurationError::NegativeDuration(duration) => {
158                write!(f, "failed to convert negative duration: {duration:?}")
159            }
160            DurationError::OutOfRange => {
161                write!(f, "failed to convert duration out of range")
162            }
163        }
164    }
165}
166
167impl core::error::Error for DurationError {}
168
169impl FromStr for Duration {
170    type Err = DurationError;
171
172    fn from_str(s: &str) -> Result<Duration, DurationError> {
173        datetime::parse_duration(s).ok_or(DurationError::ParseFailure)
174    }
175}
176
177#[cfg(feature = "chrono")]
178mod chrono {
179    use ::chrono::TimeDelta;
180
181    use super::*;
182
183    impl From<::chrono::TimeDelta> for Duration {
184        fn from(value: ::chrono::TimeDelta) -> Self {
185            let mut result = Self {
186                seconds: value.num_seconds(),
187                nanos: value.subsec_nanos(),
188            };
189            result.normalize();
190            result
191        }
192    }
193
194    impl TryFrom<Duration> for ::chrono::TimeDelta {
195        type Error = DurationError;
196
197        fn try_from(mut value: Duration) -> Result<TimeDelta, duration::DurationError> {
198            value.normalize();
199            let seconds = TimeDelta::try_seconds(value.seconds).ok_or(DurationError::OutOfRange)?;
200            let nanos = TimeDelta::nanoseconds(value.nanos.into());
201            seconds.checked_add(&nanos).ok_or(DurationError::OutOfRange)
202        }
203    }
204}
205
206#[cfg(kani)]
207mod proofs {
208    use super::*;
209
210    #[cfg(feature = "std")]
211    #[kani::proof]
212    fn check_duration_std_roundtrip() {
213        let seconds = kani::any();
214        let nanos = kani::any();
215        kani::assume(nanos < 1_000_000_000);
216        let std_duration = std::time::Duration::new(seconds, nanos);
217        let Ok(prost_duration) = Duration::try_from(std_duration) else {
218            // Test case not valid: duration out of range
219            return;
220        };
221        assert_eq!(
222            time::Duration::try_from(prost_duration).unwrap(),
223            std_duration
224        );
225
226        if std_duration != time::Duration::default() {
227            let neg_prost_duration = Duration {
228                seconds: -prost_duration.seconds,
229                nanos: -prost_duration.nanos,
230            };
231
232            assert!(matches!(
233                time::Duration::try_from(neg_prost_duration),
234                Err(DurationError::NegativeDuration(d)) if d == std_duration,
235            ))
236        }
237    }
238
239    #[cfg(feature = "std")]
240    #[kani::proof]
241    fn check_duration_std_roundtrip_nanos() {
242        let seconds = 0;
243        let nanos = kani::any();
244        let std_duration = std::time::Duration::new(seconds, nanos);
245        let Ok(prost_duration) = Duration::try_from(std_duration) else {
246            // Test case not valid: duration out of range
247            return;
248        };
249        assert_eq!(
250            time::Duration::try_from(prost_duration).unwrap(),
251            std_duration
252        );
253
254        if std_duration != time::Duration::default() {
255            let neg_prost_duration = Duration {
256                seconds: -prost_duration.seconds,
257                nanos: -prost_duration.nanos,
258            };
259
260            assert!(matches!(
261                time::Duration::try_from(neg_prost_duration),
262                Err(DurationError::NegativeDuration(d)) if d == std_duration,
263            ))
264        }
265    }
266
267    #[cfg(feature = "chrono")]
268    #[kani::proof]
269    fn check_duration_chrono_roundtrip() {
270        let seconds = kani::any();
271        let nanos = kani::any();
272        let prost_duration = Duration { seconds, nanos };
273        match ::chrono::TimeDelta::try_from(prost_duration) {
274            Err(DurationError::OutOfRange) => {
275                // Test case not valid: duration out of range
276                return;
277            }
278            Err(err) => {
279                panic!("Unexpected error: {err}")
280            }
281            Ok(chrono_duration) => {
282                let mut normalized_prost_duration = prost_duration;
283                normalized_prost_duration.normalize();
284                assert_eq!(
285                    Duration::try_from(chrono_duration).unwrap(),
286                    normalized_prost_duration
287                );
288            }
289        }
290    }
291}
292
293#[cfg(test)]
294mod tests {
295    use super::*;
296
297    #[cfg(feature = "std")]
298    #[test]
299    fn test_duration_from_str() {
300        assert_eq!(
301            Duration::from_str("0s"),
302            Ok(Duration {
303                seconds: 0,
304                nanos: 0
305            })
306        );
307        assert_eq!(
308            Duration::from_str("123s"),
309            Ok(Duration {
310                seconds: 123,
311                nanos: 0
312            })
313        );
314        assert_eq!(
315            Duration::from_str("0.123s"),
316            Ok(Duration {
317                seconds: 0,
318                nanos: 123_000_000
319            })
320        );
321        assert_eq!(
322            Duration::from_str("-123s"),
323            Ok(Duration {
324                seconds: -123,
325                nanos: 0
326            })
327        );
328        assert_eq!(
329            Duration::from_str("-0.123s"),
330            Ok(Duration {
331                seconds: 0,
332                nanos: -123_000_000
333            })
334        );
335        assert_eq!(
336            Duration::from_str("22041211.6666666666666s"),
337            Ok(Duration {
338                seconds: 22041211,
339                nanos: 666_666_666
340            })
341        );
342    }
343
344    #[cfg(feature = "std")]
345    #[test]
346    fn test_format_duration() {
347        assert_eq!(
348            "0s",
349            Duration {
350                seconds: 0,
351                nanos: 0
352            }
353            .to_string()
354        );
355        assert_eq!(
356            "123s",
357            Duration {
358                seconds: 123,
359                nanos: 0
360            }
361            .to_string()
362        );
363        assert_eq!(
364            "0.123s",
365            Duration {
366                seconds: 0,
367                nanos: 123_000_000
368            }
369            .to_string()
370        );
371        assert_eq!(
372            "-123s",
373            Duration {
374                seconds: -123,
375                nanos: 0
376            }
377            .to_string()
378        );
379        assert_eq!(
380            "-0.123s",
381            Duration {
382                seconds: 0,
383                nanos: -123_000_000
384            }
385            .to_string()
386        );
387    }
388
389    #[cfg(feature = "std")]
390    #[test]
391    fn check_duration_try_from_negative_nanos() {
392        let seconds: u64 = 0;
393        let nanos: u32 = 1;
394        let std_duration = std::time::Duration::new(seconds, nanos);
395
396        let neg_prost_duration = Duration {
397            seconds: 0,
398            nanos: -1,
399        };
400
401        assert!(matches!(
402           time::Duration::try_from(neg_prost_duration),
403           Err(DurationError::NegativeDuration(d)) if d == std_duration,
404        ))
405    }
406
407    #[test]
408    fn check_duration_normalize() {
409        #[rustfmt::skip] // Don't mangle the table formatting.
410        let cases = [
411            // --- Table of test cases ---
412            //        test seconds      test nanos  expected seconds  expected nanos
413            (line!(),            0,              0,                0,              0),
414            (line!(),            1,              1,                1,              1),
415            (line!(),           -1,             -1,               -1,             -1),
416            (line!(),            0,    999_999_999,                0,    999_999_999),
417            (line!(),            0,   -999_999_999,                0,   -999_999_999),
418            (line!(),            0,  1_000_000_000,                1,              0),
419            (line!(),            0, -1_000_000_000,               -1,              0),
420            (line!(),            0,  1_000_000_001,                1,              1),
421            (line!(),            0, -1_000_000_001,               -1,             -1),
422            (line!(),           -1,              1,                0,   -999_999_999),
423            (line!(),            1,             -1,                0,    999_999_999),
424            (line!(),           -1,  1_000_000_000,                0,              0),
425            (line!(),            1, -1_000_000_000,                0,              0),
426            (line!(), i64::MIN    ,              0,     i64::MIN    ,              0),
427            (line!(), i64::MIN + 1,              0,     i64::MIN + 1,              0),
428            (line!(), i64::MIN    ,              1,     i64::MIN + 1,   -999_999_999),
429            (line!(), i64::MIN    ,  1_000_000_000,     i64::MIN + 1,              0),
430            (line!(), i64::MIN    , -1_000_000_000,     i64::MIN    ,   -999_999_999),
431            (line!(), i64::MIN + 1, -1_000_000_000,     i64::MIN    ,              0),
432            (line!(), i64::MIN + 2, -1_000_000_000,     i64::MIN + 1,              0),
433            (line!(), i64::MIN    , -1_999_999_998,     i64::MIN    ,   -999_999_999),
434            (line!(), i64::MIN + 1, -1_999_999_998,     i64::MIN    ,   -999_999_998),
435            (line!(), i64::MIN + 2, -1_999_999_998,     i64::MIN + 1,   -999_999_998),
436            (line!(), i64::MIN    , -1_999_999_999,     i64::MIN    ,   -999_999_999),
437            (line!(), i64::MIN + 1, -1_999_999_999,     i64::MIN    ,   -999_999_999),
438            (line!(), i64::MIN + 2, -1_999_999_999,     i64::MIN + 1,   -999_999_999),
439            (line!(), i64::MIN    , -2_000_000_000,     i64::MIN    ,   -999_999_999),
440            (line!(), i64::MIN + 1, -2_000_000_000,     i64::MIN    ,   -999_999_999),
441            (line!(), i64::MIN + 2, -2_000_000_000,     i64::MIN    ,              0),
442            (line!(), i64::MIN    ,   -999_999_998,     i64::MIN    ,   -999_999_998),
443            (line!(), i64::MIN + 1,   -999_999_998,     i64::MIN + 1,   -999_999_998),
444            (line!(), i64::MAX    ,              0,     i64::MAX    ,              0),
445            (line!(), i64::MAX - 1,              0,     i64::MAX - 1,              0),
446            (line!(), i64::MAX    ,             -1,     i64::MAX - 1,    999_999_999),
447            (line!(), i64::MAX    ,  1_000_000_000,     i64::MAX    ,    999_999_999),
448            (line!(), i64::MAX - 1,  1_000_000_000,     i64::MAX    ,              0),
449            (line!(), i64::MAX - 2,  1_000_000_000,     i64::MAX - 1,              0),
450            (line!(), i64::MAX    ,  1_999_999_998,     i64::MAX    ,    999_999_999),
451            (line!(), i64::MAX - 1,  1_999_999_998,     i64::MAX    ,    999_999_998),
452            (line!(), i64::MAX - 2,  1_999_999_998,     i64::MAX - 1,    999_999_998),
453            (line!(), i64::MAX    ,  1_999_999_999,     i64::MAX    ,    999_999_999),
454            (line!(), i64::MAX - 1,  1_999_999_999,     i64::MAX    ,    999_999_999),
455            (line!(), i64::MAX - 2,  1_999_999_999,     i64::MAX - 1,    999_999_999),
456            (line!(), i64::MAX    ,  2_000_000_000,     i64::MAX    ,    999_999_999),
457            (line!(), i64::MAX - 1,  2_000_000_000,     i64::MAX    ,    999_999_999),
458            (line!(), i64::MAX - 2,  2_000_000_000,     i64::MAX    ,              0),
459            (line!(), i64::MAX    ,    999_999_998,     i64::MAX    ,    999_999_998),
460            (line!(), i64::MAX - 1,    999_999_998,     i64::MAX - 1,    999_999_998),
461        ];
462
463        for case in cases.iter() {
464            let test_duration = Duration {
465                seconds: case.1,
466                nanos: case.2,
467            };
468
469            assert_eq!(
470                test_duration.normalized(),
471                Duration {
472                    seconds: case.3,
473                    nanos: case.4,
474                },
475                "test case on line {} doesn't match",
476                case.0,
477            );
478        }
479    }
480}