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 Self {
34 seconds,
35 nanos,
36 ..Default::default()
37 }
38 }
39
40 pub fn from_unix_secs(seconds: i64) -> Self {
44 Self {
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(Self {
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) => Self {
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 Self {
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 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#[cfg(feature = "json")]
188use buffa::json_helpers::wkt::{MAX_TIMESTAMP_SECS, MIN_TIMESTAMP_SECS};
189#[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 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#[cfg(feature = "json")]
209impl serde::Serialize for Timestamp {
210 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(×tamp_to_rfc3339(self.seconds, self.nanos))
231 }
232}
233
234#[cfg(feature = "json")]
235impl<'de> serde::Deserialize<'de> for Timestamp {
236 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 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 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 let ts = Timestamp {
378 seconds: i64::MIN,
379 nanos: 0,
380 ..Default::default()
381 };
382 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 #[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 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 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); }
484
485 #[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 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 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 assert_eq!(parse_rfc3339("2021-02-30T00:00:00Z"), None);
545 }
546
547 #[test]
548 fn parse_time_component_range_rejected() {
549 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 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 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 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 assert_eq!(parse_rfc3339("2021X01X01T00X00X00Z"), None);
582 }
583
584 #[test]
585 fn parse_fractional_seconds_rejects_non_digits() {
586 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 assert_eq!(parse_rfc3339("9999-12-31T23:59:59.-3Z"), None);
594 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 assert_eq!(parse_rfc3339("9999-12-31T23:59:59-23:59"), None);
610 assert_eq!(parse_rfc3339("0001-01-01T00:00:00+23:59"), None);
612 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}