spinoso_securerandom/
uuid.rs

1//! Generator for Version 4 UUIDs.
2//!
3//! See [RFC 4122], Section 4.4.
4//!
5//! [RFC 4122]: https://tools.ietf.org/html/rfc4122#section-4.4
6
7use scolapasta_hex as hex;
8
9use crate::Error;
10
11/// The number of octets (bytes) in a UUID, as defined in [RFC4122].
12///
13/// According to RFC4122, a UUID consists of 16 octets (128 bits) and is
14/// represented as a hexadecimal string of 32 characters, typically separated by
15/// hyphens into five groups: 8-4-4-4-12.
16///
17/// [RFC4122]: https://tools.ietf.org/html/rfc4122#section-4.1
18const OCTETS: usize = 16;
19
20/// The length of an encoded UUID string, including hyphens, as defined in
21/// [RFC4122].
22///
23/// According to RFC4122, an encoded UUID string consists of 36 characters,
24/// which includes the hexadecimal digits and four hyphens in the format
25/// 8-4-4-4-12.
26///
27/// [RFC4122]: https://tools.ietf.org/html/rfc4122
28const ENCODED_LENGTH: usize = 36;
29
30#[inline]
31pub fn v4() -> Result<String, Error> {
32    let mut bytes = [0; OCTETS];
33    getrandom::fill(&mut bytes)?;
34
35    // Per RFC 4122, Section 4.4, set bits for version and `clock_seq_hi_and_reserved`.
36    bytes[6] = (bytes[6] & 0x0f) | 0x40;
37    bytes[8] = (bytes[8] & 0x3f) | 0x80;
38
39    let mut buf = String::new();
40    buf.try_reserve(ENCODED_LENGTH)?;
41
42    let mut iter = bytes.into_iter();
43    for byte in iter.by_ref().take(4) {
44        let escaped = hex::escape_byte(byte);
45        buf.push_str(escaped);
46    }
47    buf.push('-');
48    for byte in iter.by_ref().take(2) {
49        let escaped = hex::escape_byte(byte);
50        buf.push_str(escaped);
51    }
52    buf.push('-');
53    for byte in iter.by_ref().take(2) {
54        let escaped = hex::escape_byte(byte);
55        buf.push_str(escaped);
56    }
57    buf.push('-');
58    for byte in iter.by_ref().take(2) {
59        let escaped = hex::escape_byte(byte);
60        buf.push_str(escaped);
61    }
62    buf.push('-');
63    for byte in iter {
64        let escaped = hex::escape_byte(byte);
65        buf.push_str(escaped);
66    }
67    debug_assert_eq!(buf.len(), ENCODED_LENGTH, "UUID had unexpected length");
68    Ok(buf)
69}
70
71#[cfg(test)]
72mod tests {
73    use std::collections::HashSet;
74
75    use super::*;
76
77    // Number of iterations for UUID generation tests.
78    //
79    // Chosen to provide a high level of confidence in the correctness and
80    // uniqueness of the UUID generation function, striking a balance between
81    // test coverage and reasonable execution time. Can be adjusted based on
82    // specific application requirements.
83    const ITERATIONS: usize = 10240;
84
85    #[test]
86    fn test_v4_returns_valid_uuid() {
87        for _ in 0..ITERATIONS {
88            let uuid = v4().unwrap();
89            // Validate the UUID format
90            assert_eq!(uuid.len(), 36, "UUID length should be 36 characters");
91            assert_eq!(&uuid[8..9], "-", "Invalid UUID format");
92            assert_eq!(&uuid[13..14], "-", "Invalid UUID format");
93            assert_eq!(&uuid[18..19], "-", "Invalid UUID format");
94            assert_eq!(&uuid[23..24], "-", "Invalid UUID format");
95
96            // Validate that the UUID is version 4
97            assert_eq!(&uuid[14..15], "4", "Invalid UUID version");
98
99            // Validate that the non-hyphen positions are lowercase ASCII alphanumeric characters
100            for (idx, c) in uuid.chars().enumerate() {
101                if matches!(idx, 8 | 13 | 18 | 23) {
102                    assert_eq!(c, '-', "Expected hyphen at position {idx}");
103                } else {
104                    assert!(
105                        matches!(c, '0'..='9' | 'a'..='f'),
106                        "Character at position {idx} should match ASCII numeric and lowercase hex"
107                    );
108                }
109            }
110        }
111    }
112
113    #[test]
114    fn test_v4_generated_uuids_are_unique() {
115        let mut generated_uuids = HashSet::with_capacity(ITERATIONS);
116
117        for _ in 0..ITERATIONS {
118            let uuid = v4().unwrap();
119
120            // Ensure uniqueness of generated UUIDs
121            assert!(
122                generated_uuids.insert(uuid.clone()),
123                "Generated UUID is not unique: {uuid}"
124            );
125        }
126    }
127
128    #[test]
129    fn test_v4_generated_uuids_are_ascii_only() {
130        for _ in 0..ITERATIONS {
131            let uuid = v4().unwrap();
132            assert!(uuid.is_ascii(), "UUID should consist of only ASCII characters: {uuid}");
133        }
134    }
135
136    #[test]
137    fn test_v4_clock_seq_hi_and_reserved() {
138        for _ in 0..ITERATIONS {
139            let uuid = v4().unwrap();
140
141            // Extract the relevant portion of the generated UUID for comparison
142            //
143            // Per the RFC, `clock_seq_hi_and_reserved` is octet 8 (zero indexed).
144            // Additionally: the two most significant bits (bits 6 and 7) are set
145            // to zero and one, respectively.
146            let clock_seq_hi_and_reserved = u8::from_str_radix(&uuid[14..16], 16).unwrap();
147
148            // Assert that the two most significant bits are correct
149            assert_eq!(
150                clock_seq_hi_and_reserved & 0b1100_0000,
151                0b0100_0000,
152                "Incorrect clock_seq_hi_and_reserved bits in v4 UUID"
153            );
154        }
155    }
156}