audit_trail/clock.rs
1//! Time source and [`Timestamp`] type.
2
3/// A timestamp expressed as nanoseconds since the Unix epoch.
4///
5/// Stored as a `u64`, so the representable range extends well beyond the
6/// 22nd century. Operations are saturating to avoid panics on overflow.
7///
8/// # Example
9///
10/// ```
11/// use audit_trail::Timestamp;
12///
13/// let t = Timestamp::from_nanos(1_700_000_000_000_000_000);
14/// assert_eq!(t.as_nanos(), 1_700_000_000_000_000_000);
15/// ```
16#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
17pub struct Timestamp(u64);
18
19impl Timestamp {
20 /// The Unix epoch (1970-01-01T00:00:00Z), expressed as nanoseconds.
21 pub const EPOCH: Self = Self(0);
22
23 /// Construct a timestamp from nanoseconds since the Unix epoch.
24 #[inline]
25 pub const fn from_nanos(nanos: u64) -> Self {
26 Self(nanos)
27 }
28
29 /// Nanoseconds since the Unix epoch.
30 #[inline]
31 pub const fn as_nanos(self) -> u64 {
32 self.0
33 }
34}
35
36/// Pluggable time source for the audit chain.
37///
38/// Implementations are expected to be monotonic with respect to successive
39/// calls. The chain enforces monotonicity at append time and returns
40/// [`crate::Error::NonMonotonicClock`] if a regression is observed.
41///
42/// # Example
43///
44/// ```
45/// use audit_trail::{Clock, Timestamp};
46///
47/// /// A fixed clock useful for testing.
48/// struct FixedClock(Timestamp);
49///
50/// impl Clock for FixedClock {
51/// fn now(&self) -> Timestamp { self.0 }
52/// }
53///
54/// let clock = FixedClock(Timestamp::from_nanos(42));
55/// assert_eq!(clock.now().as_nanos(), 42);
56/// ```
57pub trait Clock {
58 /// Return the current timestamp.
59 fn now(&self) -> Timestamp;
60}
61
62/// Wall-clock time source backed by [`std::time::SystemTime`]. Requires
63/// the `std` feature.
64///
65/// Most production deployments want this; it returns nanoseconds since
66/// the Unix epoch using the host's system clock. The host clock is **not**
67/// strictly monotonic — if the operator adjusts time backwards, the next
68/// [`crate::Chain::append`] will return [`crate::Error::NonMonotonicClock`].
69/// Deployments that need a strictly-monotonic source should wrap a
70/// monotonic instant in a custom [`Clock`] instead.
71///
72/// On the unusual case that `SystemTime::now()` is before the Unix epoch,
73/// this returns [`Timestamp::EPOCH`] (0). On the equally-unusual case
74/// that the system clock exceeds `u64::MAX` nanoseconds past the epoch
75/// (year ~2554 and later), the value saturates at `u64::MAX`.
76///
77/// # Example
78///
79/// ```
80/// use audit_trail::{Clock, SystemClock};
81///
82/// let clock = SystemClock::new();
83/// let t = clock.now();
84/// assert!(t.as_nanos() > 0);
85/// ```
86#[cfg(feature = "std")]
87#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
88#[derive(Copy, Clone, Debug, Default)]
89pub struct SystemClock;
90
91#[cfg(feature = "std")]
92impl SystemClock {
93 /// Construct a fresh `SystemClock`.
94 #[inline]
95 pub const fn new() -> Self {
96 Self
97 }
98}
99
100#[cfg(feature = "std")]
101impl Clock for SystemClock {
102 fn now(&self) -> Timestamp {
103 let nanos = std::time::SystemTime::now()
104 .duration_since(std::time::UNIX_EPOCH)
105 .map(|d| u64::try_from(d.as_nanos()).unwrap_or(u64::MAX))
106 .unwrap_or(0);
107 Timestamp::from_nanos(nanos)
108 }
109}