1use crate::google::protobuf::Timestamp;
4
5pub(crate) const NANOS_MAX: i32 = 999_999_999;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
21pub enum TimestampError {
22 #[error("nanos field must be in [0, 999_999_999]")]
24 InvalidNanos,
25 #[error("timestamp is out of range for the target type")]
27 Overflow,
28}
29
30impl Timestamp {
31 pub fn from_unix(seconds: i64, nanos: i32) -> Self {
43 debug_assert!(
44 (0..=NANOS_MAX).contains(&nanos),
45 "nanos ({nanos}) must be in [0, 999_999_999]"
46 );
47 Self {
48 seconds,
49 nanos,
50 ..Default::default()
51 }
52 }
53
54 pub fn from_unix_secs(seconds: i64) -> Self {
58 Self {
59 seconds,
60 nanos: 0,
61 ..Default::default()
62 }
63 }
64
65 pub fn from_unix_checked(seconds: i64, nanos: i32) -> Option<Self> {
68 if (0..=NANOS_MAX).contains(&nanos) {
69 Some(Self {
70 seconds,
71 nanos,
72 ..Default::default()
73 })
74 } else {
75 None
76 }
77 }
78
79 #[cfg(feature = "std")]
83 pub fn now() -> Self {
84 std::time::SystemTime::now().into()
85 }
86}
87
88#[cfg(feature = "std")]
89impl TryFrom<Timestamp> for std::time::SystemTime {
90 type Error = TimestampError;
91
92 fn try_from(ts: Timestamp) -> Result<Self, Self::Error> {
100 if ts.nanos < 0 || ts.nanos > NANOS_MAX {
101 return Err(TimestampError::InvalidNanos);
102 }
103
104 if ts.seconds >= 0 {
105 let offset = std::time::Duration::new(ts.seconds as u64, ts.nanos as u32);
106 std::time::UNIX_EPOCH
107 .checked_add(offset)
108 .ok_or(TimestampError::Overflow)
109 } else {
110 let neg_secs = ts.seconds.unsigned_abs();
119 let base = std::time::UNIX_EPOCH
120 .checked_sub(std::time::Duration::from_secs(neg_secs))
121 .ok_or(TimestampError::Overflow)?;
122 if ts.nanos == 0 {
123 Ok(base)
124 } else {
125 base.checked_add(std::time::Duration::from_nanos(ts.nanos as u64))
126 .ok_or(TimestampError::Overflow)
127 }
128 }
129 }
130}
131
132#[cfg(feature = "std")]
133impl From<std::time::SystemTime> for Timestamp {
134 fn from(t: std::time::SystemTime) -> Self {
146 match t.duration_since(std::time::UNIX_EPOCH) {
147 Ok(d) => Self {
148 seconds: d.as_secs().min(i64::MAX as u64) as i64,
150 nanos: d.subsec_nanos() as i32,
151 ..Default::default()
152 },
153 Err(e) => {
154 let dur = e.duration();
170 if dur.subsec_nanos() == 0 {
171 let secs = dur.as_secs().min(i64::MAX as u64) as i64;
172 Self {
173 seconds: -secs,
174 nanos: 0,
175 ..Default::default()
176 }
177 } else {
178 let neg_secs = dur.as_secs().saturating_add(1).min(i64::MAX as u64) as i64;
181 Self {
182 seconds: -neg_secs,
183 nanos: (1_000_000_000u32 - dur.subsec_nanos()) as i32,
184 ..Default::default()
185 }
186 }
187 }
188 }
189 }
190}
191
192#[cfg(feature = "json")]
202use buffa::json_helpers::wkt::{MAX_TIMESTAMP_SECS, MIN_TIMESTAMP_SECS};
203#[cfg(all(test, feature = "json"))]
205use buffa::json_helpers::wkt::{date_to_days, days_to_date};
206
207#[cfg(feature = "json")]
208fn timestamp_to_rfc3339(secs: i64, nanos: i32) -> alloc::string::String {
209 buffa::json_helpers::wkt::fmt_timestamp(secs, nanos)
212 .expect("Timestamp validated before formatting")
213}
214
215#[cfg(feature = "json")]
216fn parse_rfc3339(s: &str) -> Option<(i64, i32)> {
217 buffa::json_helpers::wkt::parse_timestamp(s).ok()
218}
219
220#[cfg(feature = "json")]
223impl serde::Serialize for Timestamp {
224 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
231 use alloc::format;
232 if !(0..=NANOS_MAX).contains(&self.nanos) {
233 return Err(serde::ser::Error::custom(format!(
234 "invalid Timestamp: nanos {} is outside [0, {NANOS_MAX}]",
235 self.nanos
236 )));
237 }
238 if !(MIN_TIMESTAMP_SECS..=MAX_TIMESTAMP_SECS).contains(&self.seconds) {
239 return Err(serde::ser::Error::custom(format!(
240 "invalid Timestamp: seconds {} is outside [{}, {}]",
241 self.seconds, MIN_TIMESTAMP_SECS, MAX_TIMESTAMP_SECS
242 )));
243 }
244 s.serialize_str(×tamp_to_rfc3339(self.seconds, self.nanos))
245 }
246}
247
248#[cfg(feature = "json")]
249impl<'de> serde::Deserialize<'de> for Timestamp {
250 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
252 use alloc::{format, string::String};
253 let s: String = serde::Deserialize::deserialize(d)?;
254 let (secs, nanos) = parse_rfc3339(&s)
255 .ok_or_else(|| serde::de::Error::custom(format!("invalid RFC 3339 timestamp: {s}")))?;
256 Ok(Self {
257 seconds: secs,
258 nanos,
259 ..Default::default()
260 })
261 }
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267
268 #[test]
269 fn from_unix_secs_sets_nanos_to_zero() {
270 let ts = Timestamp::from_unix_secs(1_700_000_000);
271 assert_eq!(ts.seconds, 1_700_000_000);
272 assert_eq!(ts.nanos, 0);
273 }
274
275 #[test]
276 fn from_unix_secs_zero() {
277 let ts = Timestamp::from_unix_secs(0);
278 assert_eq!(ts.seconds, 0);
279 assert_eq!(ts.nanos, 0);
280 }
281
282 #[test]
283 fn from_unix_secs_negative() {
284 let ts = Timestamp::from_unix_secs(-1);
285 assert_eq!(ts.seconds, -1);
286 assert_eq!(ts.nanos, 0);
287 }
288
289 #[test]
290 fn from_unix_secs_i64_min() {
291 let ts = Timestamp::from_unix_secs(i64::MIN);
292 assert_eq!(ts.seconds, i64::MIN);
293 assert_eq!(ts.nanos, 0);
294 }
295
296 #[test]
297 fn from_unix_secs_i64_max() {
298 let ts = Timestamp::from_unix_secs(i64::MAX);
299 assert_eq!(ts.seconds, i64::MAX);
300 assert_eq!(ts.nanos, 0);
301 }
302
303 #[test]
304 fn from_unix_basic() {
305 let ts = Timestamp::from_unix(1_000_000_000, 500_000_000);
306 assert_eq!(ts.seconds, 1_000_000_000);
307 assert_eq!(ts.nanos, 500_000_000);
308 }
309
310 #[test]
311 fn from_unix_zero() {
312 let ts = Timestamp::from_unix(0, 0);
313 assert_eq!(ts.seconds, 0);
314 assert_eq!(ts.nanos, 0);
315 }
316
317 #[test]
318 fn from_unix_checked_valid() {
319 assert!(Timestamp::from_unix_checked(0, 0).is_some());
320 assert!(Timestamp::from_unix_checked(-100, 999_999_999).is_some());
321 }
322
323 #[test]
324 fn from_unix_checked_invalid_nanos() {
325 assert!(Timestamp::from_unix_checked(0, -1).is_none());
326 assert!(Timestamp::from_unix_checked(0, 1_000_000_000).is_none());
327 }
328
329 #[cfg(feature = "std")]
330 #[test]
331 fn systemtime_roundtrip_post_epoch() {
332 let ts = Timestamp::from_unix(1_700_000_000, 123_456_789);
333 let st: std::time::SystemTime = ts.clone().try_into().unwrap();
334 let ts2: Timestamp = st.into();
335 assert_eq!(ts, ts2);
336 }
337
338 #[cfg(feature = "std")]
339 #[test]
340 fn systemtime_roundtrip_pre_epoch() {
341 let ts = Timestamp::from_unix(-2, 500_000_000);
343 let st: std::time::SystemTime = ts.clone().try_into().unwrap();
344 let ts2: Timestamp = st.into();
345 assert_eq!(ts, ts2);
346 }
347
348 #[cfg(feature = "std")]
349 #[test]
350 fn systemtime_roundtrip_exact_pre_epoch() {
351 let ts = Timestamp::from_unix(-2, 0);
353 let st: std::time::SystemTime = ts.clone().try_into().unwrap();
354 let ts2: Timestamp = st.into();
355 assert_eq!(ts, ts2);
356 }
357
358 #[cfg(feature = "std")]
359 #[test]
360 fn systemtime_roundtrip_epoch() {
361 let ts = Timestamp::from_unix(0, 0);
362 let st: std::time::SystemTime = ts.clone().try_into().unwrap();
363 let ts2: Timestamp = st.into();
364 assert_eq!(ts, ts2);
365 }
366
367 #[cfg(feature = "std")]
368 #[test]
369 fn invalid_nanos_rejected() {
370 let ts = Timestamp {
371 seconds: 0,
372 nanos: -1,
373 ..Default::default()
374 };
375 let result: Result<std::time::SystemTime, _> = ts.try_into();
376 assert_eq!(result, Err(TimestampError::InvalidNanos));
377
378 let ts2 = Timestamp {
379 seconds: 0,
380 nanos: 1_000_000_000,
381 ..Default::default()
382 };
383 let result2: Result<std::time::SystemTime, _> = ts2.try_into();
384 assert_eq!(result2, Err(TimestampError::InvalidNanos));
385 }
386
387 #[cfg(feature = "std")]
388 #[test]
389 fn i64_min_seconds_does_not_panic() {
390 let ts = Timestamp {
392 seconds: i64::MIN,
393 nanos: 0,
394 ..Default::default()
395 };
396 let _: Result<std::time::SystemTime, _> = ts.try_into();
398 }
399
400 #[cfg(feature = "std")]
401 #[test]
402 fn now_is_positive() {
403 let ts = Timestamp::now();
404 assert!(ts.seconds > 0, "current time should be after Unix epoch");
405 }
406
407 #[test]
408 fn timestamp_view_round_trip() {
409 use crate::google::protobuf::__buffa::view::TimestampView;
410 use crate::google::protobuf::Timestamp;
411 use buffa::{Message, MessageView};
412
413 let ts = Timestamp {
414 seconds: 1_700_000_000,
415 nanos: 123_456_789,
416 ..Default::default()
417 };
418 let bytes = ts.encode_to_vec();
419 let view = TimestampView::decode_view(&bytes).expect("decode_view");
420 assert_eq!(view.seconds, ts.seconds);
421 assert_eq!(view.nanos, ts.nanos);
422
423 let owned = view.to_owned_message().unwrap();
424 assert_eq!(owned, ts);
425 }
426
427 #[cfg(feature = "json")]
428 mod serde_tests {
429 use super::*;
430
431 #[test]
434 fn days_to_date_epoch() {
435 assert_eq!(days_to_date(0), (1970, 1, 1));
436 }
437
438 #[test]
439 fn days_to_date_known_date() {
440 assert_eq!(days_to_date(18628), (2021, 1, 1));
442 }
443
444 #[test]
445 fn date_to_days_roundtrip() {
446 let (y, m, d) = days_to_date(18628);
447 assert_eq!(date_to_days(y, m, d), Some(18628));
448 }
449
450 #[test]
451 fn date_to_days_invalid_month() {
452 assert_eq!(date_to_days(2021, 13, 1), None);
453 assert_eq!(date_to_days(2021, 0, 1), None);
454 }
455
456 #[test]
457 fn rfc3339_epoch() {
458 assert_eq!(timestamp_to_rfc3339(0, 0), "1970-01-01T00:00:00Z");
459 }
460
461 #[test]
462 fn rfc3339_half_second() {
463 assert_eq!(
464 timestamp_to_rfc3339(0, 500_000_000),
465 "1970-01-01T00:00:00.500Z"
466 );
467 }
468
469 #[test]
470 fn rfc3339_one_nanosecond() {
471 assert_eq!(timestamp_to_rfc3339(0, 1), "1970-01-01T00:00:00.000000001Z");
472 }
473
474 #[test]
475 fn parse_epoch() {
476 assert_eq!(parse_rfc3339("1970-01-01T00:00:00Z"), Some((0, 0)));
477 }
478
479 #[test]
480 fn parse_with_fractional_seconds() {
481 assert_eq!(
482 parse_rfc3339("1970-01-01T00:00:00.5Z"),
483 Some((0, 500_000_000))
484 );
485 }
486
487 #[test]
488 fn parse_with_positive_offset() {
489 assert_eq!(parse_rfc3339("1970-01-01T05:00:00+05:00"), Some((0, 0)));
491 }
492
493 #[test]
494 fn parse_invalid() {
495 assert_eq!(parse_rfc3339("not-a-date"), None);
496 assert_eq!(parse_rfc3339("1970-01-01T00:00:00"), None); }
498
499 #[test]
502 fn timestamp_epoch_roundtrip() {
503 let ts = Timestamp::from_unix(0, 0);
504 let json = serde_json::to_string(&ts).unwrap();
505 assert_eq!(json, r#""1970-01-01T00:00:00Z""#);
506 let back: Timestamp = serde_json::from_str(&json).unwrap();
507 assert_eq!(back.seconds, 0);
508 assert_eq!(back.nanos, 0);
509 }
510
511 #[test]
512 fn timestamp_with_nanos_roundtrip() {
513 let ts = Timestamp::from_unix(1_000_000_000, 500_000_000);
514 let json = serde_json::to_string(&ts).unwrap();
515 let back: Timestamp = serde_json::from_str(&json).unwrap();
516 assert_eq!(back.seconds, ts.seconds);
517 assert_eq!(back.nanos, ts.nanos);
518 }
519
520 #[test]
521 fn timestamp_pre_epoch_roundtrip() {
522 let ts = Timestamp::from_unix(-2, 500_000_000);
524 let json = serde_json::to_string(&ts).unwrap();
525 let back: Timestamp = serde_json::from_str(&json).unwrap();
526 assert_eq!(back.seconds, ts.seconds);
527 assert_eq!(back.nanos, ts.nanos);
528 }
529
530 #[test]
531 fn timestamp_invalid_string_is_error() {
532 let result: Result<Timestamp, _> = serde_json::from_str(r#""not-a-date""#);
533 assert!(result.is_err());
534 }
535
536 #[test]
537 fn timestamp_invalid_nanos_is_serialize_error() {
538 let ts = Timestamp {
539 seconds: 0,
540 nanos: -1,
541 ..Default::default()
542 };
543 let result = serde_json::to_string(&ts);
544 assert!(result.is_err(), "negative nanos must fail serialization");
545 }
546
547 #[test]
548 fn parse_lowercase_separators_rejected() {
549 assert_eq!(parse_rfc3339("1970-01-01T00:00:00z"), None);
551 assert_eq!(parse_rfc3339("1970-01-01t00:00:00Z"), None);
552 assert_eq!(parse_rfc3339("1970-01-01t00:00:00z"), None);
553 }
554
555 #[test]
556 fn parse_date_to_days_rejects_feb_30() {
557 assert_eq!(parse_rfc3339("2021-02-30T00:00:00Z"), None);
559 }
560
561 #[test]
562 fn parse_time_component_range_rejected() {
563 assert_eq!(parse_rfc3339("2021-01-01T24:00:00Z"), None, "hour 24");
565 assert_eq!(parse_rfc3339("2021-01-01T25:00:00Z"), None, "hour 25");
566 assert_eq!(parse_rfc3339("2021-01-01T00:60:00Z"), None, "min 60");
567 assert_eq!(parse_rfc3339("2021-01-01T00:99:00Z"), None, "min 99");
568 assert_eq!(parse_rfc3339("2021-01-01T00:00:60Z"), None, "sec 60 (leap)");
569 assert_eq!(parse_rfc3339("2021-01-01T00:00:99Z"), None, "sec 99");
570 assert!(parse_rfc3339("2021-01-01T23:59:59Z").is_some());
572 assert!(parse_rfc3339("2021-01-01T00:00:00Z").is_some());
573 }
574
575 #[test]
576 fn parse_offset_range_rejected() {
577 assert_eq!(parse_rfc3339("2021-01-01T00:00:00+24:00"), None, "oh 24");
578 assert_eq!(parse_rfc3339("2021-01-01T00:00:00+99:00"), None, "oh 99");
579 assert_eq!(parse_rfc3339("2021-01-01T00:00:00+00:60"), None, "om 60");
580 assert_eq!(parse_rfc3339("2021-01-01T00:00:00+99:99"), None, "both");
581 assert!(parse_rfc3339("2021-01-01T00:00:00+23:59").is_some());
583 assert!(parse_rfc3339("2021-01-01T00:00:00-23:59").is_some());
584 }
585
586 #[test]
587 fn parse_separator_chars_rejected() {
588 assert_eq!(parse_rfc3339("2021X01-01T00:00:00Z"), None, "date[4]");
590 assert_eq!(parse_rfc3339("2021-01X01T00:00:00Z"), None, "date[7]");
591 assert_eq!(parse_rfc3339("2021-01-01T00X00:00Z"), None, "time[2]");
592 assert_eq!(parse_rfc3339("2021-01-01T00:00X00Z"), None, "time[5]");
593 assert_eq!(parse_rfc3339("2021-01-01T00:00:00+05X30"), None, "off");
594 assert_eq!(parse_rfc3339("2021X01X01T00X00X00Z"), None);
596 }
597
598 #[test]
599 fn parse_fractional_seconds_rejects_non_digits() {
600 assert_eq!(parse_rfc3339("1970-01-01T00:00:00.-3Z"), None, "minus");
603 assert_eq!(parse_rfc3339("1970-01-01T00:00:00.+3Z"), None, "plus");
604 assert_eq!(parse_rfc3339("1970-01-01T00:00:00.3aZ"), None, "alpha");
605 assert_eq!(parse_rfc3339("1970-01-01T00:00:00. Z"), None, "space");
606 assert_eq!(parse_rfc3339("9999-12-31T23:59:59.-3Z"), None);
608 assert_eq!(
610 parse_rfc3339("1970-01-01T00:00:00.5Z"),
611 Some((0, 500_000_000))
612 );
613 assert_eq!(
614 parse_rfc3339("1970-01-01T00:00:00.000000001Z"),
615 Some((0, 1))
616 );
617 }
618
619 #[test]
620 fn parse_offset_pushes_past_boundary_rejected() {
621 assert_eq!(parse_rfc3339("9999-12-31T23:59:59-23:59"), None);
624 assert_eq!(parse_rfc3339("0001-01-01T00:00:00+23:59"), None);
626 assert_eq!(
628 parse_rfc3339("9999-12-31T23:59:59Z"),
629 Some((MAX_TIMESTAMP_SECS, 0))
630 );
631 assert_eq!(
632 parse_rfc3339("0001-01-01T00:00:00Z"),
633 Some((MIN_TIMESTAMP_SECS, 0))
634 );
635 }
636 }
637}