Skip to main content

uuid/
v7.rs

1//! The implementation for Version 7 UUIDs.
2//!
3//! Note that you need to enable the `v7` Cargo feature
4//! in order to use this module.
5
6use core::cmp;
7
8use crate::{rng, timestamp::Timestamp, Builder, Uuid};
9
10impl Uuid {
11    /// Create a new version 7 UUID using the current time value.
12    ///
13    /// This method is a convenient alternative to [`Uuid::new_v7`] that uses the current system time
14    /// as the source timestamp. All UUIDs generated through this method by the same process are
15    /// guaranteed to be ordered by their creation.
16    #[cfg(feature = "std")]
17    pub fn now_v7() -> Self {
18        Self::new_v7(Timestamp::now(
19            crate::timestamp::context::shared_context_v7(),
20        ))
21    }
22
23    /// Create a new version 7 UUID using a time value and random bytes.
24    ///
25    /// When the `std` feature is enabled, you can also use [`Uuid::now_v7`].
26    ///
27    /// Note that usage of this method requires the `v7` feature of this crate
28    /// to be enabled.
29    ///
30    /// Also see [`Uuid::now_v7`] for a convenient way to generate version 7
31    /// UUIDs using the current system time.
32    ///
33    /// # Counter treatment
34    ///
35    /// This method accepts a [`Timestamp`] which may include a counter value.
36    /// The 74 most significant bits of the counter value are retained when
37    /// constructing the UUID, and the rest is filled with random data. Avoid
38    /// using a counter wider than 74 bits.
39    ///
40    /// # Examples
41    ///
42    /// A v7 UUID can be created from a unix [`Timestamp`] plus a 128 bit
43    /// random number. When supplied as such, the data will be combined
44    /// to ensure uniqueness and sortability at millisecond granularity.
45    ///
46    /// ```rust
47    /// # use uuid::{Uuid, Timestamp, NoContext};
48    /// let ts = Timestamp::from_unix(NoContext, 1497624119, 1234);
49    ///
50    /// let uuid = Uuid::new_v7(ts);
51    ///
52    /// assert!(
53    ///     uuid.hyphenated().to_string().starts_with("015cb15a-86d8-7")
54    /// );
55    /// ```
56    ///
57    /// A v7 UUID can also be created with a counter to ensure batches of
58    /// UUIDs created together remain sortable:
59    ///
60    /// ```rust
61    /// # use uuid::{Uuid, Timestamp, ContextV7};
62    /// let context = ContextV7::new();
63    /// let uuid1 = Uuid::new_v7(Timestamp::from_unix(&context, 1497624119, 1234));
64    /// let uuid2 = Uuid::new_v7(Timestamp::from_unix(&context, 1497624119, 1234));
65    ///
66    /// assert!(uuid1 < uuid2);
67    /// ```
68    ///
69    /// # References
70    ///
71    /// * [UUID Version 7 in RFC 9562](https://www.ietf.org/rfc/rfc9562.html#section-5.7)
72    pub fn new_v7(ts: Timestamp) -> Self {
73        let (secs, nanos) = ts.to_unix();
74        let millis = secs
75            .saturating_mul(1000)
76            .saturating_add(nanos as u64 / 1_000_000);
77
78        let (mut counter, counter_bits) = ts.counter();
79
80        // `Builder::from_unix_timestamp_millis` takes the top 80 bits of this value,
81        // so the counter is placed directly below the version nibble and shifted
82        // around the variant:
83        //
84        // bit 127                                                       bit 48
85        // | ver (4) |    rand_a (12)    | var (2) |       rand_b (62)        | ...
86        //           |<- counter <= 12 ->|
87        //           |<---- counter > 12: shifted by 2 over the variant ---->|
88        const RAND_A_BITS: u32 = 12;
89        const PAYLOAD_BITS: u32 = RAND_A_BITS + 62;
90
91        // Retain the most significant bits of a counter wider than the payload
92        let mut counter_bits = cmp::min(counter_bits as u32, 128);
93        if counter_bits > PAYLOAD_BITS {
94            counter >>= counter_bits - PAYLOAD_BITS;
95            counter_bits = PAYLOAD_BITS;
96        }
97
98        // Shift the counter around the variant field
99        if counter_bits > RAND_A_BITS {
100            let mask = u128::MAX << (counter_bits - RAND_A_BITS);
101            counter = (counter & !mask) | ((counter & mask) << 2);
102            counter_bits += 2;
103        }
104
105        let counter_and_random = if counter_bits == 0 {
106            rng::u128()
107        } else {
108            let shift = 124 - counter_bits;
109
110            (rng::u128() & (u128::MAX >> (128 - shift))) | (counter << shift)
111        };
112
113        Builder::from_unix_timestamp_millis(
114            millis,
115            &counter_and_random.to_be_bytes()[..10].try_into().unwrap(),
116        )
117        .into_uuid()
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    use crate::{std::string::ToString, ClockSequence, NoContext, NonNilUuid, Variant, Version};
126
127    #[cfg(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")))]
128    use wasm_bindgen_test::*;
129
130    #[test]
131    #[cfg_attr(
132        all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")),
133        wasm_bindgen_test
134    )]
135    fn test_new() {
136        let ts: u64 = 1645557742000;
137
138        let seconds = ts / 1000;
139        let nanos = ((ts % 1000) * 1_000_000) as u32;
140
141        let timestamp = Timestamp::from_unix(NoContext, seconds, nanos);
142
143        let uuid = Uuid::new_v7(timestamp);
144        let uustr = uuid.hyphenated().to_string();
145
146        assert_eq!(
147            NonNilUuid::new_v7(timestamp).get().get_timestamp(),
148            uuid.get_timestamp()
149        );
150        assert_eq!(uuid.get_version(), Some(Version::SortRand));
151        assert_eq!(uuid.get_variant(), Variant::RFC4122);
152        assert!(uuid.hyphenated().to_string().starts_with("017f22e2-79b0-7"));
153
154        // Ensure parsing the same UUID produces the same timestamp
155        let parsed = Uuid::parse_str(uustr.as_str()).unwrap();
156
157        assert_eq!(uuid, parsed);
158    }
159
160    #[test]
161    #[cfg_attr(
162        all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")),
163        wasm_bindgen_test
164    )]
165    #[cfg(feature = "std")]
166    fn test_now() {
167        let uuid = Uuid::now_v7();
168
169        assert_eq!(uuid.get_version(), Some(Version::SortRand));
170        assert_eq!(uuid.get_variant(), Variant::RFC4122);
171        assert!(!NonNilUuid::now_v7().get().is_nil());
172    }
173
174    #[test]
175    #[cfg_attr(
176        all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")),
177        wasm_bindgen_test
178    )]
179    fn test_sorting() {
180        let time1: u64 = 1_496_854_535;
181        let time_fraction1: u32 = 812_000_000;
182
183        let time2 = time1 + 4000;
184        let time_fraction2 = time_fraction1;
185
186        let uuid1 = Uuid::new_v7(Timestamp::from_unix(NoContext, time1, time_fraction1));
187        let uuid2 = Uuid::new_v7(Timestamp::from_unix(NoContext, time2, time_fraction2));
188
189        assert!(uuid1.as_bytes() < uuid2.as_bytes());
190        assert!(uuid1.to_string() < uuid2.to_string());
191    }
192
193    #[test]
194    #[cfg_attr(
195        all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")),
196        wasm_bindgen_test
197    )]
198    fn test_new_timestamp_roundtrip() {
199        let time: u64 = 1_496_854_535;
200        let time_fraction: u32 = 812_000_000;
201
202        let ts = Timestamp::from_unix(NoContext, time, time_fraction);
203
204        let uuid = Uuid::new_v7(ts);
205
206        let decoded_ts = uuid.get_timestamp().unwrap();
207
208        assert_eq!(ts.to_unix(), decoded_ts.to_unix());
209    }
210
211    #[test]
212    #[cfg_attr(
213        all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")),
214        wasm_bindgen_test
215    )]
216    fn test_new_max_context() {
217        struct MaxContext;
218
219        impl ClockSequence for MaxContext {
220            type Output = u128;
221
222            fn generate_sequence(&self, _seconds: u64, _nanos: u32) -> Self::Output {
223                u128::MAX
224            }
225
226            fn usable_bits(&self) -> usize {
227                128
228            }
229        }
230
231        let time: u64 = 1_496_854_535;
232        let time_fraction: u32 = 812_000_000;
233
234        // Ensure we don't overflow here
235        let ts = Timestamp::from_unix(MaxContext, time, time_fraction);
236
237        let uuid = Uuid::new_v7(ts);
238
239        assert_eq!(uuid.get_version(), Some(Version::SortRand));
240        assert_eq!(uuid.get_variant(), Variant::RFC4122);
241
242        let decoded_ts = uuid.get_timestamp().unwrap();
243
244        assert_eq!(ts.to_unix(), decoded_ts.to_unix());
245    }
246
247    #[test]
248    #[cfg_attr(
249        all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")),
250        wasm_bindgen_test
251    )]
252    fn test_new_counter_range() {
253        for (width, eq) in [
254            (0, false),
255            (3, false),
256            (43, false),
257            (74, true),
258            (u8::MAX, true),
259        ] {
260            for counter in [0u128, u128::MAX] {
261                let ts = Timestamp::from_unix_time(1_700_000_000, 0, counter, width);
262
263                let a = Uuid::new_v7(ts);
264                let b = Uuid::new_v7(ts);
265
266                assert_eq!((1_700_000_000, 0), a.get_timestamp().unwrap().to_unix());
267                assert_eq!((1_700_000_000, 0), b.get_timestamp().unwrap().to_unix());
268
269                assert_eq!(
270                    eq,
271                    a == b,
272                    "{:>032x} = {:>032x} with counter {counter:x} should be {eq:?}",
273                    a.as_u128(),
274                    b.as_u128()
275                );
276            }
277        }
278    }
279
280    #[test]
281    #[cfg_attr(
282        all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")),
283        wasm_bindgen_test
284    )]
285    fn test_42bit_counter_is_fully_preserved() {
286        fn rand_a(uuid: &Uuid) -> u16 {
287            let b = uuid.as_bytes();
288
289            (((b[6] & 0x0f) as u16) << 8) | b[7] as u16
290        }
291
292        // `rand_a` and the top 30 bits of `rand_b`, with the variant masked out
293        fn counter_bits(uuid: &Uuid) -> (u16, [u8; 4]) {
294            let b = uuid.as_bytes();
295
296            (rand_a(uuid), [b[8] & 0x3f, b[9], b[10], b[11]])
297        }
298
299        for bit in 0..42 {
300            let counter = 1u128 << bit;
301
302            let with = Uuid::new_v7(Timestamp::from_unix_time(0, 0, counter, 42));
303            let without = Uuid::new_v7(Timestamp::from_unix_time(0, 0, 0, 42));
304
305            assert_ne!(
306                counter_bits(&with),
307                counter_bits(&without),
308                "counter bit {bit} did not reach the UUID"
309            );
310        }
311
312        let all_ones = Uuid::new_v7(Timestamp::from_unix_time(0, 0, (1 << 42) - 1, 42));
313        assert_eq!(0x0fff, rand_a(&all_ones));
314        assert_eq!(Variant::RFC4122, all_ones.get_variant());
315
316        let before = Uuid::new_v7(Timestamp::from_unix_time(1, 0, 0x3f_ffff_ffff, 42));
317        let after = Uuid::new_v7(Timestamp::from_unix_time(1, 0, 0x40_0000_0000, 42));
318        assert!(before < after, "{before} should sort before {after}");
319    }
320
321    #[test]
322    #[cfg_attr(
323        all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")),
324        wasm_bindgen_test
325    )]
326    fn test_additional_precision_preserves_sorting() {
327        for start in (0..1_000_000).step_by(50_000) {
328            for delta in [50_000u32, 100_000, 200_000] {
329                if start + delta >= 1_000_000 {
330                    continue;
331                }
332
333                let context = crate::ContextV7::new().with_additional_precision();
334
335                let earlier = Uuid::new_v7(Timestamp::from_unix(&context, 1_700_000_000, start));
336                let later =
337                    Uuid::new_v7(Timestamp::from_unix(&context, 1_700_000_000, start + delta));
338
339                assert!(
340                    earlier < later,
341                    "{start}ns gave {earlier} and {}ns gave {later}",
342                    start + delta
343                );
344            }
345        }
346    }
347
348    #[test]
349    #[cfg_attr(
350        all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")),
351        wasm_bindgen_test
352    )]
353    fn test_new_max() {
354        let ts = Timestamp::from_unix_time(u64::MAX, 0, 0, 0);
355        let uuid = Uuid::new_v7(ts);
356
357        let decoded_ts = uuid.get_timestamp().unwrap();
358
359        assert_eq!((281474976710, 655000000), decoded_ts.to_unix());
360    }
361}