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    /// Only up to 74 bits of the counter value will be used when constructing
37    /// the UUID. Any unused counter bits will be filled with random data.
38    ///
39    /// # Examples
40    ///
41    /// A v7 UUID can be created from a unix [`Timestamp`] plus a 128 bit
42    /// random number. When supplied as such, the data will be combined
43    /// to ensure uniqueness and sortability at millisecond granularity.
44    ///
45    /// ```rust
46    /// # use uuid::{Uuid, Timestamp, NoContext};
47    /// let ts = Timestamp::from_unix(NoContext, 1497624119, 1234);
48    ///
49    /// let uuid = Uuid::new_v7(ts);
50    ///
51    /// assert!(
52    ///     uuid.hyphenated().to_string().starts_with("015cb15a-86d8-7")
53    /// );
54    /// ```
55    ///
56    /// A v7 UUID can also be created with a counter to ensure batches of
57    /// UUIDs created together remain sortable:
58    ///
59    /// ```rust
60    /// # use uuid::{Uuid, Timestamp, ContextV7};
61    /// let context = ContextV7::new();
62    /// let uuid1 = Uuid::new_v7(Timestamp::from_unix(&context, 1497624119, 1234));
63    /// let uuid2 = Uuid::new_v7(Timestamp::from_unix(&context, 1497624119, 1234));
64    ///
65    /// assert!(uuid1 < uuid2);
66    /// ```
67    ///
68    /// # References
69    ///
70    /// * [UUID Version 7 in RFC 9562](https://www.ietf.org/rfc/rfc9562.html#section-5.7)
71    pub fn new_v7(ts: Timestamp) -> Self {
72        let (secs, nanos) = ts.to_unix();
73        let millis = secs
74            .saturating_mul(1000)
75            .saturating_add(nanos as u64 / 1_000_000);
76
77        let (counter, counter_bits) = ts.counter();
78
79        // If the counter intersects the variant field then shift around it.
80        // This ensures that any bits set in the counter that would intersect
81        // the variant are still preserved
82        let shift_counter_over_variant = |mut counter: u128, mut counter_bits: u32| {
83            let mask = u128::MAX << (cmp::min(128, counter_bits) - 12);
84            counter = (counter & !mask) | ((counter & mask) << 2);
85            counter_bits += 2;
86
87            (counter, counter_bits)
88        };
89
90        // Mask `counter_bits` of the `counter` into `dst`
91        let mask_counter_into_random = |mut dst: u128, counter: u128, counter_bits: u32| {
92            dst &= u128::MAX >> counter_bits;
93            dst |= counter << (128 - counter_bits);
94
95            dst
96        };
97
98        let counter_and_random = match counter_bits {
99            // The counter doesn't contribute any bits
100            0 => rng::u128(),
101            // The counter doesn't intersect the variant field
102            // It needs to be merged with random data
103            ..12 => {
104                let counter_bits = counter_bits as u32;
105
106                mask_counter_into_random(rng::u128(), counter, counter_bits)
107            }
108            // `rand_a` (12 bits) + `rand_b` (62 bits) + `var` (2 bits)
109            // The counter needs to be shifted around the variant and merged with random data
110            ..74 => {
111                let (counter, counter_bits) =
112                    shift_counter_over_variant(counter, counter_bits as u32);
113
114                mask_counter_into_random(rng::u128(), counter, counter_bits)
115            }
116            // The counter overrides all bits
117            74.. => {
118                let (counter, _) = shift_counter_over_variant(counter, counter_bits as u32);
119
120                counter
121            }
122        };
123
124        Builder::from_unix_timestamp_millis(
125            millis,
126            &counter_and_random.to_be_bytes()[..10].try_into().unwrap(),
127        )
128        .into_uuid()
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    use crate::{std::string::ToString, ClockSequence, NoContext, Variant, Version};
137
138    #[cfg(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")))]
139    use wasm_bindgen_test::*;
140
141    #[test]
142    #[cfg_attr(
143        all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")),
144        wasm_bindgen_test
145    )]
146    fn test_new() {
147        let ts: u64 = 1645557742000;
148
149        let seconds = ts / 1000;
150        let nanos = ((ts % 1000) * 1_000_000) as u32;
151
152        let uuid = Uuid::new_v7(Timestamp::from_unix(NoContext, seconds, nanos));
153        let uustr = uuid.hyphenated().to_string();
154
155        assert_eq!(uuid.get_version(), Some(Version::SortRand));
156        assert_eq!(uuid.get_variant(), Variant::RFC4122);
157        assert!(uuid.hyphenated().to_string().starts_with("017f22e2-79b0-7"));
158
159        // Ensure parsing the same UUID produces the same timestamp
160        let parsed = Uuid::parse_str(uustr.as_str()).unwrap();
161
162        assert_eq!(uuid, parsed);
163    }
164
165    #[test]
166    #[cfg_attr(
167        all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")),
168        wasm_bindgen_test
169    )]
170    #[cfg(feature = "std")]
171    fn test_now() {
172        let uuid = Uuid::now_v7();
173
174        assert_eq!(uuid.get_version(), Some(Version::SortRand));
175        assert_eq!(uuid.get_variant(), Variant::RFC4122);
176    }
177
178    #[test]
179    #[cfg_attr(
180        all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")),
181        wasm_bindgen_test
182    )]
183    fn test_sorting() {
184        let time1: u64 = 1_496_854_535;
185        let time_fraction1: u32 = 812_000_000;
186
187        let time2 = time1 + 4000;
188        let time_fraction2 = time_fraction1;
189
190        let uuid1 = Uuid::new_v7(Timestamp::from_unix(NoContext, time1, time_fraction1));
191        let uuid2 = Uuid::new_v7(Timestamp::from_unix(NoContext, time2, time_fraction2));
192
193        assert!(uuid1.as_bytes() < uuid2.as_bytes());
194        assert!(uuid1.to_string() < uuid2.to_string());
195    }
196
197    #[test]
198    #[cfg_attr(
199        all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")),
200        wasm_bindgen_test
201    )]
202    fn test_new_timestamp_roundtrip() {
203        let time: u64 = 1_496_854_535;
204        let time_fraction: u32 = 812_000_000;
205
206        let ts = Timestamp::from_unix(NoContext, time, time_fraction);
207
208        let uuid = Uuid::new_v7(ts);
209
210        let decoded_ts = uuid.get_timestamp().unwrap();
211
212        assert_eq!(ts.to_unix(), decoded_ts.to_unix());
213    }
214
215    #[test]
216    #[cfg_attr(
217        all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")),
218        wasm_bindgen_test
219    )]
220    fn test_new_max_context() {
221        struct MaxContext;
222
223        impl ClockSequence for MaxContext {
224            type Output = u128;
225
226            fn generate_sequence(&self, _seconds: u64, _nanos: u32) -> Self::Output {
227                u128::MAX
228            }
229
230            fn usable_bits(&self) -> usize {
231                128
232            }
233        }
234
235        let time: u64 = 1_496_854_535;
236        let time_fraction: u32 = 812_000_000;
237
238        // Ensure we don't overflow here
239        let ts = Timestamp::from_unix(MaxContext, time, time_fraction);
240
241        let uuid = Uuid::new_v7(ts);
242
243        assert_eq!(uuid.get_version(), Some(Version::SortRand));
244        assert_eq!(uuid.get_variant(), Variant::RFC4122);
245
246        let decoded_ts = uuid.get_timestamp().unwrap();
247
248        assert_eq!(ts.to_unix(), decoded_ts.to_unix());
249    }
250
251    #[test]
252    #[cfg_attr(
253        all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")),
254        wasm_bindgen_test
255    )]
256    fn test_new_counter_range() {
257        for (width, eq) in [
258            (0, false),
259            (3, false),
260            (43, false),
261            (74, true),
262            (u8::MAX, true),
263        ] {
264            for counter in [0u128, u128::MAX] {
265                let ts = Timestamp::from_unix_time(1_700_000_000, 0, counter, width);
266
267                let a = Uuid::new_v7(ts);
268                let b = Uuid::new_v7(ts);
269
270                assert_eq!((1_700_000_000, 0), a.get_timestamp().unwrap().to_unix());
271                assert_eq!((1_700_000_000, 0), b.get_timestamp().unwrap().to_unix());
272
273                assert_eq!(
274                    eq,
275                    a == b,
276                    "{:>032x} = {:>032x} with counter {counter:x} should be {eq:?}",
277                    a.as_u128(),
278                    b.as_u128()
279                );
280            }
281        }
282    }
283
284    #[test]
285    #[cfg_attr(
286        all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")),
287        wasm_bindgen_test
288    )]
289    fn test_new_max() {
290        let ts = Timestamp::from_unix_time(u64::MAX, 0, 0, 0);
291        let uuid = Uuid::new_v7(ts);
292
293        let decoded_ts = uuid.get_timestamp().unwrap();
294
295        assert_eq!((281474976710, 655000000), decoded_ts.to_unix());
296    }
297}