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, ".{:09}s", nanos)
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://developers.google.com/protocol-buffers/docs/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
167#[cfg(feature = "std")]
168impl std::error::Error for DurationError {}
169
170impl FromStr for Duration {
171    type Err = DurationError;
172
173    fn from_str(s: &str) -> Result<Duration, DurationError> {
174        datetime::parse_duration(s).ok_or(DurationError::ParseFailure)
175    }
176}
177
178#[cfg(feature = "chrono")]
179mod chrono {
180    use ::chrono::TimeDelta;
181
182    use super::*;
183
184    impl From<::chrono::TimeDelta> for Duration {
185        fn from(value: ::chrono::TimeDelta) -> Self {
186            let mut result = Self {
187                seconds: value.num_seconds(),
188                nanos: value.subsec_nanos(),
189            };
190            result.normalize();
191            result
192        }
193    }
194
195    impl TryFrom<Duration> for ::chrono::TimeDelta {
196        type Error = DurationError;
197
198        fn try_from(mut value: Duration) -> Result<TimeDelta, duration::DurationError> {
199            value.normalize();
200            let seconds = TimeDelta::try_seconds(value.seconds).ok_or(DurationError::OutOfRange)?;
201            let nanos = TimeDelta::nanoseconds(value.nanos.into());
202            seconds.checked_add(&nanos).ok_or(DurationError::OutOfRange)
203        }
204    }
205}
206
207#[cfg(kani)]
208mod proofs {
209    use super::*;
210
211    #[cfg(feature = "std")]
212    #[kani::proof]
213    fn check_duration_std_roundtrip() {
214        let seconds = kani::any();
215        let nanos = kani::any();
216        kani::assume(nanos < 1_000_000_000);
217        let std_duration = std::time::Duration::new(seconds, nanos);
218        let Ok(prost_duration) = Duration::try_from(std_duration) else {
219            // Test case not valid: duration out of range
220            return;
221        };
222        assert_eq!(
223            time::Duration::try_from(prost_duration).unwrap(),
224            std_duration
225        );
226
227        if std_duration != time::Duration::default() {
228            let neg_prost_duration = Duration {
229                seconds: -prost_duration.seconds,
230                nanos: -prost_duration.nanos,
231            };
232
233            assert!(matches!(
234                time::Duration::try_from(neg_prost_duration),
235                Err(DurationError::NegativeDuration(d)) if d == std_duration,
236            ))
237        }
238    }
239
240    #[cfg(feature = "std")]
241    #[kani::proof]
242    fn check_duration_std_roundtrip_nanos() {
243        let seconds = 0;
244        let nanos = kani::any();
245        let std_duration = std::time::Duration::new(seconds, nanos);
246        let Ok(prost_duration) = Duration::try_from(std_duration) else {
247            // Test case not valid: duration out of range
248            return;
249        };
250        assert_eq!(
251            time::Duration::try_from(prost_duration).unwrap(),
252            std_duration
253        );
254
255        if std_duration != time::Duration::default() {
256            let neg_prost_duration = Duration {
257                seconds: -prost_duration.seconds,
258                nanos: -prost_duration.nanos,
259            };
260
261            assert!(matches!(
262                time::Duration::try_from(neg_prost_duration),
263                Err(DurationError::NegativeDuration(d)) if d == std_duration,
264            ))
265        }
266    }
267
268    #[cfg(feature = "chrono")]
269    #[kani::proof]
270    fn check_duration_chrono_roundtrip() {
271        let seconds = kani::any();
272        let nanos = kani::any();
273        let prost_duration = Duration { seconds, nanos };
274        match ::chrono::TimeDelta::try_from(prost_duration) {
275            Err(DurationError::OutOfRange) => {
276                // Test case not valid: duration out of range
277                return;
278            }
279            Err(err) => {
280                panic!("Unexpected error: {err}")
281            }
282            Ok(chrono_duration) => {
283                let mut normalized_prost_duration = prost_duration;
284                normalized_prost_duration.normalize();
285                assert_eq!(
286                    Duration::try_from(chrono_duration).unwrap(),
287                    normalized_prost_duration
288                );
289            }
290        }
291    }
292}
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297
298    #[cfg(feature = "std")]
299    #[test]
300    fn test_duration_from_str() {
301        assert_eq!(
302            Duration::from_str("0s"),
303            Ok(Duration {
304                seconds: 0,
305                nanos: 0
306            })
307        );
308        assert_eq!(
309            Duration::from_str("123s"),
310            Ok(Duration {
311                seconds: 123,
312                nanos: 0
313            })
314        );
315        assert_eq!(
316            Duration::from_str("0.123s"),
317            Ok(Duration {
318                seconds: 0,
319                nanos: 123_000_000
320            })
321        );
322        assert_eq!(
323            Duration::from_str("-123s"),
324            Ok(Duration {
325                seconds: -123,
326                nanos: 0
327            })
328        );
329        assert_eq!(
330            Duration::from_str("-0.123s"),
331            Ok(Duration {
332                seconds: 0,
333                nanos: -123_000_000
334            })
335        );
336        assert_eq!(
337            Duration::from_str("22041211.6666666666666s"),
338            Ok(Duration {
339                seconds: 22041211,
340                nanos: 666_666_666
341            })
342        );
343    }
344
345    #[cfg(feature = "std")]
346    #[test]
347    fn test_format_duration() {
348        assert_eq!(
349            "0s",
350            Duration {
351                seconds: 0,
352                nanos: 0
353            }
354            .to_string()
355        );
356        assert_eq!(
357            "123s",
358            Duration {
359                seconds: 123,
360                nanos: 0
361            }
362            .to_string()
363        );
364        assert_eq!(
365            "0.123s",
366            Duration {
367                seconds: 0,
368                nanos: 123_000_000
369            }
370            .to_string()
371        );
372        assert_eq!(
373            "-123s",
374            Duration {
375                seconds: -123,
376                nanos: 0
377            }
378            .to_string()
379        );
380        assert_eq!(
381            "-0.123s",
382            Duration {
383                seconds: 0,
384                nanos: -123_000_000
385            }
386            .to_string()
387        );
388    }
389
390    #[cfg(feature = "std")]
391    #[test]
392    fn check_duration_try_from_negative_nanos() {
393        let seconds: u64 = 0;
394        let nanos: u32 = 1;
395        let std_duration = std::time::Duration::new(seconds, nanos);
396
397        let neg_prost_duration = Duration {
398            seconds: 0,
399            nanos: -1,
400        };
401
402        assert!(matches!(
403           time::Duration::try_from(neg_prost_duration),
404           Err(DurationError::NegativeDuration(d)) if d == std_duration,
405        ))
406    }
407
408    #[test]
409    fn check_duration_normalize() {
410        #[rustfmt::skip] // Don't mangle the table formatting.
411        let cases = [
412            // --- Table of test cases ---
413            //        test seconds      test nanos  expected seconds  expected nanos
414            (line!(),            0,              0,                0,              0),
415            (line!(),            1,              1,                1,              1),
416            (line!(),           -1,             -1,               -1,             -1),
417            (line!(),            0,    999_999_999,                0,    999_999_999),
418            (line!(),            0,   -999_999_999,                0,   -999_999_999),
419            (line!(),            0,  1_000_000_000,                1,              0),
420            (line!(),            0, -1_000_000_000,               -1,              0),
421            (line!(),            0,  1_000_000_001,                1,              1),
422            (line!(),            0, -1_000_000_001,               -1,             -1),
423            (line!(),           -1,              1,                0,   -999_999_999),
424            (line!(),            1,             -1,                0,    999_999_999),
425            (line!(),           -1,  1_000_000_000,                0,              0),
426            (line!(),            1, -1_000_000_000,                0,              0),
427            (line!(), i64::MIN    ,              0,     i64::MIN    ,              0),
428            (line!(), i64::MIN + 1,              0,     i64::MIN + 1,              0),
429            (line!(), i64::MIN    ,              1,     i64::MIN + 1,   -999_999_999),
430            (line!(), i64::MIN    ,  1_000_000_000,     i64::MIN + 1,              0),
431            (line!(), i64::MIN    , -1_000_000_000,     i64::MIN    ,   -999_999_999),
432            (line!(), i64::MIN + 1, -1_000_000_000,     i64::MIN    ,              0),
433            (line!(), i64::MIN + 2, -1_000_000_000,     i64::MIN + 1,              0),
434            (line!(), i64::MIN    , -1_999_999_998,     i64::MIN    ,   -999_999_999),
435            (line!(), i64::MIN + 1, -1_999_999_998,     i64::MIN    ,   -999_999_998),
436            (line!(), i64::MIN + 2, -1_999_999_998,     i64::MIN + 1,   -999_999_998),
437            (line!(), i64::MIN    , -1_999_999_999,     i64::MIN    ,   -999_999_999),
438            (line!(), i64::MIN + 1, -1_999_999_999,     i64::MIN    ,   -999_999_999),
439            (line!(), i64::MIN + 2, -1_999_999_999,     i64::MIN + 1,   -999_999_999),
440            (line!(), i64::MIN    , -2_000_000_000,     i64::MIN    ,   -999_999_999),
441            (line!(), i64::MIN + 1, -2_000_000_000,     i64::MIN    ,   -999_999_999),
442            (line!(), i64::MIN + 2, -2_000_000_000,     i64::MIN    ,              0),
443            (line!(), i64::MIN    ,   -999_999_998,     i64::MIN    ,   -999_999_998),
444            (line!(), i64::MIN + 1,   -999_999_998,     i64::MIN + 1,   -999_999_998),
445            (line!(), i64::MAX    ,              0,     i64::MAX    ,              0),
446            (line!(), i64::MAX - 1,              0,     i64::MAX - 1,              0),
447            (line!(), i64::MAX    ,             -1,     i64::MAX - 1,    999_999_999),
448            (line!(), i64::MAX    ,  1_000_000_000,     i64::MAX    ,    999_999_999),
449            (line!(), i64::MAX - 1,  1_000_000_000,     i64::MAX    ,              0),
450            (line!(), i64::MAX - 2,  1_000_000_000,     i64::MAX - 1,              0),
451            (line!(), i64::MAX    ,  1_999_999_998,     i64::MAX    ,    999_999_999),
452            (line!(), i64::MAX - 1,  1_999_999_998,     i64::MAX    ,    999_999_998),
453            (line!(), i64::MAX - 2,  1_999_999_998,     i64::MAX - 1,    999_999_998),
454            (line!(), i64::MAX    ,  1_999_999_999,     i64::MAX    ,    999_999_999),
455            (line!(), i64::MAX - 1,  1_999_999_999,     i64::MAX    ,    999_999_999),
456            (line!(), i64::MAX - 2,  1_999_999_999,     i64::MAX - 1,    999_999_999),
457            (line!(), i64::MAX    ,  2_000_000_000,     i64::MAX    ,    999_999_999),
458            (line!(), i64::MAX - 1,  2_000_000_000,     i64::MAX    ,    999_999_999),
459            (line!(), i64::MAX - 2,  2_000_000_000,     i64::MAX    ,              0),
460            (line!(), i64::MAX    ,    999_999_998,     i64::MAX    ,    999_999_998),
461            (line!(), i64::MAX - 1,    999_999_998,     i64::MAX - 1,    999_999_998),
462        ];
463
464        for case in cases.iter() {
465            let test_duration = Duration {
466                seconds: case.1,
467                nanos: case.2,
468            };
469
470            assert_eq!(
471                test_duration.normalized(),
472                Duration {
473                    seconds: case.3,
474                    nanos: case.4,
475                },
476                "test case on line {} doesn't match",
477                case.0,
478            );
479        }
480    }
481}