Skip to main content

matter_cert/
time.rs

1//! Matter time representation.
2//!
3//! Matter certificates use a 32-bit "epoch-s" counter — seconds since
4//! 2000-01-01T00:00:00Z (NOT Unix epoch). [`MatterTime`] wraps this
5//! representation natively; conversions to/from Unix time are explicit.
6
7use core::cmp::Ordering;
8
9/// The offset (in Unix seconds) of the Matter epoch from the Unix epoch.
10/// Matter epoch starts 2000-01-01T00:00:00Z; that's 946,684,800 Unix seconds.
11const MATTER_EPOCH_UNIX_OFFSET: u64 = 946_684_800;
12
13/// A Matter time value: seconds since 2000-01-01T00:00:00Z, wrapping
14/// as a `u32` (the spec's wire-native representation).
15///
16/// `MatterTime(0)` carries a special meaning of "no expiry" for
17/// certificate `not_after` fields; see [`MatterTime::NO_EXPIRY`].
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub struct MatterTime(pub u32);
20
21impl MatterTime {
22    /// Sentinel meaning "no expiry" for certificate `not_after`.
23    pub const NO_EXPIRY: Self = Self(0);
24
25    /// Convert from Unix seconds. Saturates at `u32::MAX` on overflow.
26    /// Unix times before the Matter epoch saturate to `MatterTime(0)`.
27    #[must_use]
28    pub fn from_unix_secs(unix: u64) -> Self {
29        let matter = unix.saturating_sub(MATTER_EPOCH_UNIX_OFFSET);
30        let clamped = u32::try_from(matter).unwrap_or(u32::MAX);
31        Self(clamped)
32    }
33
34    /// Convert to Unix seconds.
35    #[must_use]
36    pub fn to_unix_secs(self) -> u64 {
37        u64::from(self.0) + MATTER_EPOCH_UNIX_OFFSET
38    }
39}
40
41impl PartialOrd for MatterTime {
42    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
43        Some(self.cmp(other))
44    }
45}
46
47impl Ord for MatterTime {
48    fn cmp(&self, other: &Self) -> Ordering {
49        self.0.cmp(&other.0)
50    }
51}
52
53#[cfg(test)]
54#[allow(clippy::unwrap_used)] // Test-code carve-out: see CLAUDE.md.
55mod tests {
56    use super::*;
57
58    #[test]
59    fn unix_to_matter_to_unix_round_trip() {
60        let unix = 1_763_337_600u64;
61        let matter = MatterTime::from_unix_secs(unix);
62        assert_eq!(matter.to_unix_secs(), unix);
63    }
64
65    #[test]
66    fn matter_epoch_zero_maps_to_2000_01_01() {
67        assert_eq!(MatterTime(0).to_unix_secs(), MATTER_EPOCH_UNIX_OFFSET);
68    }
69
70    #[test]
71    fn no_expiry_constant_is_zero() {
72        assert_eq!(MatterTime::NO_EXPIRY, MatterTime(0));
73    }
74
75    #[test]
76    fn pre_matter_epoch_unix_saturates_to_zero() {
77        let unix = 946_684_799u64;
78        assert_eq!(MatterTime::from_unix_secs(unix), MatterTime(0));
79    }
80
81    #[test]
82    fn unix_at_matter_u32_max_boundary_is_exact() {
83        // The largest Matter-representable Unix second: epoch offset
84        // plus u32::MAX. This must NOT saturate — it's the highest
85        // exact-representable point.
86        let unix = MATTER_EPOCH_UNIX_OFFSET + u64::from(u32::MAX);
87        assert_eq!(MatterTime::from_unix_secs(unix), MatterTime(u32::MAX));
88    }
89
90    #[test]
91    fn unix_just_above_matter_u32_max_saturates() {
92        // One second past the boundary must saturate to u32::MAX (not
93        // wrap to 0). Exercises the `u32::try_from(...).unwrap_or(u32::MAX)`
94        // branch in from_unix_secs.
95        let unix = MATTER_EPOCH_UNIX_OFFSET + u64::from(u32::MAX) + 1;
96        assert_eq!(MatterTime::from_unix_secs(unix), MatterTime(u32::MAX));
97    }
98
99    #[test]
100    fn unix_u64_max_saturates_to_u32_max() {
101        // The most extreme overflow input.
102        assert_eq!(MatterTime::from_unix_secs(u64::MAX), MatterTime(u32::MAX));
103    }
104
105    #[test]
106    fn ordering_uses_native_u32() {
107        assert!(MatterTime(100) < MatterTime(200));
108        assert!(MatterTime(u32::MAX) > MatterTime(0));
109    }
110}