Skip to main content

buffa_types/
timestamp_ext.rs

1//! Ergonomic helpers for [`google::protobuf::Timestamp`](crate::google::protobuf::Timestamp).
2
3use crate::google::protobuf::Timestamp;
4
5/// Errors that can occur when converting a [`Timestamp`] to a Rust time type.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
7pub enum TimestampError {
8    /// The nanoseconds field is outside the valid range `[0, 999_999_999]`.
9    #[error("nanos field must be in [0, 999_999_999]")]
10    InvalidNanos,
11    /// The timestamp is too far in the past or future for the target type.
12    #[error("timestamp is out of range for SystemTime")]
13    Overflow,
14}
15
16impl Timestamp {
17    /// Create a [`Timestamp`] from a Unix epoch offset.
18    ///
19    /// `seconds` is the number of seconds since (or before, if negative) the
20    /// Unix epoch.  `nanos` must be in `[0, 999_999_999]`.
21    ///
22    /// # Panics
23    ///
24    /// Panics in debug mode if `nanos` is outside `[0, 999_999_999]`.
25    /// In release mode the value is stored as-is, producing an invalid
26    /// timestamp.  Use [`Timestamp::from_unix_checked`] for a checked
27    /// variant that returns `None` on invalid input.
28    pub fn from_unix(seconds: i64, nanos: i32) -> Self {
29        debug_assert!(
30            (0..=999_999_999).contains(&nanos),
31            "nanos ({nanos}) must be in [0, 999_999_999]"
32        );
33        Self {
34            seconds,
35            nanos,
36            ..Default::default()
37        }
38    }
39
40    /// Create a [`Timestamp`] from a whole number of Unix seconds (nanoseconds = 0).
41    ///
42    /// This is a convenience shorthand for `Timestamp::from_unix(seconds, 0)`.
43    pub fn from_unix_secs(seconds: i64) -> Self {
44        Self {
45            seconds,
46            nanos: 0,
47            ..Default::default()
48        }
49    }
50
51    /// Create a [`Timestamp`] from a Unix epoch offset, returning `None` if
52    /// `nanos` is outside `[0, 999_999_999]`.
53    pub fn from_unix_checked(seconds: i64, nanos: i32) -> Option<Self> {
54        if (0..=999_999_999).contains(&nanos) {
55            Some(Self {
56                seconds,
57                nanos,
58                ..Default::default()
59            })
60        } else {
61            None
62        }
63    }
64
65    /// Return the current wall-clock time as a [`Timestamp`].
66    ///
67    /// Requires the `std` feature.
68    #[cfg(feature = "std")]
69    pub fn now() -> Self {
70        std::time::SystemTime::now().into()
71    }
72}
73
74#[cfg(feature = "std")]
75impl TryFrom<Timestamp> for std::time::SystemTime {
76    type Error = TimestampError;
77
78    /// Convert a protobuf [`Timestamp`] to a [`std::time::SystemTime`].
79    ///
80    /// # Errors
81    ///
82    /// Returns [`TimestampError::InvalidNanos`] if `nanos` is outside
83    /// `[0, 999_999_999]`, or [`TimestampError::Overflow`] if the result
84    /// does not fit in a [`std::time::SystemTime`].
85    fn try_from(ts: Timestamp) -> Result<Self, Self::Error> {
86        if ts.nanos < 0 || ts.nanos > 999_999_999 {
87            return Err(TimestampError::InvalidNanos);
88        }
89
90        if ts.seconds >= 0 {
91            let offset = std::time::Duration::new(ts.seconds as u64, ts.nanos as u32);
92            std::time::UNIX_EPOCH
93                .checked_add(offset)
94                .ok_or(TimestampError::Overflow)
95        } else {
96            // ts.seconds is negative: move backward from epoch, then forward by nanos.
97            //
98            // For example, ts.seconds = -2, ts.nanos = 500_000_000 represents
99            // -1.5 seconds from epoch (i.e. 1.5 s before epoch):
100            //   result = UNIX_EPOCH - 2s + 0.5s = UNIX_EPOCH - 1.5s
101            //
102            // unsigned_abs() avoids the overflow that `(-ts.seconds) as u64` would
103            // cause when ts.seconds == i64::MIN (which cannot be negated in i64).
104            let neg_secs = ts.seconds.unsigned_abs();
105            let base = std::time::UNIX_EPOCH
106                .checked_sub(std::time::Duration::from_secs(neg_secs))
107                .ok_or(TimestampError::Overflow)?;
108            if ts.nanos == 0 {
109                Ok(base)
110            } else {
111                base.checked_add(std::time::Duration::from_nanos(ts.nanos as u64))
112                    .ok_or(TimestampError::Overflow)
113            }
114        }
115    }
116}
117
118#[cfg(feature = "std")]
119impl From<std::time::SystemTime> for Timestamp {
120    /// Convert a [`std::time::SystemTime`] to a protobuf [`Timestamp`].
121    ///
122    /// Pre-epoch times (where `t < UNIX_EPOCH`) are represented with a
123    /// negative `seconds` field and a non-negative `nanos` field, following
124    /// the protobuf convention that `nanos` is always in `[0, 999_999_999]`.
125    ///
126    /// # Saturation
127    ///
128    /// Times more than ~292 billion years from the epoch (beyond `i64::MAX`
129    /// seconds) are saturated to `i64::MAX` seconds rather than wrapping,
130    /// which would produce a semantically incorrect negative timestamp.
131    fn from(t: std::time::SystemTime) -> Self {
132        match t.duration_since(std::time::UNIX_EPOCH) {
133            Ok(d) => Self {
134                // Saturate at i64::MAX to avoid wrapping for times far in the future.
135                seconds: d.as_secs().min(i64::MAX as u64) as i64,
136                nanos: d.subsec_nanos() as i32,
137                ..Default::default()
138            },
139            Err(e) => {
140                // `e.duration()` is how far `t` is *before* the epoch.
141                // We need: seconds = floor(t - epoch), nanos = (t - epoch) - seconds.
142                //
143                // Example: t is 1.5s before epoch → duration = 1.5s
144                //   floor = -2 (the largest integer ≤ -1.5)
145                //   nanos = -1.5 - (-2) = 0.5s = 500_000_000 ns
146                //
147                // In terms of the subtraction duration `dur = e.duration()`:
148                //   If dur.subsec_nanos() == 0:
149                //     seconds = -(dur.as_secs() as i64), nanos = 0
150                //   Else:
151                //     seconds = -(dur.as_secs() as i64 + 1)
152                //     nanos = 1_000_000_000 - dur.subsec_nanos()
153                //
154                // Saturate at i64::MAX to avoid wrapping for extreme pre-epoch times.
155                let dur = e.duration();
156                if dur.subsec_nanos() == 0 {
157                    let secs = dur.as_secs().min(i64::MAX as u64) as i64;
158                    Self {
159                        seconds: -secs,
160                        nanos: 0,
161                        ..Default::default()
162                    }
163                } else {
164                    // saturating_add avoids overflow when dur.as_secs() == u64::MAX,
165                    // then clamp to i64::MAX before converting.
166                    let neg_secs = dur.as_secs().saturating_add(1).min(i64::MAX as u64) as i64;
167                    Self {
168                        seconds: -neg_secs,
169                        nanos: (1_000_000_000u32 - dur.subsec_nanos()) as i32,
170                        ..Default::default()
171                    }
172                }
173            }
174        }
175    }
176}
177
178// ── RFC 3339 formatting ──────────────────────────────────────────────────────
179//
180// The shared formatting and parsing primitives live in
181// `buffa::json_helpers::wkt`. Both this typed serde impl and `buffa-descriptor`'s
182// reflective JSON codec call into the same code, so the two paths can't drift
183// on edge cases the conformance suite exercises. The functions below are thin
184// adapters that preserve the `Option`-returning private API the test suite
185// targets.
186
187#[cfg(feature = "json")]
188use buffa::json_helpers::wkt::{MAX_TIMESTAMP_SECS, MIN_TIMESTAMP_SECS};
189// The civil-calendar helpers are exercised directly by the test module.
190#[cfg(all(test, feature = "json"))]
191use buffa::json_helpers::wkt::{date_to_days, days_to_date};
192
193#[cfg(feature = "json")]
194fn timestamp_to_rfc3339(secs: i64, nanos: i32) -> alloc::string::String {
195    // The serde `Serialize` impl validates `seconds` and `nanos` bounds
196    // before calling this; `expect` documents the invariant.
197    buffa::json_helpers::wkt::fmt_timestamp(secs, nanos)
198        .expect("Timestamp validated before formatting")
199}
200
201#[cfg(feature = "json")]
202fn parse_rfc3339(s: &str) -> Option<(i64, i32)> {
203    buffa::json_helpers::wkt::parse_timestamp(s).ok()
204}
205
206// ── serde impls ──────────────────────────────────────────────────────────────
207
208#[cfg(feature = "json")]
209impl serde::Serialize for Timestamp {
210    /// Serializes as an RFC 3339 string (e.g. `"2021-01-01T00:00:00Z"`).
211    ///
212    /// # Errors
213    ///
214    /// Returns a serialization error if `nanos` is outside `[0, 999_999_999]`
215    /// or if `seconds` is outside the proto spec range (years 0001–9999).
216    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
217        use alloc::format;
218        if !(0..=999_999_999).contains(&self.nanos) {
219            return Err(serde::ser::Error::custom(format!(
220                "invalid Timestamp: nanos {} is outside [0, 999_999_999]",
221                self.nanos
222            )));
223        }
224        if !(MIN_TIMESTAMP_SECS..=MAX_TIMESTAMP_SECS).contains(&self.seconds) {
225            return Err(serde::ser::Error::custom(format!(
226                "invalid Timestamp: seconds {} is outside [{}, {}]",
227                self.seconds, MIN_TIMESTAMP_SECS, MAX_TIMESTAMP_SECS
228            )));
229        }
230        s.serialize_str(&timestamp_to_rfc3339(self.seconds, self.nanos))
231    }
232}
233
234#[cfg(feature = "json")]
235impl<'de> serde::Deserialize<'de> for Timestamp {
236    /// Deserializes from an RFC 3339 string.
237    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
238        use alloc::{format, string::String};
239        let s: String = serde::Deserialize::deserialize(d)?;
240        let (secs, nanos) = parse_rfc3339(&s)
241            .ok_or_else(|| serde::de::Error::custom(format!("invalid RFC 3339 timestamp: {s}")))?;
242        Ok(Self {
243            seconds: secs,
244            nanos,
245            ..Default::default()
246        })
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253
254    #[test]
255    fn from_unix_secs_sets_nanos_to_zero() {
256        let ts = Timestamp::from_unix_secs(1_700_000_000);
257        assert_eq!(ts.seconds, 1_700_000_000);
258        assert_eq!(ts.nanos, 0);
259    }
260
261    #[test]
262    fn from_unix_secs_zero() {
263        let ts = Timestamp::from_unix_secs(0);
264        assert_eq!(ts.seconds, 0);
265        assert_eq!(ts.nanos, 0);
266    }
267
268    #[test]
269    fn from_unix_secs_negative() {
270        let ts = Timestamp::from_unix_secs(-1);
271        assert_eq!(ts.seconds, -1);
272        assert_eq!(ts.nanos, 0);
273    }
274
275    #[test]
276    fn from_unix_secs_i64_min() {
277        let ts = Timestamp::from_unix_secs(i64::MIN);
278        assert_eq!(ts.seconds, i64::MIN);
279        assert_eq!(ts.nanos, 0);
280    }
281
282    #[test]
283    fn from_unix_secs_i64_max() {
284        let ts = Timestamp::from_unix_secs(i64::MAX);
285        assert_eq!(ts.seconds, i64::MAX);
286        assert_eq!(ts.nanos, 0);
287    }
288
289    #[test]
290    fn from_unix_basic() {
291        let ts = Timestamp::from_unix(1_000_000_000, 500_000_000);
292        assert_eq!(ts.seconds, 1_000_000_000);
293        assert_eq!(ts.nanos, 500_000_000);
294    }
295
296    #[test]
297    fn from_unix_zero() {
298        let ts = Timestamp::from_unix(0, 0);
299        assert_eq!(ts.seconds, 0);
300        assert_eq!(ts.nanos, 0);
301    }
302
303    #[test]
304    fn from_unix_checked_valid() {
305        assert!(Timestamp::from_unix_checked(0, 0).is_some());
306        assert!(Timestamp::from_unix_checked(-100, 999_999_999).is_some());
307    }
308
309    #[test]
310    fn from_unix_checked_invalid_nanos() {
311        assert!(Timestamp::from_unix_checked(0, -1).is_none());
312        assert!(Timestamp::from_unix_checked(0, 1_000_000_000).is_none());
313    }
314
315    #[cfg(feature = "std")]
316    #[test]
317    fn systemtime_roundtrip_post_epoch() {
318        let ts = Timestamp::from_unix(1_700_000_000, 123_456_789);
319        let st: std::time::SystemTime = ts.clone().try_into().unwrap();
320        let ts2: Timestamp = st.into();
321        assert_eq!(ts, ts2);
322    }
323
324    #[cfg(feature = "std")]
325    #[test]
326    fn systemtime_roundtrip_pre_epoch() {
327        // -1.5 seconds before epoch: seconds = -2, nanos = 500_000_000
328        let ts = Timestamp::from_unix(-2, 500_000_000);
329        let st: std::time::SystemTime = ts.clone().try_into().unwrap();
330        let ts2: Timestamp = st.into();
331        assert_eq!(ts, ts2);
332    }
333
334    #[cfg(feature = "std")]
335    #[test]
336    fn systemtime_roundtrip_exact_pre_epoch() {
337        // Exactly 2 seconds before epoch.
338        let ts = Timestamp::from_unix(-2, 0);
339        let st: std::time::SystemTime = ts.clone().try_into().unwrap();
340        let ts2: Timestamp = st.into();
341        assert_eq!(ts, ts2);
342    }
343
344    #[cfg(feature = "std")]
345    #[test]
346    fn systemtime_roundtrip_epoch() {
347        let ts = Timestamp::from_unix(0, 0);
348        let st: std::time::SystemTime = ts.clone().try_into().unwrap();
349        let ts2: Timestamp = st.into();
350        assert_eq!(ts, ts2);
351    }
352
353    #[cfg(feature = "std")]
354    #[test]
355    fn invalid_nanos_rejected() {
356        let ts = Timestamp {
357            seconds: 0,
358            nanos: -1,
359            ..Default::default()
360        };
361        let result: Result<std::time::SystemTime, _> = ts.try_into();
362        assert_eq!(result, Err(TimestampError::InvalidNanos));
363
364        let ts2 = Timestamp {
365            seconds: 0,
366            nanos: 1_000_000_000,
367            ..Default::default()
368        };
369        let result2: Result<std::time::SystemTime, _> = ts2.try_into();
370        assert_eq!(result2, Err(TimestampError::InvalidNanos));
371    }
372
373    #[cfg(feature = "std")]
374    #[test]
375    fn i64_min_seconds_does_not_panic() {
376        // i64::MIN cannot be negated in i64; unsigned_abs() must be used.
377        let ts = Timestamp {
378            seconds: i64::MIN,
379            nanos: 0,
380            ..Default::default()
381        };
382        // The conversion should either succeed or return Overflow, never panic.
383        let _: Result<std::time::SystemTime, _> = ts.try_into();
384    }
385
386    #[cfg(feature = "std")]
387    #[test]
388    fn now_is_positive() {
389        let ts = Timestamp::now();
390        assert!(ts.seconds > 0, "current time should be after Unix epoch");
391    }
392
393    #[test]
394    fn timestamp_view_round_trip() {
395        use crate::google::protobuf::__buffa::view::TimestampView;
396        use crate::google::protobuf::Timestamp;
397        use buffa::{Message, MessageView};
398
399        let ts = Timestamp {
400            seconds: 1_700_000_000,
401            nanos: 123_456_789,
402            ..Default::default()
403        };
404        let bytes = ts.encode_to_vec();
405        let view = TimestampView::decode_view(&bytes).expect("decode_view");
406        assert_eq!(view.seconds, ts.seconds);
407        assert_eq!(view.nanos, ts.nanos);
408
409        let owned = view.to_owned_message();
410        assert_eq!(owned, ts);
411    }
412
413    #[cfg(feature = "json")]
414    mod serde_tests {
415        use super::*;
416
417        // ---- RFC 3339 helper unit tests -----------------------------------
418
419        #[test]
420        fn days_to_date_epoch() {
421            assert_eq!(days_to_date(0), (1970, 1, 1));
422        }
423
424        #[test]
425        fn days_to_date_known_date() {
426            // 2021-01-01: days since epoch = 18628
427            assert_eq!(days_to_date(18628), (2021, 1, 1));
428        }
429
430        #[test]
431        fn date_to_days_roundtrip() {
432            let (y, m, d) = days_to_date(18628);
433            assert_eq!(date_to_days(y, m, d), Some(18628));
434        }
435
436        #[test]
437        fn date_to_days_invalid_month() {
438            assert_eq!(date_to_days(2021, 13, 1), None);
439            assert_eq!(date_to_days(2021, 0, 1), None);
440        }
441
442        #[test]
443        fn rfc3339_epoch() {
444            assert_eq!(timestamp_to_rfc3339(0, 0), "1970-01-01T00:00:00Z");
445        }
446
447        #[test]
448        fn rfc3339_half_second() {
449            assert_eq!(
450                timestamp_to_rfc3339(0, 500_000_000),
451                "1970-01-01T00:00:00.500Z"
452            );
453        }
454
455        #[test]
456        fn rfc3339_one_nanosecond() {
457            assert_eq!(timestamp_to_rfc3339(0, 1), "1970-01-01T00:00:00.000000001Z");
458        }
459
460        #[test]
461        fn parse_epoch() {
462            assert_eq!(parse_rfc3339("1970-01-01T00:00:00Z"), Some((0, 0)));
463        }
464
465        #[test]
466        fn parse_with_fractional_seconds() {
467            assert_eq!(
468                parse_rfc3339("1970-01-01T00:00:00.5Z"),
469                Some((0, 500_000_000))
470            );
471        }
472
473        #[test]
474        fn parse_with_positive_offset() {
475            // +05:00 means local is 5h ahead, so UTC = local - 5h
476            assert_eq!(parse_rfc3339("1970-01-01T05:00:00+05:00"), Some((0, 0)));
477        }
478
479        #[test]
480        fn parse_invalid() {
481            assert_eq!(parse_rfc3339("not-a-date"), None);
482            assert_eq!(parse_rfc3339("1970-01-01T00:00:00"), None); // missing tz
483        }
484
485        // ---- serde roundtrips ---------------------------------------------
486
487        #[test]
488        fn timestamp_epoch_roundtrip() {
489            let ts = Timestamp::from_unix(0, 0);
490            let json = serde_json::to_string(&ts).unwrap();
491            assert_eq!(json, r#""1970-01-01T00:00:00Z""#);
492            let back: Timestamp = serde_json::from_str(&json).unwrap();
493            assert_eq!(back.seconds, 0);
494            assert_eq!(back.nanos, 0);
495        }
496
497        #[test]
498        fn timestamp_with_nanos_roundtrip() {
499            let ts = Timestamp::from_unix(1_000_000_000, 500_000_000);
500            let json = serde_json::to_string(&ts).unwrap();
501            let back: Timestamp = serde_json::from_str(&json).unwrap();
502            assert_eq!(back.seconds, ts.seconds);
503            assert_eq!(back.nanos, ts.nanos);
504        }
505
506        #[test]
507        fn timestamp_pre_epoch_roundtrip() {
508            // -1.5 seconds before epoch: seconds = -2, nanos = 500_000_000
509            let ts = Timestamp::from_unix(-2, 500_000_000);
510            let json = serde_json::to_string(&ts).unwrap();
511            let back: Timestamp = serde_json::from_str(&json).unwrap();
512            assert_eq!(back.seconds, ts.seconds);
513            assert_eq!(back.nanos, ts.nanos);
514        }
515
516        #[test]
517        fn timestamp_invalid_string_is_error() {
518            let result: Result<Timestamp, _> = serde_json::from_str(r#""not-a-date""#);
519            assert!(result.is_err());
520        }
521
522        #[test]
523        fn timestamp_invalid_nanos_is_serialize_error() {
524            let ts = Timestamp {
525                seconds: 0,
526                nanos: -1,
527                ..Default::default()
528            };
529            let result = serde_json::to_string(&ts);
530            assert!(result.is_err(), "negative nanos must fail serialization");
531        }
532
533        #[test]
534        fn parse_lowercase_separators_rejected() {
535            // Proto3 JSON spec requires uppercase 'T' and 'Z'.
536            assert_eq!(parse_rfc3339("1970-01-01T00:00:00z"), None);
537            assert_eq!(parse_rfc3339("1970-01-01t00:00:00Z"), None);
538            assert_eq!(parse_rfc3339("1970-01-01t00:00:00z"), None);
539        }
540
541        #[test]
542        fn parse_date_to_days_rejects_feb_30() {
543            // "Feb 30" is not a real date; parse_rfc3339 must return None.
544            assert_eq!(parse_rfc3339("2021-02-30T00:00:00Z"), None);
545        }
546
547        #[test]
548        fn parse_time_component_range_rejected() {
549            // Hour, minute, second must be in valid ranges.
550            assert_eq!(parse_rfc3339("2021-01-01T24:00:00Z"), None, "hour 24");
551            assert_eq!(parse_rfc3339("2021-01-01T25:00:00Z"), None, "hour 25");
552            assert_eq!(parse_rfc3339("2021-01-01T00:60:00Z"), None, "min 60");
553            assert_eq!(parse_rfc3339("2021-01-01T00:99:00Z"), None, "min 99");
554            assert_eq!(parse_rfc3339("2021-01-01T00:00:60Z"), None, "sec 60 (leap)");
555            assert_eq!(parse_rfc3339("2021-01-01T00:00:99Z"), None, "sec 99");
556            // Valid boundaries.
557            assert!(parse_rfc3339("2021-01-01T23:59:59Z").is_some());
558            assert!(parse_rfc3339("2021-01-01T00:00:00Z").is_some());
559        }
560
561        #[test]
562        fn parse_offset_range_rejected() {
563            assert_eq!(parse_rfc3339("2021-01-01T00:00:00+24:00"), None, "oh 24");
564            assert_eq!(parse_rfc3339("2021-01-01T00:00:00+99:00"), None, "oh 99");
565            assert_eq!(parse_rfc3339("2021-01-01T00:00:00+00:60"), None, "om 60");
566            assert_eq!(parse_rfc3339("2021-01-01T00:00:00+99:99"), None, "both");
567            // Valid boundaries.
568            assert!(parse_rfc3339("2021-01-01T00:00:00+23:59").is_some());
569            assert!(parse_rfc3339("2021-01-01T00:00:00-23:59").is_some());
570        }
571
572        #[test]
573        fn parse_separator_chars_rejected() {
574            // Hyphens in date, colons in time, colon in offset are required.
575            assert_eq!(parse_rfc3339("2021X01-01T00:00:00Z"), None, "date[4]");
576            assert_eq!(parse_rfc3339("2021-01X01T00:00:00Z"), None, "date[7]");
577            assert_eq!(parse_rfc3339("2021-01-01T00X00:00Z"), None, "time[2]");
578            assert_eq!(parse_rfc3339("2021-01-01T00:00X00Z"), None, "time[5]");
579            assert_eq!(parse_rfc3339("2021-01-01T00:00:00+05X30"), None, "off");
580            // All separators wrong at once.
581            assert_eq!(parse_rfc3339("2021X01X01T00X00X00Z"), None);
582        }
583
584        #[test]
585        fn parse_fractional_seconds_rejects_non_digits() {
586            // Regression (fuzzer-found): i32::parse accepts '-' and '+',
587            // which previously allowed "T23:59:59.-3Z" → nanos = -30_000_000.
588            assert_eq!(parse_rfc3339("1970-01-01T00:00:00.-3Z"), None, "minus");
589            assert_eq!(parse_rfc3339("1970-01-01T00:00:00.+3Z"), None, "plus");
590            assert_eq!(parse_rfc3339("1970-01-01T00:00:00.3aZ"), None, "alpha");
591            assert_eq!(parse_rfc3339("1970-01-01T00:00:00. Z"), None, "space");
592            // Edge: 9999-12-31T23:59:59.-3Z — the fuzzer's original crash input.
593            assert_eq!(parse_rfc3339("9999-12-31T23:59:59.-3Z"), None);
594            // Valid digits still work.
595            assert_eq!(
596                parse_rfc3339("1970-01-01T00:00:00.5Z"),
597                Some((0, 500_000_000))
598            );
599            assert_eq!(
600                parse_rfc3339("1970-01-01T00:00:00.000000001Z"),
601                Some((0, 1))
602            );
603        }
604
605        #[test]
606        fn parse_offset_pushes_past_boundary_rejected() {
607            // Year is 9999 (passes pre-offset check), but -23:59 offset means
608            // UTC is in year 10000 — must be rejected per proto Timestamp range.
609            assert_eq!(parse_rfc3339("9999-12-31T23:59:59-23:59"), None);
610            // Year is 0001 (passes), but +23:59 offset means UTC is in year 0.
611            assert_eq!(parse_rfc3339("0001-01-01T00:00:00+23:59"), None);
612            // Boundary values that just fit are OK.
613            assert_eq!(
614                parse_rfc3339("9999-12-31T23:59:59Z"),
615                Some((MAX_TIMESTAMP_SECS, 0))
616            );
617            assert_eq!(
618                parse_rfc3339("0001-01-01T00:00:00Z"),
619                Some((MIN_TIMESTAMP_SECS, 0))
620            );
621        }
622    }
623}