Skip to main content

id_forge/
uuid.rs

1//! # UUID generation
2//!
3//! UUID v4 (random) and v7 (time-ordered) per RFC 9562.
4//!
5//! UUIDs produced by this module set the version and variant bits exactly
6//! as the RFC requires, so they round-trip through any conformant parser.
7//!
8//! ```
9//! use id_forge::uuid::Uuid;
10//!
11//! let v4 = Uuid::v4();
12//! let v7 = Uuid::v7();
13//! let parsed = Uuid::parse_str(&v4.to_string()).unwrap();
14//! assert_eq!(v4, parsed);
15//! assert_eq!(v7.to_string().len(), 36);
16//! ```
17//!
18//! ## Randomness
19//!
20//! The random portion is filled by an inline xoshiro256\*\* generator
21//! seeded from process ID, wall-clock nanoseconds, and a per-process
22//! counter. This is fast and unpredictable across processes, but it is
23//! **not** cryptographically secure — use a CSPRNG for session tokens
24//! or API keys.
25
26use core::fmt;
27use std::cell::RefCell;
28use std::sync::atomic::{AtomicU64, Ordering};
29use std::time::{SystemTime, UNIX_EPOCH};
30
31/// A 128-bit UUID.
32///
33/// The internal representation is 16 big-endian bytes per RFC 9562.
34///
35/// # Example
36///
37/// ```
38/// use id_forge::uuid::Uuid;
39///
40/// let id = Uuid::v4();
41/// assert_eq!(id.to_string().len(), 36);
42/// ```
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
44pub struct Uuid([u8; 16]);
45
46impl Uuid {
47    /// The Nil UUID: all 128 bits set to zero (RFC 9562 §5.9).
48    ///
49    /// # Example
50    ///
51    /// ```
52    /// use id_forge::uuid::Uuid;
53    ///
54    /// assert_eq!(Uuid::nil().to_string(), "00000000-0000-0000-0000-000000000000");
55    /// ```
56    pub const fn nil() -> Self {
57        Self([0u8; 16])
58    }
59
60    /// The Max UUID: all 128 bits set to one (RFC 9562 §5.10).
61    ///
62    /// # Example
63    ///
64    /// ```
65    /// use id_forge::uuid::Uuid;
66    ///
67    /// assert_eq!(Uuid::max().to_string(), "ffffffff-ffff-ffff-ffff-ffffffffffff");
68    /// ```
69    pub const fn max() -> Self {
70        Self([0xff; 16])
71    }
72
73    /// Construct a v4 (random) UUID per RFC 9562 §5.4.
74    ///
75    /// 122 random bits with the version nibble set to `0100` and the
76    /// variant bits set to `10` (RFC 4122 layout).
77    ///
78    /// # Example
79    ///
80    /// ```
81    /// use id_forge::uuid::Uuid;
82    ///
83    /// let id = Uuid::v4();
84    /// assert_eq!(id.version(), 4);
85    /// ```
86    pub fn v4() -> Self {
87        let mut bytes = random_bytes_16();
88        bytes[6] = (bytes[6] & 0x0f) | 0x40;
89        bytes[8] = (bytes[8] & 0x3f) | 0x80;
90        Self(bytes)
91    }
92
93    /// Construct a v7 (time-ordered) UUID per RFC 9562 §5.7.
94    ///
95    /// 48-bit big-endian millisecond timestamp prefix, 74 random bits,
96    /// with the version nibble set to `0111` and the RFC 4122 variant bits.
97    /// Two v7 IDs generated in different milliseconds compare in
98    /// timestamp order byte-wise.
99    ///
100    /// # Example
101    ///
102    /// ```
103    /// use id_forge::uuid::Uuid;
104    ///
105    /// let id = Uuid::v7();
106    /// assert_eq!(id.version(), 7);
107    /// ```
108    pub fn v7() -> Self {
109        let ms = SystemTime::now()
110            .duration_since(UNIX_EPOCH)
111            .map(|d| d.as_millis() as u64)
112            .unwrap_or(0);
113        let mut bytes = random_bytes_16();
114        let ms_bytes = ms.to_be_bytes();
115        bytes[0..6].copy_from_slice(&ms_bytes[2..8]);
116        bytes[6] = (bytes[6] & 0x0f) | 0x70;
117        bytes[8] = (bytes[8] & 0x3f) | 0x80;
118        Self(bytes)
119    }
120
121    /// Wrap a 16-byte big-endian representation.
122    ///
123    /// The bytes are taken as-is; no version or variant bits are
124    /// touched. Use this to round-trip an externally generated UUID
125    /// or to reconstruct one from storage.
126    ///
127    /// # Example
128    ///
129    /// ```
130    /// use id_forge::uuid::Uuid;
131    ///
132    /// let id = Uuid::v4();
133    /// let copy = Uuid::from_bytes(id.as_bytes());
134    /// assert_eq!(id, copy);
135    /// ```
136    pub const fn from_bytes(bytes: &[u8; 16]) -> Self {
137        Self(*bytes)
138    }
139
140    /// Return the raw 16-byte big-endian representation.
141    pub const fn as_bytes(&self) -> &[u8; 16] {
142        &self.0
143    }
144
145    /// Return the version nibble (the high 4 bits of byte 6).
146    ///
147    /// `4` for v4, `7` for v7, `0` for [`Uuid::nil`], `15` for [`Uuid::max`].
148    pub const fn version(&self) -> u8 {
149        self.0[6] >> 4
150    }
151
152    /// Parse a UUID from its canonical 36-character hyphenated form
153    /// (e.g. `f47ac10b-58cc-4372-a567-0e02b2c3d479`).
154    ///
155    /// Parsing is case-insensitive. Returns [`ParseError`] if the
156    /// input is not exactly 36 characters, has hyphens in the wrong
157    /// positions, or contains a non-hex digit.
158    ///
159    /// # Example
160    ///
161    /// ```
162    /// use id_forge::uuid::Uuid;
163    ///
164    /// let id = Uuid::parse_str("f47ac10b-58cc-4372-a567-0e02b2c3d479").unwrap();
165    /// assert_eq!(id.to_string(), "f47ac10b-58cc-4372-a567-0e02b2c3d479");
166    /// ```
167    pub fn parse_str(input: &str) -> Result<Self, ParseError> {
168        let bytes = input.as_bytes();
169        if bytes.len() != 36 {
170            return Err(ParseError::InvalidLength(bytes.len()));
171        }
172        let hyphen_positions = [8usize, 13, 18, 23];
173        for &p in &hyphen_positions {
174            if bytes[p] != b'-' {
175                return Err(ParseError::InvalidGroup(p));
176            }
177        }
178        let mut out = [0u8; 16];
179        let mut hex_idx = 0;
180        let mut byte_idx = 0;
181        while hex_idx < 36 {
182            if hyphen_positions.contains(&hex_idx) {
183                hex_idx += 1;
184                continue;
185            }
186            let hi = hex_value(bytes[hex_idx]).ok_or(ParseError::InvalidChar(hex_idx))?;
187            let lo = hex_value(bytes[hex_idx + 1]).ok_or(ParseError::InvalidChar(hex_idx + 1))?;
188            out[byte_idx] = (hi << 4) | lo;
189            byte_idx += 1;
190            hex_idx += 2;
191        }
192        Ok(Self(out))
193    }
194}
195
196impl Default for Uuid {
197    fn default() -> Self {
198        Self::nil()
199    }
200}
201
202impl fmt::Display for Uuid {
203    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204        let b = &self.0;
205        write!(
206            f,
207            "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
208            b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
209            b[8], b[9], b[10], b[11], b[12], b[13], b[14], b[15]
210        )
211    }
212}
213
214impl core::str::FromStr for Uuid {
215    type Err = ParseError;
216    fn from_str(s: &str) -> Result<Self, Self::Err> {
217        Self::parse_str(s)
218    }
219}
220
221/// Error returned by [`Uuid::parse_str`].
222#[derive(Debug, Clone, Copy, PartialEq, Eq)]
223pub enum ParseError {
224    /// Input was not exactly 36 characters. The value is the actual length.
225    InvalidLength(usize),
226    /// A hyphen was missing at the given byte position (expected 8, 13, 18, or 23).
227    InvalidGroup(usize),
228    /// A non-hex digit was found at the given byte position.
229    InvalidChar(usize),
230}
231
232impl fmt::Display for ParseError {
233    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
234        match self {
235            Self::InvalidLength(n) => write!(f, "expected 36 characters, got {n}"),
236            Self::InvalidGroup(p) => write!(f, "expected hyphen at position {p}"),
237            Self::InvalidChar(p) => write!(f, "invalid hex digit at position {p}"),
238        }
239    }
240}
241
242impl std::error::Error for ParseError {}
243
244#[inline]
245const fn hex_value(c: u8) -> Option<u8> {
246    match c {
247        b'0'..=b'9' => Some(c - b'0'),
248        b'a'..=b'f' => Some(c - b'a' + 10),
249        b'A'..=b'F' => Some(c - b'A' + 10),
250        _ => None,
251    }
252}
253
254// -------- Inline xoshiro256** --------
255
256thread_local! {
257    static RNG: RefCell<Xoshiro256SS> = RefCell::new(Xoshiro256SS::from_entropy());
258}
259
260fn random_bytes_16() -> [u8; 16] {
261    RNG.with(|cell| {
262        let mut r = cell.borrow_mut();
263        let a = r.next_u64();
264        let b = r.next_u64();
265        let mut out = [0u8; 16];
266        out[0..8].copy_from_slice(&a.to_be_bytes());
267        out[8..16].copy_from_slice(&b.to_be_bytes());
268        out
269    })
270}
271
272struct Xoshiro256SS {
273    s: [u64; 4],
274}
275
276impl Xoshiro256SS {
277    fn from_entropy() -> Self {
278        static SEED_COUNTER: AtomicU64 = AtomicU64::new(0);
279        let pid = std::process::id() as u64;
280        let nanos = SystemTime::now()
281            .duration_since(UNIX_EPOCH)
282            .map(|d| d.as_nanos() as u64)
283            .unwrap_or(0);
284        let counter = SEED_COUNTER.fetch_add(1, Ordering::Relaxed);
285        let seed = pid
286            .wrapping_mul(0x9E37_79B9_7F4A_7C15)
287            .wrapping_add(nanos)
288            .wrapping_add(counter.wrapping_mul(0xBF58_476D_1CE4_E5B9));
289        Self::from_seed(seed)
290    }
291
292    fn from_seed(mut seed: u64) -> Self {
293        let mut s = [0u64; 4];
294        for slot in &mut s {
295            seed = seed.wrapping_add(0x9E37_79B9_7F4A_7C15);
296            let mut z = seed;
297            z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
298            z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
299            *slot = z ^ (z >> 31);
300        }
301        if s == [0; 4] {
302            s[0] = 1;
303        }
304        Self { s }
305    }
306
307    #[inline]
308    fn next_u64(&mut self) -> u64 {
309        let result = self.s[1].wrapping_mul(5).rotate_left(7).wrapping_mul(9);
310        let t = self.s[1] << 17;
311        self.s[2] ^= self.s[0];
312        self.s[3] ^= self.s[1];
313        self.s[1] ^= self.s[2];
314        self.s[0] ^= self.s[3];
315        self.s[2] ^= t;
316        self.s[3] = self.s[3].rotate_left(45);
317        result
318    }
319}
320
321#[cfg(test)]
322mod tests {
323    use super::*;
324
325    #[test]
326    fn v4_version_and_variant() {
327        let id = Uuid::v4();
328        assert_eq!(id.version(), 4);
329        assert_eq!(id.0[8] & 0xc0, 0x80);
330    }
331
332    #[test]
333    fn v7_version_and_variant() {
334        let id = Uuid::v7();
335        assert_eq!(id.version(), 7);
336        assert_eq!(id.0[8] & 0xc0, 0x80);
337    }
338
339    #[test]
340    fn display_format_canonical() {
341        let id = Uuid::v4();
342        let s = id.to_string();
343        assert_eq!(s.len(), 36);
344        let hyphen_positions: Vec<usize> = s
345            .char_indices()
346            .filter_map(|(i, c)| if c == '-' { Some(i) } else { None })
347            .collect();
348        assert_eq!(hyphen_positions, vec![8, 13, 18, 23]);
349    }
350
351    #[test]
352    fn v4_pair_differs() {
353        assert_ne!(Uuid::v4(), Uuid::v4());
354    }
355
356    #[test]
357    fn v7_pair_differs() {
358        assert_ne!(Uuid::v7(), Uuid::v7());
359    }
360
361    #[test]
362    fn v7_time_ordered_across_ms() {
363        let a = Uuid::v7();
364        std::thread::sleep(std::time::Duration::from_millis(2));
365        let b = Uuid::v7();
366        assert!(b.as_bytes() > a.as_bytes());
367    }
368
369    #[test]
370    fn nil_and_max() {
371        assert_eq!(Uuid::nil().as_bytes(), &[0u8; 16]);
372        assert_eq!(Uuid::max().as_bytes(), &[0xffu8; 16]);
373        assert_eq!(
374            Uuid::nil().to_string(),
375            "00000000-0000-0000-0000-000000000000"
376        );
377        assert_eq!(
378            Uuid::max().to_string(),
379            "ffffffff-ffff-ffff-ffff-ffffffffffff"
380        );
381    }
382
383    #[test]
384    fn default_is_nil() {
385        assert_eq!(Uuid::default(), Uuid::nil());
386    }
387
388    #[test]
389    fn from_bytes_roundtrip() {
390        let id = Uuid::v4();
391        assert_eq!(Uuid::from_bytes(id.as_bytes()), id);
392    }
393
394    // RFC 9562 Appendix A.2 — v4 example.
395    #[test]
396    fn parse_rfc9562_v4_example() {
397        let s = "919108f7-52d1-4320-9bac-f847db4148a8";
398        let id = Uuid::parse_str(s).unwrap();
399        assert_eq!(id.version(), 4);
400        assert_eq!(id.0[8] & 0xc0, 0x80);
401        assert_eq!(id.to_string(), s);
402    }
403
404    // RFC 9562 Appendix A.6 — v7 example.
405    #[test]
406    fn parse_rfc9562_v7_example() {
407        let s = "017f22e2-79b0-7cc3-98c4-dc0c0c07398f";
408        let id = Uuid::parse_str(s).unwrap();
409        assert_eq!(id.version(), 7);
410        assert_eq!(id.0[8] & 0xc0, 0x80);
411        assert_eq!(id.to_string(), s);
412    }
413
414    #[test]
415    fn parse_uppercase() {
416        let id = Uuid::parse_str("F47AC10B-58CC-4372-A567-0E02B2C3D479").unwrap();
417        assert_eq!(id.to_string(), "f47ac10b-58cc-4372-a567-0e02b2c3d479");
418    }
419
420    #[test]
421    fn parse_nil() {
422        let id = Uuid::parse_str("00000000-0000-0000-0000-000000000000").unwrap();
423        assert_eq!(id, Uuid::nil());
424    }
425
426    #[test]
427    fn parse_rejects_short() {
428        assert!(matches!(
429            Uuid::parse_str("abc"),
430            Err(ParseError::InvalidLength(3))
431        ));
432    }
433
434    #[test]
435    fn parse_rejects_missing_hyphen() {
436        assert!(matches!(
437            Uuid::parse_str("f47ac10b_58cc-4372-a567-0e02b2c3d479"),
438            Err(ParseError::InvalidGroup(8))
439        ));
440    }
441
442    #[test]
443    fn parse_rejects_bad_hex() {
444        assert!(matches!(
445            Uuid::parse_str("g47ac10b-58cc-4372-a567-0e02b2c3d479"),
446            Err(ParseError::InvalidChar(0))
447        ));
448    }
449
450    #[test]
451    fn from_str_works() {
452        let id: Uuid = "f47ac10b-58cc-4372-a567-0e02b2c3d479".parse().unwrap();
453        assert_eq!(id.to_string(), "f47ac10b-58cc-4372-a567-0e02b2c3d479");
454    }
455
456    #[test]
457    fn xoshiro_seeded_is_nonzero_state() {
458        let mut r = Xoshiro256SS::from_seed(0);
459        let a = r.next_u64();
460        let b = r.next_u64();
461        assert_ne!(a, 0);
462        assert_ne!(a, b);
463    }
464
465    #[test]
466    fn many_v4_unique() {
467        use std::collections::HashSet;
468        let mut set = HashSet::new();
469        for _ in 0..10_000 {
470            assert!(set.insert(Uuid::v4()));
471        }
472    }
473}