nectar_primitives/
timestamp.rs1use derive_more::{Display, From, Into};
8use std::time::Duration;
9
10#[cfg(feature = "serde")]
11use serde::{Deserialize, Serialize};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Display, From, Into)]
15#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
16#[cfg_attr(feature = "serde", serde(transparent))]
17#[display("{_0}")]
18pub struct Timestamp(i64);
19
20#[non_exhaustive]
22#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
23pub enum TimestampError {
24 #[error("timestamp drifted by {drift_seconds}s (window ±{window_seconds}s)")]
26 OutsideSkewWindow {
27 drift_seconds: i64,
29 window_seconds: i64,
31 },
32}
33
34impl Timestamp {
35 pub const ZERO: Self = Self(0);
37
38 #[inline]
40 pub const fn from_seconds(s: i64) -> Self {
41 Self(s)
42 }
43
44 #[inline]
46 pub const fn get(self) -> i64 {
47 self.0
48 }
49
50 #[inline]
52 pub const fn to_be_bytes(self) -> [u8; 8] {
53 self.0.to_be_bytes()
54 }
55
56 pub fn now() -> Self {
66 use web_time::{SystemTime, UNIX_EPOCH};
67 let secs = SystemTime::now()
68 .duration_since(UNIX_EPOCH)
69 .expect("system clock set before unix epoch")
70 .as_secs();
71 Self(i64::try_from(secs).expect("system clock exceeds i64 unix seconds"))
73 }
74
75 pub fn skew_check(self, local: Self, window: Duration) -> Result<(), TimestampError> {
80 let drift = self.0.saturating_sub(local.0);
81 let window_secs = i64::try_from(window.as_secs()).unwrap_or(i64::MAX);
82 if drift.unsigned_abs() <= window_secs.unsigned_abs() {
83 Ok(())
84 } else {
85 Err(TimestampError::OutsideSkewWindow {
86 drift_seconds: drift,
87 window_seconds: window_secs,
88 })
89 }
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96
97 #[test]
98 fn skew_check_within_window() {
99 let local = Timestamp::from_seconds(1_000_000);
100 let remote = Timestamp::from_seconds(1_000_030); assert!(remote.skew_check(local, Duration::from_secs(60)).is_ok());
102 }
103
104 #[test]
105 fn skew_check_negative_within_window() {
106 let local = Timestamp::from_seconds(1_000_000);
107 let remote = Timestamp::from_seconds(999_940); assert!(remote.skew_check(local, Duration::from_secs(60)).is_ok());
109 }
110
111 #[test]
112 fn skew_check_outside_window() {
113 let local = Timestamp::from_seconds(1_000_000);
114 let remote = Timestamp::from_seconds(1_000_120); let err = remote
116 .skew_check(local, Duration::from_secs(60))
117 .unwrap_err();
118 assert!(matches!(
119 err,
120 TimestampError::OutsideSkewWindow {
121 drift_seconds: 120,
122 window_seconds: 60
123 }
124 ));
125 }
126
127 #[test]
128 fn be_bytes_signed() {
129 let t = Timestamp::from_seconds(-1);
130 assert_eq!(t.to_be_bytes(), [0xff; 8]);
131 let t = Timestamp::from_seconds(1);
132 assert_eq!(t.to_be_bytes(), [0, 0, 0, 0, 0, 0, 0, 1]);
133 }
134
135 #[test]
136 fn now_is_positive() {
137 assert!(Timestamp::now().get() > 1_700_000_000);
138 }
139}