Skip to main content

ps_uuid/methods/
new_v7.rs

1use std::time::Duration;
2
3use crate::UUID;
4
5impl UUID {
6    /// Build an RFC-4122 **Version 7** (Unix-epoch, time-ordered) UUID.
7    ///
8    /// Layout (big-endian, draft RFC “UUID Version 7”, 2022-07-07):
9    ///
10    /// - bytes 0‥=5 – 48-bit Unix epoch timestamp in **milliseconds**
11    /// - bytes 6‥=7 – 12 extra timestamp bits derived from the current
12    ///   sub-millisecond nanoseconds
13    ///   - upper nibble of byte 6 is the version \(7\)
14    /// - bytes 8‥=15 – 64 bits of caller-supplied randomness
15    ///   - two MSBs of byte 8 are the RFC-4122 variant
16    ///
17    /// The `timestamp` argument expresses the elapsed time since
18    /// 1970-01-01 00:00:00 UTC (`Duration::as_millis()` must fit into
19    /// 48 bits).\
20    /// `random_bytes` supplies the final eight random bytes that complete the
21    /// 128-bit UUID.
22    ///
23    /// The function never fails; any excess upper bits in the timestamp are
24    /// truncated, and the *version* \(0b0111\) and *variant* \(0b10xxxxxx\)
25    /// fields are fixed automatically.
26    #[must_use]
27    pub fn new_v7(timestamp: Duration, random_bytes: [u8; 8]) -> Self {
28        let mut uuid = Self::nil();
29
30        // 48-bit Unix-epoch milliseconds (network order)
31        uuid.bytes[0..6].copy_from_slice(&timestamp.as_millis().to_be_bytes()[10..16]);
32
33        // Sub-millisecond nanoseconds -> 18 extra timestamp bits
34        let nanos = (timestamp.subsec_nanos() % 1_000_000).to_be_bytes();
35
36        // Byte 6: Version (0x7_)  + 4 timestamp bits
37        uuid.bytes[6] = 0x70 | nanos[1];
38
39        // Byte 7: next 8 timestamp bits
40        uuid.bytes[7] = nanos[2];
41
42        // Caller-provided random payload
43        uuid.bytes[8..].copy_from_slice(&random_bytes);
44
45        // Overwrite version & variant once more to guarantee correctness
46        uuid.with_version(7)
47    }
48}
49
50// ────────────────────────────────────────────────────────────────────────────
51// Test-suite for `UUID::new_v7`
52// ────────────────────────────────────────────────────────────────────────────
53#[cfg(test)]
54mod tests {
55    use super::*;
56    use std::time::Duration;
57
58    // Helper – extract version & variant
59    const fn version(b: &[u8; 16]) -> u8 {
60        b[6] >> 4
61    }
62    const fn variant(b: &[u8; 16]) -> u8 {
63        b[8] >> 6
64    }
65
66    #[test]
67    fn version_and_variant_are_correct() {
68        let uuid = UUID::new_v7(Duration::from_nanos(0), [0; 8]);
69        let bytes = uuid.as_bytes();
70        assert_eq!(version(bytes), 0b0111, "version must be 7");
71        assert_eq!(variant(bytes), 0b10, "variant must be RFC-4122");
72    }
73
74    #[test]
75    fn timestamp_is_encoded_big_endian() {
76        // 0x0123_4567_89AB milliseconds  (≈ 2023-03-18 01:32:21 UTC)
77        let ms = 0x0123_4567_89ABu64;
78        let dur = Duration::from_millis(ms);
79        let uuid = UUID::new_v7(dur, [0; 8]);
80        let b = uuid.as_bytes();
81
82        assert_eq!(
83            &b[0..6],
84            &[0x01, 0x23, 0x45, 0x67, 0x89, 0xAB],
85            "48-bit millisecond timestamp must be big-endian"
86        );
87    }
88
89    #[test]
90    fn extra_timestamp_bits_are_encoded() {
91        // Choose 987 654 ns (0x0F 1206) inside the millisecond
92        let dur = Duration::from_millis(123) + Duration::from_nanos(987_654);
93        let uuid = UUID::new_v7(dur, [0; 8]);
94        let b = uuid.as_bytes();
95
96        let nanos = (987_654u32).to_be_bytes();
97
98        // byte 6 low nibble & byte 7 must match nanos[1] / nanos[2]
99        assert_eq!(b[6] & 0x0F, nanos[1]);
100        assert_eq!(b[7], nanos[2]);
101    }
102
103    #[test]
104    fn random_payload_is_inserted_verbatim() {
105        let rnd = [0x81, 2, 3, 4, 5, 6, 7, 8];
106        let uuid = UUID::new_v7(Duration::from_secs(0), rnd);
107        assert_eq!(&uuid.as_bytes()[8..16], &rnd);
108    }
109
110    #[test]
111    fn identical_input_is_deterministic() {
112        let ts = Duration::from_secs(42);
113        let rnd = [9, 8, 7, 6, 5, 4, 3, 2];
114
115        let a = UUID::new_v7(ts, rnd);
116        let b = UUID::new_v7(ts, rnd);
117
118        assert_eq!(a.bytes, b.bytes, "same input must yield same UUID");
119    }
120
121    #[test]
122    fn different_random_payloads_differ() {
123        let ts = Duration::from_secs(1);
124        let a = UUID::new_v7(ts, [0; 8]);
125        let b = UUID::new_v7(ts, [1; 8]);
126        assert_ne!(a.bytes, b.bytes, "different random bytes must change UUID");
127    }
128}