prost_types/
duration.rs

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