1use core::fmt;
27use std::time::{SystemTime, UNIX_EPOCH};
28
29use crate::rng;
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
44pub struct Uuid([u8; 16]);
45
46impl Uuid {
47 pub const fn nil() -> Self {
57 Self([0u8; 16])
58 }
59
60 pub const fn max() -> Self {
70 Self([0xff; 16])
71 }
72
73 pub fn v4() -> Self {
87 let mut bytes = rng::next_bytes_16();
88 bytes[6] = (bytes[6] & 0x0f) | 0x40;
89 bytes[8] = (bytes[8] & 0x3f) | 0x80;
90 Self(bytes)
91 }
92
93 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 = rng::next_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 pub const fn from_bytes(bytes: &[u8; 16]) -> Self {
137 Self(*bytes)
138 }
139
140 pub const fn as_bytes(&self) -> &[u8; 16] {
151 &self.0
152 }
153
154 pub const fn version(&self) -> u8 {
168 self.0[6] >> 4
169 }
170
171 pub fn parse_str(input: &str) -> Result<Self, ParseError> {
187 let bytes = input.as_bytes();
188 if bytes.len() != 36 {
189 return Err(ParseError::InvalidLength(bytes.len()));
190 }
191 let hyphen_positions = [8usize, 13, 18, 23];
192 for &p in &hyphen_positions {
193 if bytes[p] != b'-' {
194 return Err(ParseError::InvalidGroup(p));
195 }
196 }
197 let mut out = [0u8; 16];
198 let mut hex_idx = 0;
199 let mut byte_idx = 0;
200 while hex_idx < 36 {
201 if hyphen_positions.contains(&hex_idx) {
202 hex_idx += 1;
203 continue;
204 }
205 let hi = hex_value(bytes[hex_idx]).ok_or(ParseError::InvalidChar(hex_idx))?;
206 let lo = hex_value(bytes[hex_idx + 1]).ok_or(ParseError::InvalidChar(hex_idx + 1))?;
207 out[byte_idx] = (hi << 4) | lo;
208 byte_idx += 1;
209 hex_idx += 2;
210 }
211 Ok(Self(out))
212 }
213}
214
215impl Default for Uuid {
216 fn default() -> Self {
217 Self::nil()
218 }
219}
220
221impl fmt::Display for Uuid {
222 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223 let b = &self.0;
224 write!(
225 f,
226 "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
227 b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
228 b[8], b[9], b[10], b[11], b[12], b[13], b[14], b[15]
229 )
230 }
231}
232
233impl core::str::FromStr for Uuid {
234 type Err = ParseError;
235 fn from_str(s: &str) -> Result<Self, Self::Err> {
236 Self::parse_str(s)
237 }
238}
239
240#[derive(Debug, Clone, Copy, PartialEq, Eq)]
242pub enum ParseError {
243 InvalidLength(usize),
245 InvalidGroup(usize),
247 InvalidChar(usize),
249}
250
251impl fmt::Display for ParseError {
252 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
253 match self {
254 Self::InvalidLength(n) => write!(f, "expected 36 characters, got {n}"),
255 Self::InvalidGroup(p) => write!(f, "expected hyphen at position {p}"),
256 Self::InvalidChar(p) => write!(f, "invalid hex digit at position {p}"),
257 }
258 }
259}
260
261impl std::error::Error for ParseError {}
262
263#[inline]
264const fn hex_value(c: u8) -> Option<u8> {
265 match c {
266 b'0'..=b'9' => Some(c - b'0'),
267 b'a'..=b'f' => Some(c - b'a' + 10),
268 b'A'..=b'F' => Some(c - b'A' + 10),
269 _ => None,
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276
277 #[test]
278 fn v4_version_and_variant() {
279 let id = Uuid::v4();
280 assert_eq!(id.version(), 4);
281 assert_eq!(id.0[8] & 0xc0, 0x80);
282 }
283
284 #[test]
285 fn v7_version_and_variant() {
286 let id = Uuid::v7();
287 assert_eq!(id.version(), 7);
288 assert_eq!(id.0[8] & 0xc0, 0x80);
289 }
290
291 #[test]
292 fn display_format_canonical() {
293 let id = Uuid::v4();
294 let s = id.to_string();
295 assert_eq!(s.len(), 36);
296 let hyphen_positions: Vec<usize> = s
297 .char_indices()
298 .filter_map(|(i, c)| if c == '-' { Some(i) } else { None })
299 .collect();
300 assert_eq!(hyphen_positions, vec![8, 13, 18, 23]);
301 }
302
303 #[test]
304 fn v4_pair_differs() {
305 assert_ne!(Uuid::v4(), Uuid::v4());
306 }
307
308 #[test]
309 fn v7_pair_differs() {
310 assert_ne!(Uuid::v7(), Uuid::v7());
311 }
312
313 #[test]
314 fn v7_time_ordered_across_ms() {
315 let a = Uuid::v7();
316 std::thread::sleep(std::time::Duration::from_millis(2));
317 let b = Uuid::v7();
318 assert!(b.as_bytes() > a.as_bytes());
319 }
320
321 #[test]
322 fn nil_and_max() {
323 assert_eq!(Uuid::nil().as_bytes(), &[0u8; 16]);
324 assert_eq!(Uuid::max().as_bytes(), &[0xffu8; 16]);
325 assert_eq!(
326 Uuid::nil().to_string(),
327 "00000000-0000-0000-0000-000000000000"
328 );
329 assert_eq!(
330 Uuid::max().to_string(),
331 "ffffffff-ffff-ffff-ffff-ffffffffffff"
332 );
333 }
334
335 #[test]
336 fn default_is_nil() {
337 assert_eq!(Uuid::default(), Uuid::nil());
338 }
339
340 #[test]
341 fn from_bytes_roundtrip() {
342 let id = Uuid::v4();
343 assert_eq!(Uuid::from_bytes(id.as_bytes()), id);
344 }
345
346 #[test]
348 fn parse_rfc9562_v4_example() {
349 let s = "919108f7-52d1-4320-9bac-f847db4148a8";
350 let id = Uuid::parse_str(s).unwrap();
351 assert_eq!(id.version(), 4);
352 assert_eq!(id.0[8] & 0xc0, 0x80);
353 assert_eq!(id.to_string(), s);
354 }
355
356 #[test]
358 fn parse_rfc9562_v7_example() {
359 let s = "017f22e2-79b0-7cc3-98c4-dc0c0c07398f";
360 let id = Uuid::parse_str(s).unwrap();
361 assert_eq!(id.version(), 7);
362 assert_eq!(id.0[8] & 0xc0, 0x80);
363 assert_eq!(id.to_string(), s);
364 }
365
366 #[test]
367 fn parse_uppercase() {
368 let id = Uuid::parse_str("F47AC10B-58CC-4372-A567-0E02B2C3D479").unwrap();
369 assert_eq!(id.to_string(), "f47ac10b-58cc-4372-a567-0e02b2c3d479");
370 }
371
372 #[test]
373 fn parse_nil() {
374 let id = Uuid::parse_str("00000000-0000-0000-0000-000000000000").unwrap();
375 assert_eq!(id, Uuid::nil());
376 }
377
378 #[test]
379 fn parse_rejects_short() {
380 assert!(matches!(
381 Uuid::parse_str("abc"),
382 Err(ParseError::InvalidLength(3))
383 ));
384 }
385
386 #[test]
387 fn parse_rejects_missing_hyphen() {
388 assert!(matches!(
389 Uuid::parse_str("f47ac10b_58cc-4372-a567-0e02b2c3d479"),
390 Err(ParseError::InvalidGroup(8))
391 ));
392 }
393
394 #[test]
395 fn parse_rejects_bad_hex() {
396 assert!(matches!(
397 Uuid::parse_str("g47ac10b-58cc-4372-a567-0e02b2c3d479"),
398 Err(ParseError::InvalidChar(0))
399 ));
400 }
401
402 #[test]
403 fn from_str_works() {
404 let id: Uuid = "f47ac10b-58cc-4372-a567-0e02b2c3d479".parse().unwrap();
405 assert_eq!(id.to_string(), "f47ac10b-58cc-4372-a567-0e02b2c3d479");
406 }
407
408 #[test]
409 fn many_v4_unique() {
410 use std::collections::HashSet;
411 let mut set = HashSet::new();
412 for _ in 0..10_000 {
413 assert!(set.insert(Uuid::v4()));
414 }
415 }
416}