spinoso_securerandom/
uuid.rs1use scolapasta_hex as hex;
8
9use crate::Error;
10
11const OCTETS: usize = 16;
19
20const 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 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 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 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 assert_eq!(&uuid[14..15], "4", "Invalid UUID version");
98
99 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 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 let clock_seq_hi_and_reserved = u8::from_str_radix(&uuid[14..16], 16).unwrap();
147
148 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}