use derive_more::{Display, From, Into};
use std::time::Duration;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Display, From, Into)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
#[display("{_0}")]
pub struct Timestamp(i64);
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum TimestampError {
#[error("timestamp drifted by {drift_seconds}s (window ±{window_seconds}s)")]
OutsideSkewWindow {
drift_seconds: i64,
window_seconds: i64,
},
}
impl Timestamp {
pub const ZERO: Self = Self(0);
#[inline]
pub const fn from_seconds(s: i64) -> Self {
Self(s)
}
#[inline]
pub const fn get(self) -> i64 {
self.0
}
#[inline]
pub const fn to_be_bytes(self) -> [u8; 8] {
self.0.to_be_bytes()
}
pub fn now() -> Self {
use std::time::{SystemTime, UNIX_EPOCH};
let secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock set before unix epoch")
.as_secs();
Self(i64::try_from(secs).expect("system clock exceeds i64 unix seconds"))
}
pub fn skew_check(self, local: Self, window: Duration) -> Result<(), TimestampError> {
let drift = self.0.saturating_sub(local.0);
let window_secs = i64::try_from(window.as_secs()).unwrap_or(i64::MAX);
if drift.unsigned_abs() <= window_secs.unsigned_abs() {
Ok(())
} else {
Err(TimestampError::OutsideSkewWindow {
drift_seconds: drift,
window_seconds: window_secs,
})
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn skew_check_within_window() {
let local = Timestamp::from_seconds(1_000_000);
let remote = Timestamp::from_seconds(1_000_030); assert!(remote.skew_check(local, Duration::from_secs(60)).is_ok());
}
#[test]
fn skew_check_negative_within_window() {
let local = Timestamp::from_seconds(1_000_000);
let remote = Timestamp::from_seconds(999_940); assert!(remote.skew_check(local, Duration::from_secs(60)).is_ok());
}
#[test]
fn skew_check_outside_window() {
let local = Timestamp::from_seconds(1_000_000);
let remote = Timestamp::from_seconds(1_000_120); let err = remote
.skew_check(local, Duration::from_secs(60))
.unwrap_err();
assert!(matches!(
err,
TimestampError::OutsideSkewWindow {
drift_seconds: 120,
window_seconds: 60
}
));
}
#[test]
fn be_bytes_signed() {
let t = Timestamp::from_seconds(-1);
assert_eq!(t.to_be_bytes(), [0xff; 8]);
let t = Timestamp::from_seconds(1);
assert_eq!(t.to_be_bytes(), [0, 0, 0, 0, 0, 0, 0, 1]);
}
#[test]
fn now_is_positive() {
assert!(Timestamp::now().get() > 1_700_000_000);
}
}