1use rand::RngCore as _;
2
3#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
4pub struct Uuid([u8; 16]);
5
6#[derive(Debug, PartialEq, Eq)]
7pub struct ParseUuidError;
8
9impl std::fmt::Display for ParseUuidError {
10 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
11 write!(f, "invalid uuid")
12 }
13}
14
15impl std::error::Error for ParseUuidError {}
16
17pub fn new_v4() -> Uuid {
18 let mut bytes = [0_u8; 16];
19 let mut rng = rand::rng();
20 rng.fill_bytes(&mut bytes);
21 bytes[6] = 0x40 | (bytes[6] & 0x0F);
23 bytes[8] = 0x80 | (bytes[8] & 0x3F);
24 Uuid(bytes)
25}
26
27impl Uuid {
28 pub fn as_bytes(&self) -> &[u8; 16] {
29 &self.0
30 }
31}
32
33impl std::fmt::Display for Uuid {
34 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35 let b = &self.0;
36 write!(
37 f,
38 "{:02x}{:02x}{:02x}{:02x}-\
39 {:02x}{:02x}-\
40 {:02x}{:02x}-\
41 {:02x}{:02x}-\
42 {:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
43 b[0],
44 b[1],
45 b[2],
46 b[3],
47 b[4],
48 b[5],
49 b[6],
50 b[7],
51 b[8],
52 b[9],
53 b[10],
54 b[11],
55 b[12],
56 b[13],
57 b[14],
58 b[15],
59 )
60 }
61}
62
63impl std::str::FromStr for Uuid {
64 type Err = ParseUuidError;
65
66 fn from_str(s: &str) -> Result<Self, Self::Err> {
67 let bytes = s.as_bytes();
68 if bytes.len() != 36 {
69 return Err(ParseUuidError);
70 }
71 if bytes[8] != b'-'
73 || bytes[13] != b'-'
74 || bytes[18] != b'-'
75 || bytes[23] != b'-'
76 {
77 return Err(ParseUuidError);
78 }
79 let hex_positions =
80 [0, 2, 4, 6, 9, 11, 14, 16, 19, 21, 24, 26, 28, 30, 32, 34];
81 let mut out = [0_u8; 16];
82 for (i, &pos) in hex_positions.iter().enumerate() {
83 let hi = from_hex(bytes[pos])?;
84 let lo = from_hex(bytes[pos + 1])?;
85 out[i] = (hi << 4) | lo;
86 }
87 Ok(Self(out))
88 }
89}
90
91fn from_hex(b: u8) -> Result<u8, ParseUuidError> {
92 match b {
93 b'0'..=b'9' => Ok(b - b'0'),
94 b'a'..=b'f' => Ok(b - b'a' + 10),
95 b'A'..=b'F' => Ok(b - b'A' + 10),
96 _ => Err(ParseUuidError),
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use std::str::FromStr as _;
104
105 #[test]
106 fn version_and_variant_bits() {
107 for _ in 0..64 {
108 let u = new_v4();
109 let b = u.as_bytes();
110 assert_eq!(b[6] & 0xF0, 0x40, "version nibble must be 4");
111 assert_eq!(b[8] & 0xC0, 0x80, "variant must be RFC 4122");
112 }
113 }
114
115 #[test]
116 fn round_trip() {
117 let u = new_v4();
118 let s = u.to_string();
119 let parsed = Uuid::from_str(&s).unwrap();
120 assert_eq!(u, parsed);
121 assert_eq!(s.len(), 36);
122 }
123
124 #[test]
125 fn pinned_format() {
126 let bytes = [
127 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0x4c, 0xde, 0x8f, 0x01, 0x23,
128 0x45, 0x67, 0x89, 0xab, 0xcd,
129 ];
130 let u = Uuid(bytes);
131 assert_eq!(u.to_string(), "01234567-89ab-4cde-8f01-23456789abcd");
132 let parsed =
133 Uuid::from_str("01234567-89ab-4cde-8f01-23456789abcd").unwrap();
134 assert_eq!(parsed, u);
135 }
136
137 #[test]
138 fn rejects_malformed() {
139 assert!(Uuid::from_str("").is_err());
140 assert!(Uuid::from_str("not-a-uuid").is_err());
141 assert!(
142 Uuid::from_str("01234567-89ab-4cde-8f01-23456789abcdx").is_err()
143 );
144 assert!(
145 Uuid::from_str("01234567x89ab-4cde-8f01-23456789abcd").is_err()
146 );
147 assert!(
148 Uuid::from_str("0123456g-89ab-4cde-8f01-23456789abcd").is_err()
149 );
150 }
151
152 #[test]
153 fn accepts_uppercase_outputs_lowercase() {
154 let u =
155 Uuid::from_str("01234567-89AB-4CDE-8F01-23456789ABCD").unwrap();
156 assert_eq!(u.to_string(), "01234567-89ab-4cde-8f01-23456789abcd");
157 }
158}