use crate::google::protobuf::Timestamp;
pub(crate) const NANOS_MAX: i32 = 999_999_999;
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum TimestampError {
#[error("nanos field must be in [0, 999_999_999]")]
InvalidNanos,
#[error("timestamp is out of range for the target type")]
Overflow,
}
impl Timestamp {
pub fn from_unix(seconds: i64, nanos: i32) -> Self {
debug_assert!(
(0..=NANOS_MAX).contains(&nanos),
"nanos ({nanos}) must be in [0, 999_999_999]"
);
Self {
seconds,
nanos,
..Default::default()
}
}
pub fn from_unix_secs(seconds: i64) -> Self {
Self {
seconds,
nanos: 0,
..Default::default()
}
}
pub fn from_unix_checked(seconds: i64, nanos: i32) -> Option<Self> {
if (0..=NANOS_MAX).contains(&nanos) {
Some(Self {
seconds,
nanos,
..Default::default()
})
} else {
None
}
}
#[cfg(feature = "std")]
pub fn now() -> Self {
std::time::SystemTime::now().into()
}
}
#[cfg(feature = "std")]
impl TryFrom<Timestamp> for std::time::SystemTime {
type Error = TimestampError;
fn try_from(ts: Timestamp) -> Result<Self, Self::Error> {
if ts.nanos < 0 || ts.nanos > NANOS_MAX {
return Err(TimestampError::InvalidNanos);
}
if ts.seconds >= 0 {
let offset = std::time::Duration::new(ts.seconds as u64, ts.nanos as u32);
std::time::UNIX_EPOCH
.checked_add(offset)
.ok_or(TimestampError::Overflow)
} else {
let neg_secs = ts.seconds.unsigned_abs();
let base = std::time::UNIX_EPOCH
.checked_sub(std::time::Duration::from_secs(neg_secs))
.ok_or(TimestampError::Overflow)?;
if ts.nanos == 0 {
Ok(base)
} else {
base.checked_add(std::time::Duration::from_nanos(ts.nanos as u64))
.ok_or(TimestampError::Overflow)
}
}
}
}
#[cfg(feature = "std")]
impl From<std::time::SystemTime> for Timestamp {
fn from(t: std::time::SystemTime) -> Self {
match t.duration_since(std::time::UNIX_EPOCH) {
Ok(d) => Self {
seconds: d.as_secs().min(i64::MAX as u64) as i64,
nanos: d.subsec_nanos() as i32,
..Default::default()
},
Err(e) => {
let dur = e.duration();
if dur.subsec_nanos() == 0 {
let secs = dur.as_secs().min(i64::MAX as u64) as i64;
Self {
seconds: -secs,
nanos: 0,
..Default::default()
}
} else {
let neg_secs = dur.as_secs().saturating_add(1).min(i64::MAX as u64) as i64;
Self {
seconds: -neg_secs,
nanos: (1_000_000_000u32 - dur.subsec_nanos()) as i32,
..Default::default()
}
}
}
}
}
}
#[cfg(feature = "json")]
use buffa::json_helpers::wkt::{MAX_TIMESTAMP_SECS, MIN_TIMESTAMP_SECS};
#[cfg(all(test, feature = "json"))]
use buffa::json_helpers::wkt::{date_to_days, days_to_date};
#[cfg(feature = "json")]
fn timestamp_to_rfc3339(secs: i64, nanos: i32) -> alloc::string::String {
buffa::json_helpers::wkt::fmt_timestamp(secs, nanos)
.expect("Timestamp validated before formatting")
}
#[cfg(feature = "json")]
fn parse_rfc3339(s: &str) -> Option<(i64, i32)> {
buffa::json_helpers::wkt::parse_timestamp(s).ok()
}
#[cfg(feature = "json")]
impl serde::Serialize for Timestamp {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
use alloc::format;
if !(0..=NANOS_MAX).contains(&self.nanos) {
return Err(serde::ser::Error::custom(format!(
"invalid Timestamp: nanos {} is outside [0, {NANOS_MAX}]",
self.nanos
)));
}
if !(MIN_TIMESTAMP_SECS..=MAX_TIMESTAMP_SECS).contains(&self.seconds) {
return Err(serde::ser::Error::custom(format!(
"invalid Timestamp: seconds {} is outside [{}, {}]",
self.seconds, MIN_TIMESTAMP_SECS, MAX_TIMESTAMP_SECS
)));
}
s.serialize_str(×tamp_to_rfc3339(self.seconds, self.nanos))
}
}
#[cfg(feature = "json")]
impl<'de> serde::Deserialize<'de> for Timestamp {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
use alloc::{format, string::String};
let s: String = serde::Deserialize::deserialize(d)?;
let (secs, nanos) = parse_rfc3339(&s)
.ok_or_else(|| serde::de::Error::custom(format!("invalid RFC 3339 timestamp: {s}")))?;
Ok(Self {
seconds: secs,
nanos,
..Default::default()
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_unix_secs_sets_nanos_to_zero() {
let ts = Timestamp::from_unix_secs(1_700_000_000);
assert_eq!(ts.seconds, 1_700_000_000);
assert_eq!(ts.nanos, 0);
}
#[test]
fn from_unix_secs_zero() {
let ts = Timestamp::from_unix_secs(0);
assert_eq!(ts.seconds, 0);
assert_eq!(ts.nanos, 0);
}
#[test]
fn from_unix_secs_negative() {
let ts = Timestamp::from_unix_secs(-1);
assert_eq!(ts.seconds, -1);
assert_eq!(ts.nanos, 0);
}
#[test]
fn from_unix_secs_i64_min() {
let ts = Timestamp::from_unix_secs(i64::MIN);
assert_eq!(ts.seconds, i64::MIN);
assert_eq!(ts.nanos, 0);
}
#[test]
fn from_unix_secs_i64_max() {
let ts = Timestamp::from_unix_secs(i64::MAX);
assert_eq!(ts.seconds, i64::MAX);
assert_eq!(ts.nanos, 0);
}
#[test]
fn from_unix_basic() {
let ts = Timestamp::from_unix(1_000_000_000, 500_000_000);
assert_eq!(ts.seconds, 1_000_000_000);
assert_eq!(ts.nanos, 500_000_000);
}
#[test]
fn from_unix_zero() {
let ts = Timestamp::from_unix(0, 0);
assert_eq!(ts.seconds, 0);
assert_eq!(ts.nanos, 0);
}
#[test]
fn from_unix_checked_valid() {
assert!(Timestamp::from_unix_checked(0, 0).is_some());
assert!(Timestamp::from_unix_checked(-100, 999_999_999).is_some());
}
#[test]
fn from_unix_checked_invalid_nanos() {
assert!(Timestamp::from_unix_checked(0, -1).is_none());
assert!(Timestamp::from_unix_checked(0, 1_000_000_000).is_none());
}
#[cfg(feature = "std")]
#[test]
fn systemtime_roundtrip_post_epoch() {
let ts = Timestamp::from_unix(1_700_000_000, 123_456_789);
let st: std::time::SystemTime = ts.clone().try_into().unwrap();
let ts2: Timestamp = st.into();
assert_eq!(ts, ts2);
}
#[cfg(feature = "std")]
#[test]
fn systemtime_roundtrip_pre_epoch() {
let ts = Timestamp::from_unix(-2, 500_000_000);
let st: std::time::SystemTime = ts.clone().try_into().unwrap();
let ts2: Timestamp = st.into();
assert_eq!(ts, ts2);
}
#[cfg(feature = "std")]
#[test]
fn systemtime_roundtrip_exact_pre_epoch() {
let ts = Timestamp::from_unix(-2, 0);
let st: std::time::SystemTime = ts.clone().try_into().unwrap();
let ts2: Timestamp = st.into();
assert_eq!(ts, ts2);
}
#[cfg(feature = "std")]
#[test]
fn systemtime_roundtrip_epoch() {
let ts = Timestamp::from_unix(0, 0);
let st: std::time::SystemTime = ts.clone().try_into().unwrap();
let ts2: Timestamp = st.into();
assert_eq!(ts, ts2);
}
#[cfg(feature = "std")]
#[test]
fn invalid_nanos_rejected() {
let ts = Timestamp {
seconds: 0,
nanos: -1,
..Default::default()
};
let result: Result<std::time::SystemTime, _> = ts.try_into();
assert_eq!(result, Err(TimestampError::InvalidNanos));
let ts2 = Timestamp {
seconds: 0,
nanos: 1_000_000_000,
..Default::default()
};
let result2: Result<std::time::SystemTime, _> = ts2.try_into();
assert_eq!(result2, Err(TimestampError::InvalidNanos));
}
#[cfg(feature = "std")]
#[test]
fn i64_min_seconds_does_not_panic() {
let ts = Timestamp {
seconds: i64::MIN,
nanos: 0,
..Default::default()
};
let _: Result<std::time::SystemTime, _> = ts.try_into();
}
#[cfg(feature = "std")]
#[test]
fn now_is_positive() {
let ts = Timestamp::now();
assert!(ts.seconds > 0, "current time should be after Unix epoch");
}
#[test]
fn timestamp_view_round_trip() {
use crate::google::protobuf::__buffa::view::TimestampView;
use crate::google::protobuf::Timestamp;
use buffa::{Message, MessageView};
let ts = Timestamp {
seconds: 1_700_000_000,
nanos: 123_456_789,
..Default::default()
};
let bytes = ts.encode_to_vec();
let view = TimestampView::decode_view(&bytes).expect("decode_view");
assert_eq!(view.seconds, ts.seconds);
assert_eq!(view.nanos, ts.nanos);
let owned = view.to_owned_message().unwrap();
assert_eq!(owned, ts);
}
#[cfg(feature = "json")]
mod serde_tests {
use super::*;
#[test]
fn days_to_date_epoch() {
assert_eq!(days_to_date(0), (1970, 1, 1));
}
#[test]
fn days_to_date_known_date() {
assert_eq!(days_to_date(18628), (2021, 1, 1));
}
#[test]
fn date_to_days_roundtrip() {
let (y, m, d) = days_to_date(18628);
assert_eq!(date_to_days(y, m, d), Some(18628));
}
#[test]
fn date_to_days_invalid_month() {
assert_eq!(date_to_days(2021, 13, 1), None);
assert_eq!(date_to_days(2021, 0, 1), None);
}
#[test]
fn rfc3339_epoch() {
assert_eq!(timestamp_to_rfc3339(0, 0), "1970-01-01T00:00:00Z");
}
#[test]
fn rfc3339_half_second() {
assert_eq!(
timestamp_to_rfc3339(0, 500_000_000),
"1970-01-01T00:00:00.500Z"
);
}
#[test]
fn rfc3339_one_nanosecond() {
assert_eq!(timestamp_to_rfc3339(0, 1), "1970-01-01T00:00:00.000000001Z");
}
#[test]
fn parse_epoch() {
assert_eq!(parse_rfc3339("1970-01-01T00:00:00Z"), Some((0, 0)));
}
#[test]
fn parse_with_fractional_seconds() {
assert_eq!(
parse_rfc3339("1970-01-01T00:00:00.5Z"),
Some((0, 500_000_000))
);
}
#[test]
fn parse_with_positive_offset() {
assert_eq!(parse_rfc3339("1970-01-01T05:00:00+05:00"), Some((0, 0)));
}
#[test]
fn parse_invalid() {
assert_eq!(parse_rfc3339("not-a-date"), None);
assert_eq!(parse_rfc3339("1970-01-01T00:00:00"), None); }
#[test]
fn timestamp_epoch_roundtrip() {
let ts = Timestamp::from_unix(0, 0);
let json = serde_json::to_string(&ts).unwrap();
assert_eq!(json, r#""1970-01-01T00:00:00Z""#);
let back: Timestamp = serde_json::from_str(&json).unwrap();
assert_eq!(back.seconds, 0);
assert_eq!(back.nanos, 0);
}
#[test]
fn timestamp_with_nanos_roundtrip() {
let ts = Timestamp::from_unix(1_000_000_000, 500_000_000);
let json = serde_json::to_string(&ts).unwrap();
let back: Timestamp = serde_json::from_str(&json).unwrap();
assert_eq!(back.seconds, ts.seconds);
assert_eq!(back.nanos, ts.nanos);
}
#[test]
fn timestamp_pre_epoch_roundtrip() {
let ts = Timestamp::from_unix(-2, 500_000_000);
let json = serde_json::to_string(&ts).unwrap();
let back: Timestamp = serde_json::from_str(&json).unwrap();
assert_eq!(back.seconds, ts.seconds);
assert_eq!(back.nanos, ts.nanos);
}
#[test]
fn timestamp_invalid_string_is_error() {
let result: Result<Timestamp, _> = serde_json::from_str(r#""not-a-date""#);
assert!(result.is_err());
}
#[test]
fn timestamp_invalid_nanos_is_serialize_error() {
let ts = Timestamp {
seconds: 0,
nanos: -1,
..Default::default()
};
let result = serde_json::to_string(&ts);
assert!(result.is_err(), "negative nanos must fail serialization");
}
#[test]
fn parse_lowercase_separators_rejected() {
assert_eq!(parse_rfc3339("1970-01-01T00:00:00z"), None);
assert_eq!(parse_rfc3339("1970-01-01t00:00:00Z"), None);
assert_eq!(parse_rfc3339("1970-01-01t00:00:00z"), None);
}
#[test]
fn parse_date_to_days_rejects_feb_30() {
assert_eq!(parse_rfc3339("2021-02-30T00:00:00Z"), None);
}
#[test]
fn parse_time_component_range_rejected() {
assert_eq!(parse_rfc3339("2021-01-01T24:00:00Z"), None, "hour 24");
assert_eq!(parse_rfc3339("2021-01-01T25:00:00Z"), None, "hour 25");
assert_eq!(parse_rfc3339("2021-01-01T00:60:00Z"), None, "min 60");
assert_eq!(parse_rfc3339("2021-01-01T00:99:00Z"), None, "min 99");
assert_eq!(parse_rfc3339("2021-01-01T00:00:60Z"), None, "sec 60 (leap)");
assert_eq!(parse_rfc3339("2021-01-01T00:00:99Z"), None, "sec 99");
assert!(parse_rfc3339("2021-01-01T23:59:59Z").is_some());
assert!(parse_rfc3339("2021-01-01T00:00:00Z").is_some());
}
#[test]
fn parse_offset_range_rejected() {
assert_eq!(parse_rfc3339("2021-01-01T00:00:00+24:00"), None, "oh 24");
assert_eq!(parse_rfc3339("2021-01-01T00:00:00+99:00"), None, "oh 99");
assert_eq!(parse_rfc3339("2021-01-01T00:00:00+00:60"), None, "om 60");
assert_eq!(parse_rfc3339("2021-01-01T00:00:00+99:99"), None, "both");
assert!(parse_rfc3339("2021-01-01T00:00:00+23:59").is_some());
assert!(parse_rfc3339("2021-01-01T00:00:00-23:59").is_some());
}
#[test]
fn parse_separator_chars_rejected() {
assert_eq!(parse_rfc3339("2021X01-01T00:00:00Z"), None, "date[4]");
assert_eq!(parse_rfc3339("2021-01X01T00:00:00Z"), None, "date[7]");
assert_eq!(parse_rfc3339("2021-01-01T00X00:00Z"), None, "time[2]");
assert_eq!(parse_rfc3339("2021-01-01T00:00X00Z"), None, "time[5]");
assert_eq!(parse_rfc3339("2021-01-01T00:00:00+05X30"), None, "off");
assert_eq!(parse_rfc3339("2021X01X01T00X00X00Z"), None);
}
#[test]
fn parse_fractional_seconds_rejects_non_digits() {
assert_eq!(parse_rfc3339("1970-01-01T00:00:00.-3Z"), None, "minus");
assert_eq!(parse_rfc3339("1970-01-01T00:00:00.+3Z"), None, "plus");
assert_eq!(parse_rfc3339("1970-01-01T00:00:00.3aZ"), None, "alpha");
assert_eq!(parse_rfc3339("1970-01-01T00:00:00. Z"), None, "space");
assert_eq!(parse_rfc3339("9999-12-31T23:59:59.-3Z"), None);
assert_eq!(
parse_rfc3339("1970-01-01T00:00:00.5Z"),
Some((0, 500_000_000))
);
assert_eq!(
parse_rfc3339("1970-01-01T00:00:00.000000001Z"),
Some((0, 1))
);
}
#[test]
fn parse_offset_pushes_past_boundary_rejected() {
assert_eq!(parse_rfc3339("9999-12-31T23:59:59-23:59"), None);
assert_eq!(parse_rfc3339("0001-01-01T00:00:00+23:59"), None);
assert_eq!(
parse_rfc3339("9999-12-31T23:59:59Z"),
Some((MAX_TIMESTAMP_SECS, 0))
);
assert_eq!(
parse_rfc3339("0001-01-01T00:00:00Z"),
Some((MIN_TIMESTAMP_SECS, 0))
);
}
}
}