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