1use crate::google::protobuf::Timestamp;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
7pub enum TimestampError {
8 #[error("nanos field must be in [0, 999_999_999]")]
10 InvalidNanos,
11 #[error("timestamp is out of range for SystemTime")]
13 Overflow,
14}
15
16impl Timestamp {
17 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 Timestamp {
34 seconds,
35 nanos,
36 ..Default::default()
37 }
38 }
39
40 pub fn from_unix_secs(seconds: i64) -> Self {
44 Timestamp {
45 seconds,
46 nanos: 0,
47 ..Default::default()
48 }
49 }
50
51 pub fn from_unix_checked(seconds: i64, nanos: i32) -> Option<Self> {
54 if (0..=999_999_999).contains(&nanos) {
55 Some(Timestamp {
56 seconds,
57 nanos,
58 ..Default::default()
59 })
60 } else {
61 None
62 }
63 }
64
65 #[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 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 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 fn from(t: std::time::SystemTime) -> Self {
132 match t.duration_since(std::time::UNIX_EPOCH) {
133 Ok(d) => Timestamp {
134 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 let dur = e.duration();
156 if dur.subsec_nanos() == 0 {
157 let secs = dur.as_secs().min(i64::MAX as u64) as i64;
158 Timestamp {
159 seconds: -secs,
160 nanos: 0,
161 ..Default::default()
162 }
163 } else {
164 let neg_secs = dur.as_secs().saturating_add(1).min(i64::MAX as u64) as i64;
167 Timestamp {
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#[cfg(feature = "json")]
183fn days_to_date(days: i64) -> (i64, u8, u8) {
184 let z = days + 719468;
185 let era = (if z >= 0 { z } else { z - 146096 }) / 146097;
186 let doe = z - era * 146097;
187 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
188 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
189 let mp = (5 * doy + 2) / 153;
190 let d = doy - (153 * mp + 2) / 5 + 1;
191 let m = if mp < 10 { mp + 3 } else { mp - 9 };
192 (
193 yoe + era * 400 + if m <= 2 { 1 } else { 0 },
194 m as u8,
195 d as u8,
196 )
197}
198
199#[cfg(feature = "json")]
206fn date_to_days(y: i64, m: u8, d: u8) -> Option<i64> {
207 if !(1..=12).contains(&m) || !(1..=31).contains(&d) {
208 return None;
209 }
210 let (ya, ma) = if m <= 2 { (y - 1, m + 9) } else { (y, m - 3) };
211 let era = (if ya >= 0 { ya } else { ya - 399 }) / 400;
212 let yoe = ya - era * 400;
213 let doy = (153 * ma as i64 + 2) / 5 + d as i64 - 1;
214 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
215 let days = era * 146097 + doe - 719468;
216 if days_to_date(days) != (y, m, d) {
219 return None;
220 }
221 Some(days)
222}
223
224#[cfg(feature = "json")]
227fn timestamp_to_rfc3339(secs: i64, nanos: i32) -> alloc::string::String {
228 use alloc::format;
229 use alloc::string::String;
230 let (tod, day) = {
231 let r = secs % 86400;
232 if r >= 0 {
233 (r, secs / 86400)
234 } else {
235 (r + 86400, secs / 86400 - 1)
236 }
237 };
238 let (y, mo, d) = days_to_date(day);
239 let h = tod / 3600;
240 let mi = (tod % 3600) / 60;
241 let s = tod % 60;
242 let frac = if nanos == 0 {
243 String::new()
244 } else if nanos % 1_000_000 == 0 {
245 format!(".{:03}", nanos / 1_000_000)
246 } else if nanos % 1_000 == 0 {
247 format!(".{:06}", nanos / 1_000)
248 } else {
249 format!(".{:09}", nanos)
250 };
251 format!("{y:04}-{mo:02}-{d:02}T{h:02}:{mi:02}:{s:02}{frac}Z")
252}
253
254#[cfg(feature = "json")]
258fn parse_rfc3339(s: &str) -> Option<(i64, i32)> {
259 if !s.is_ascii() {
262 return None;
263 }
264 let (dt, tz_offset) = if let Some(rest) = s.strip_suffix('Z') {
266 (rest, 0i64)
267 } else {
268 let len = s.len();
269 if len < 6 {
270 return None;
271 }
272 let sign: i64 = match s.as_bytes()[len - 6] {
273 b'+' => -1,
274 b'-' => 1,
275 _ => return None,
276 };
277 if s.as_bytes()[len - 3] != b':' {
279 return None;
280 }
281 let oh: i64 = s[len - 5..len - 3].parse().ok()?;
282 let om: i64 = s[len - 2..].parse().ok()?;
283 if !(0..=23).contains(&oh) || !(0..=59).contains(&om) {
284 return None;
285 }
286 (&s[..len - 6], sign * (oh * 3600 + om * 60))
287 };
288
289 let t = dt.find('T')?;
291 let (date, time) = (&dt[..t], &dt[t + 1..]);
292 if date.len() != 10 || time.len() < 8 {
293 return None;
294 }
295
296 let date_b = date.as_bytes();
298 let time_b = time.as_bytes();
299 if date_b[4] != b'-' || date_b[7] != b'-' || time_b[2] != b':' || time_b[5] != b':' {
300 return None;
301 }
302
303 let year: i64 = date[0..4].parse().ok()?;
304 let month: u8 = date[5..7].parse().ok()?;
305 let day: u8 = date[8..10].parse().ok()?;
306 let hour: i64 = time[0..2].parse().ok()?;
307 let min: i64 = time[3..5].parse().ok()?;
308 let sec: i64 = time[6..8].parse().ok()?;
309 if !(0..=23).contains(&hour) || !(0..=59).contains(&min) || !(0..=59).contains(&sec) {
313 return None;
314 }
315
316 let nanos = if time.len() > 8 {
317 if time.as_bytes()[8] != b'.' {
318 return None;
319 }
320 let frac = &time[9..];
321 if frac.is_empty() || frac.len() > 9 || !frac.bytes().all(|b| b.is_ascii_digit()) {
324 return None;
325 }
326 let n: i32 = frac.parse().ok()?;
327 n * 10_i32.pow(9 - frac.len() as u32)
328 } else {
329 0
330 };
331
332 if !(1..=9999).contains(&year) {
335 return None;
336 }
337 let days = date_to_days(year, month, day)?;
338 let unix = days * 86400 + hour * 3600 + min * 60 + sec + tz_offset;
339 if !(MIN_TIMESTAMP_SECS..=MAX_TIMESTAMP_SECS).contains(&unix) {
343 return None;
344 }
345 Some((unix, nanos))
346}
347
348#[cfg(feature = "json")]
352const MIN_TIMESTAMP_SECS: i64 = -62_135_596_800; #[cfg(feature = "json")]
354const MAX_TIMESTAMP_SECS: i64 = 253_402_300_799; #[cfg(feature = "json")]
357impl serde::Serialize for Timestamp {
358 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
365 use alloc::format;
366 if !(0..=999_999_999).contains(&self.nanos) {
367 return Err(serde::ser::Error::custom(format!(
368 "invalid Timestamp: nanos {} is outside [0, 999_999_999]",
369 self.nanos
370 )));
371 }
372 if !(MIN_TIMESTAMP_SECS..=MAX_TIMESTAMP_SECS).contains(&self.seconds) {
373 return Err(serde::ser::Error::custom(format!(
374 "invalid Timestamp: seconds {} is outside [{}, {}]",
375 self.seconds, MIN_TIMESTAMP_SECS, MAX_TIMESTAMP_SECS
376 )));
377 }
378 s.serialize_str(×tamp_to_rfc3339(self.seconds, self.nanos))
379 }
380}
381
382#[cfg(feature = "json")]
383impl<'de> serde::Deserialize<'de> for Timestamp {
384 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
386 use alloc::{format, string::String};
387 let s: String = serde::Deserialize::deserialize(d)?;
388 let (secs, nanos) = parse_rfc3339(&s)
389 .ok_or_else(|| serde::de::Error::custom(format!("invalid RFC 3339 timestamp: {s}")))?;
390 Ok(Timestamp {
391 seconds: secs,
392 nanos,
393 ..Default::default()
394 })
395 }
396}
397
398#[cfg(test)]
399mod tests {
400 use super::*;
401
402 #[test]
403 fn from_unix_secs_sets_nanos_to_zero() {
404 let ts = Timestamp::from_unix_secs(1_700_000_000);
405 assert_eq!(ts.seconds, 1_700_000_000);
406 assert_eq!(ts.nanos, 0);
407 }
408
409 #[test]
410 fn from_unix_secs_zero() {
411 let ts = Timestamp::from_unix_secs(0);
412 assert_eq!(ts.seconds, 0);
413 assert_eq!(ts.nanos, 0);
414 }
415
416 #[test]
417 fn from_unix_secs_negative() {
418 let ts = Timestamp::from_unix_secs(-1);
419 assert_eq!(ts.seconds, -1);
420 assert_eq!(ts.nanos, 0);
421 }
422
423 #[test]
424 fn from_unix_secs_i64_min() {
425 let ts = Timestamp::from_unix_secs(i64::MIN);
426 assert_eq!(ts.seconds, i64::MIN);
427 assert_eq!(ts.nanos, 0);
428 }
429
430 #[test]
431 fn from_unix_secs_i64_max() {
432 let ts = Timestamp::from_unix_secs(i64::MAX);
433 assert_eq!(ts.seconds, i64::MAX);
434 assert_eq!(ts.nanos, 0);
435 }
436
437 #[test]
438 fn from_unix_basic() {
439 let ts = Timestamp::from_unix(1_000_000_000, 500_000_000);
440 assert_eq!(ts.seconds, 1_000_000_000);
441 assert_eq!(ts.nanos, 500_000_000);
442 }
443
444 #[test]
445 fn from_unix_zero() {
446 let ts = Timestamp::from_unix(0, 0);
447 assert_eq!(ts.seconds, 0);
448 assert_eq!(ts.nanos, 0);
449 }
450
451 #[test]
452 fn from_unix_checked_valid() {
453 assert!(Timestamp::from_unix_checked(0, 0).is_some());
454 assert!(Timestamp::from_unix_checked(-100, 999_999_999).is_some());
455 }
456
457 #[test]
458 fn from_unix_checked_invalid_nanos() {
459 assert!(Timestamp::from_unix_checked(0, -1).is_none());
460 assert!(Timestamp::from_unix_checked(0, 1_000_000_000).is_none());
461 }
462
463 #[cfg(feature = "std")]
464 #[test]
465 fn systemtime_roundtrip_post_epoch() {
466 let ts = Timestamp::from_unix(1_700_000_000, 123_456_789);
467 let st: std::time::SystemTime = ts.clone().try_into().unwrap();
468 let ts2: Timestamp = st.into();
469 assert_eq!(ts, ts2);
470 }
471
472 #[cfg(feature = "std")]
473 #[test]
474 fn systemtime_roundtrip_pre_epoch() {
475 let ts = Timestamp::from_unix(-2, 500_000_000);
477 let st: std::time::SystemTime = ts.clone().try_into().unwrap();
478 let ts2: Timestamp = st.into();
479 assert_eq!(ts, ts2);
480 }
481
482 #[cfg(feature = "std")]
483 #[test]
484 fn systemtime_roundtrip_exact_pre_epoch() {
485 let ts = Timestamp::from_unix(-2, 0);
487 let st: std::time::SystemTime = ts.clone().try_into().unwrap();
488 let ts2: Timestamp = st.into();
489 assert_eq!(ts, ts2);
490 }
491
492 #[cfg(feature = "std")]
493 #[test]
494 fn systemtime_roundtrip_epoch() {
495 let ts = Timestamp::from_unix(0, 0);
496 let st: std::time::SystemTime = ts.clone().try_into().unwrap();
497 let ts2: Timestamp = st.into();
498 assert_eq!(ts, ts2);
499 }
500
501 #[cfg(feature = "std")]
502 #[test]
503 fn invalid_nanos_rejected() {
504 let ts = Timestamp {
505 seconds: 0,
506 nanos: -1,
507 ..Default::default()
508 };
509 let result: Result<std::time::SystemTime, _> = ts.try_into();
510 assert_eq!(result, Err(TimestampError::InvalidNanos));
511
512 let ts2 = Timestamp {
513 seconds: 0,
514 nanos: 1_000_000_000,
515 ..Default::default()
516 };
517 let result2: Result<std::time::SystemTime, _> = ts2.try_into();
518 assert_eq!(result2, Err(TimestampError::InvalidNanos));
519 }
520
521 #[cfg(feature = "std")]
522 #[test]
523 fn i64_min_seconds_does_not_panic() {
524 let ts = Timestamp {
526 seconds: i64::MIN,
527 nanos: 0,
528 ..Default::default()
529 };
530 let _: Result<std::time::SystemTime, _> = ts.try_into();
532 }
533
534 #[cfg(feature = "std")]
535 #[test]
536 fn now_is_positive() {
537 let ts = Timestamp::now();
538 assert!(ts.seconds > 0, "current time should be after Unix epoch");
539 }
540
541 #[test]
542 fn timestamp_view_round_trip() {
543 use crate::google::protobuf::{Timestamp, TimestampView};
544 use buffa::{Message, MessageView};
545
546 let ts = Timestamp {
547 seconds: 1_700_000_000,
548 nanos: 123_456_789,
549 ..Default::default()
550 };
551 let bytes = ts.encode_to_vec();
552 let view = TimestampView::decode_view(&bytes).expect("decode_view");
553 assert_eq!(view.seconds, ts.seconds);
554 assert_eq!(view.nanos, ts.nanos);
555
556 let owned = view.to_owned_message();
557 assert_eq!(owned, ts);
558 }
559
560 #[cfg(feature = "json")]
561 mod serde_tests {
562 use super::*;
563
564 #[test]
567 fn days_to_date_epoch() {
568 assert_eq!(days_to_date(0), (1970, 1, 1));
569 }
570
571 #[test]
572 fn days_to_date_known_date() {
573 assert_eq!(days_to_date(18628), (2021, 1, 1));
575 }
576
577 #[test]
578 fn date_to_days_roundtrip() {
579 let (y, m, d) = days_to_date(18628);
580 assert_eq!(date_to_days(y, m, d), Some(18628));
581 }
582
583 #[test]
584 fn date_to_days_invalid_month() {
585 assert_eq!(date_to_days(2021, 13, 1), None);
586 assert_eq!(date_to_days(2021, 0, 1), None);
587 }
588
589 #[test]
590 fn rfc3339_epoch() {
591 assert_eq!(timestamp_to_rfc3339(0, 0), "1970-01-01T00:00:00Z");
592 }
593
594 #[test]
595 fn rfc3339_half_second() {
596 assert_eq!(
597 timestamp_to_rfc3339(0, 500_000_000),
598 "1970-01-01T00:00:00.500Z"
599 );
600 }
601
602 #[test]
603 fn rfc3339_one_nanosecond() {
604 assert_eq!(timestamp_to_rfc3339(0, 1), "1970-01-01T00:00:00.000000001Z");
605 }
606
607 #[test]
608 fn parse_epoch() {
609 assert_eq!(parse_rfc3339("1970-01-01T00:00:00Z"), Some((0, 0)));
610 }
611
612 #[test]
613 fn parse_with_fractional_seconds() {
614 assert_eq!(
615 parse_rfc3339("1970-01-01T00:00:00.5Z"),
616 Some((0, 500_000_000))
617 );
618 }
619
620 #[test]
621 fn parse_with_positive_offset() {
622 assert_eq!(parse_rfc3339("1970-01-01T05:00:00+05:00"), Some((0, 0)));
624 }
625
626 #[test]
627 fn parse_invalid() {
628 assert_eq!(parse_rfc3339("not-a-date"), None);
629 assert_eq!(parse_rfc3339("1970-01-01T00:00:00"), None); }
631
632 #[test]
635 fn timestamp_epoch_roundtrip() {
636 let ts = Timestamp::from_unix(0, 0);
637 let json = serde_json::to_string(&ts).unwrap();
638 assert_eq!(json, r#""1970-01-01T00:00:00Z""#);
639 let back: Timestamp = serde_json::from_str(&json).unwrap();
640 assert_eq!(back.seconds, 0);
641 assert_eq!(back.nanos, 0);
642 }
643
644 #[test]
645 fn timestamp_with_nanos_roundtrip() {
646 let ts = Timestamp::from_unix(1_000_000_000, 500_000_000);
647 let json = serde_json::to_string(&ts).unwrap();
648 let back: Timestamp = serde_json::from_str(&json).unwrap();
649 assert_eq!(back.seconds, ts.seconds);
650 assert_eq!(back.nanos, ts.nanos);
651 }
652
653 #[test]
654 fn timestamp_pre_epoch_roundtrip() {
655 let ts = Timestamp::from_unix(-2, 500_000_000);
657 let json = serde_json::to_string(&ts).unwrap();
658 let back: Timestamp = serde_json::from_str(&json).unwrap();
659 assert_eq!(back.seconds, ts.seconds);
660 assert_eq!(back.nanos, ts.nanos);
661 }
662
663 #[test]
664 fn timestamp_invalid_string_is_error() {
665 let result: Result<Timestamp, _> = serde_json::from_str(r#""not-a-date""#);
666 assert!(result.is_err());
667 }
668
669 #[test]
670 fn timestamp_invalid_nanos_is_serialize_error() {
671 let ts = Timestamp {
672 seconds: 0,
673 nanos: -1,
674 ..Default::default()
675 };
676 let result = serde_json::to_string(&ts);
677 assert!(result.is_err(), "negative nanos must fail serialization");
678 }
679
680 #[test]
681 fn parse_lowercase_separators_rejected() {
682 assert_eq!(parse_rfc3339("1970-01-01T00:00:00z"), None);
684 assert_eq!(parse_rfc3339("1970-01-01t00:00:00Z"), None);
685 assert_eq!(parse_rfc3339("1970-01-01t00:00:00z"), None);
686 }
687
688 #[test]
689 fn parse_date_to_days_rejects_feb_30() {
690 assert_eq!(parse_rfc3339("2021-02-30T00:00:00Z"), None);
692 }
693
694 #[test]
695 fn parse_time_component_range_rejected() {
696 assert_eq!(parse_rfc3339("2021-01-01T24:00:00Z"), None, "hour 24");
698 assert_eq!(parse_rfc3339("2021-01-01T25:00:00Z"), None, "hour 25");
699 assert_eq!(parse_rfc3339("2021-01-01T00:60:00Z"), None, "min 60");
700 assert_eq!(parse_rfc3339("2021-01-01T00:99:00Z"), None, "min 99");
701 assert_eq!(parse_rfc3339("2021-01-01T00:00:60Z"), None, "sec 60 (leap)");
702 assert_eq!(parse_rfc3339("2021-01-01T00:00:99Z"), None, "sec 99");
703 assert!(parse_rfc3339("2021-01-01T23:59:59Z").is_some());
705 assert!(parse_rfc3339("2021-01-01T00:00:00Z").is_some());
706 }
707
708 #[test]
709 fn parse_offset_range_rejected() {
710 assert_eq!(parse_rfc3339("2021-01-01T00:00:00+24:00"), None, "oh 24");
711 assert_eq!(parse_rfc3339("2021-01-01T00:00:00+99:00"), None, "oh 99");
712 assert_eq!(parse_rfc3339("2021-01-01T00:00:00+00:60"), None, "om 60");
713 assert_eq!(parse_rfc3339("2021-01-01T00:00:00+99:99"), None, "both");
714 assert!(parse_rfc3339("2021-01-01T00:00:00+23:59").is_some());
716 assert!(parse_rfc3339("2021-01-01T00:00:00-23:59").is_some());
717 }
718
719 #[test]
720 fn parse_separator_chars_rejected() {
721 assert_eq!(parse_rfc3339("2021X01-01T00:00:00Z"), None, "date[4]");
723 assert_eq!(parse_rfc3339("2021-01X01T00:00:00Z"), None, "date[7]");
724 assert_eq!(parse_rfc3339("2021-01-01T00X00:00Z"), None, "time[2]");
725 assert_eq!(parse_rfc3339("2021-01-01T00:00X00Z"), None, "time[5]");
726 assert_eq!(parse_rfc3339("2021-01-01T00:00:00+05X30"), None, "off");
727 assert_eq!(parse_rfc3339("2021X01X01T00X00X00Z"), None);
729 }
730
731 #[test]
732 fn parse_fractional_seconds_rejects_non_digits() {
733 assert_eq!(parse_rfc3339("1970-01-01T00:00:00.-3Z"), None, "minus");
736 assert_eq!(parse_rfc3339("1970-01-01T00:00:00.+3Z"), None, "plus");
737 assert_eq!(parse_rfc3339("1970-01-01T00:00:00.3aZ"), None, "alpha");
738 assert_eq!(parse_rfc3339("1970-01-01T00:00:00. Z"), None, "space");
739 assert_eq!(parse_rfc3339("9999-12-31T23:59:59.-3Z"), None);
741 assert_eq!(
743 parse_rfc3339("1970-01-01T00:00:00.5Z"),
744 Some((0, 500_000_000))
745 );
746 assert_eq!(
747 parse_rfc3339("1970-01-01T00:00:00.000000001Z"),
748 Some((0, 1))
749 );
750 }
751
752 #[test]
753 fn parse_offset_pushes_past_boundary_rejected() {
754 assert_eq!(parse_rfc3339("9999-12-31T23:59:59-23:59"), None);
757 assert_eq!(parse_rfc3339("0001-01-01T00:00:00+23:59"), None);
759 assert_eq!(
761 parse_rfc3339("9999-12-31T23:59:59Z"),
762 Some((MAX_TIMESTAMP_SECS, 0))
763 );
764 assert_eq!(
765 parse_rfc3339("0001-01-01T00:00:00Z"),
766 Some((MIN_TIMESTAMP_SECS, 0))
767 );
768 }
769 }
770}