1#![allow(clippy::manual_range_contains)]
7
8use core::fmt;
9use core::str::FromStr;
10
11#[inline]
15pub fn is_lowercase_hex(value: &str) -> bool {
16 value
17 .bytes()
18 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
19}
20
21#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
23pub struct Id128([u8; 16]);
24
25impl Id128 {
26 pub const NIL: Self = Self([0; 16]);
28
29 #[inline]
31 pub const fn from_bytes(bytes: [u8; 16]) -> Self {
32 Self(bytes)
33 }
34
35 #[inline]
37 pub const fn as_bytes(&self) -> &[u8; 16] {
38 &self.0
39 }
40
41 #[inline]
43 pub const fn is_nil(&self) -> bool {
44 let b = &self.0;
45 let mut i = 0;
46 while i < 16 {
47 if b[i] != 0 {
48 return false;
49 }
50 i += 1;
51 }
52 true
53 }
54
55 #[inline]
57 pub const fn from_u128(v: u128) -> Self {
58 Self(v.to_be_bytes())
59 }
60
61 #[inline]
63 pub const fn to_u128(&self) -> u128 {
64 u128::from_be_bytes(self.0)
65 }
66}
67
68const HEX_CHARS: &[u8; 16] = b"0123456789abcdef";
69
70impl fmt::Display for Id128 {
71 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72 let b = &self.0;
73 let mut buf = [0u8; 36];
74 let mut pos = 0;
75
76 let groups: &[(usize, usize)] = &[(0, 4), (4, 6), (6, 8), (8, 10), (10, 16)];
78 for (gi, &(start, end)) in groups.iter().enumerate() {
79 if gi > 0 {
80 buf[pos] = b'-';
81 pos += 1;
82 }
83 for i in start..end {
84 buf[pos] = HEX_CHARS[(b[i] >> 4) as usize];
85 buf[pos + 1] = HEX_CHARS[(b[i] & 0x0f) as usize];
86 pos += 2;
87 }
88 }
89 f.write_str(core::str::from_utf8(&buf[..pos]).expect("hex chars are valid utf8"))
90 }
91}
92
93impl fmt::Debug for Id128 {
94 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95 write!(f, "Id128({self})")
96 }
97}
98
99#[derive(Clone, Copy, Debug, PartialEq, Eq)]
101pub enum ParseIdError {
102 InvalidLength,
104 InvalidHex,
106}
107
108impl ParseIdError {
109 pub const INVALID_LENGTH_TEXT: &'static str = "expected UUID: 32 hex chars or 36 with hyphens";
116
117 pub const INVALID_HEX_TEXT: &'static str = "invalid hex character in UUID";
120}
121
122impl fmt::Display for ParseIdError {
123 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124 match self {
125 Self::InvalidLength => f.write_str(Self::INVALID_LENGTH_TEXT),
126 Self::InvalidHex => f.write_str(Self::INVALID_HEX_TEXT),
127 }
128 }
129}
130
131#[cfg(feature = "std")]
132impl std::error::Error for ParseIdError {}
133
134fn hex_val(c: u8) -> Option<u8> {
135 match c {
136 b'0'..=b'9' => Some(c - b'0'),
137 b'a'..=b'f' => Some(c - b'a' + 10),
138 b'A'..=b'F' => Some(c - b'A' + 10),
139 _ => None,
140 }
141}
142
143fn parse_hex_bytes(hex: &[u8]) -> Result<[u8; 16], ParseIdError> {
144 if hex.len() != 32 {
145 return Err(ParseIdError::InvalidLength);
146 }
147 let mut bytes = [0u8; 16];
148 for i in 0..16 {
149 let hi = hex_val(hex[i * 2]).ok_or(ParseIdError::InvalidHex)?;
150 let lo = hex_val(hex[i * 2 + 1]).ok_or(ParseIdError::InvalidHex)?;
151 bytes[i] = (hi << 4) | lo;
152 }
153 Ok(bytes)
154}
155
156impl FromStr for Id128 {
157 type Err = ParseIdError;
158
159 fn from_str(s: &str) -> Result<Self, Self::Err> {
160 let b = s.as_bytes();
161 match b.len() {
162 32 => Ok(Self(parse_hex_bytes(b)?)),
163 36 => {
164 if b[8] != b'-' || b[13] != b'-' || b[18] != b'-' || b[23] != b'-' {
166 return Err(ParseIdError::InvalidHex);
167 }
168 let mut hex = [0u8; 32];
169 hex[..8].copy_from_slice(&b[..8]);
170 hex[8..12].copy_from_slice(&b[9..13]);
171 hex[12..16].copy_from_slice(&b[14..18]);
172 hex[16..20].copy_from_slice(&b[19..23]);
173 hex[20..32].copy_from_slice(&b[24..36]);
174 Ok(Self(parse_hex_bytes(&hex)?))
175 }
176 _ => Err(ParseIdError::InvalidLength),
177 }
178 }
179}
180
181impl Default for Id128 {
182 #[inline]
183 fn default() -> Self {
184 Self::NIL
185 }
186}
187
188#[cfg(feature = "serde")]
189impl serde::Serialize for Id128 {
190 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
191 use alloc::string::ToString;
192 serializer.serialize_str(&self.to_string())
193 }
194}
195
196#[cfg(feature = "serde")]
197impl<'de> serde::Deserialize<'de> for Id128 {
198 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
199 let s = alloc::string::String::deserialize(deserializer)?;
202 s.parse().map_err(serde::de::Error::custom)
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209 use alloc::format;
210 use alloc::string::ToString;
211
212 #[test]
213 fn nil() {
214 assert!(Id128::NIL.is_nil());
215 assert!(!Id128::from_u128(1).is_nil());
216 }
217
218 #[test]
219 fn roundtrip_u128() {
220 let v: u128 = 0xdeadbeef_12345678_9abcdef0_11223344;
221 let id = Id128::from_u128(v);
222 assert_eq!(id.to_u128(), v);
223 }
224
225 #[test]
226 fn display_is_hyphenated_uuid() {
227 let id = Id128::from_u128(0xabcdef0123456789abcdef0123456789);
228 let s = format!("{id}");
229 assert_eq!(s.len(), 36);
230 assert_eq!(s, "abcdef01-2345-6789-abcd-ef0123456789");
231 }
232
233 #[test]
234 fn parse_hyphenated() {
235 let id: Id128 = "abcdef01-2345-6789-abcd-ef0123456789".parse().unwrap();
236 assert_eq!(id.to_u128(), 0xabcdef0123456789abcdef0123456789);
237 }
238
239 #[test]
240 fn parse_simple() {
241 let id: Id128 = "abcdef0123456789abcdef0123456789".parse().unwrap();
242 assert_eq!(id.to_u128(), 0xabcdef0123456789abcdef0123456789);
243 }
244
245 #[test]
246 fn display_parse_roundtrip() {
247 let id = Id128::from_u128(0xabcdef0123456789abcdef0123456789);
248 let s = format!("{id}");
249 let parsed: Id128 = s.parse().unwrap();
250 assert_eq!(parsed, id);
251 }
252
253 #[test]
254 fn parse_errors() {
255 assert_eq!("abc".parse::<Id128>(), Err(ParseIdError::InvalidLength));
256 assert_eq!(
257 "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz".parse::<Id128>(),
258 Err(ParseIdError::InvalidHex)
259 );
260 assert_eq!(
262 "abcdef01-2345-6789-abcd-ef012345678".parse::<Id128>(),
263 Err(ParseIdError::InvalidLength)
264 );
265 }
266
267 #[test]
272 fn parse_error_display_is_pinned() {
273 assert_eq!(
274 ParseIdError::InvalidLength.to_string(),
275 ParseIdError::INVALID_LENGTH_TEXT
276 );
277 assert_eq!(
278 ParseIdError::InvalidHex.to_string(),
279 ParseIdError::INVALID_HEX_TEXT
280 );
281 }
282
283 #[test]
284 fn ordering() {
285 let a = Id128::from_u128(1);
286 let b = Id128::from_u128(2);
287 assert!(a < b);
288 }
289
290 #[test]
291 fn lowercase_hex_accepts_digits_and_a_through_f_only() {
292 assert!(is_lowercase_hex("0123456789abcdef"));
293 assert!(is_lowercase_hex(""));
294 assert!(!is_lowercase_hex("abcdeF"));
295 assert!(!is_lowercase_hex("abcdeg"));
296 assert!(!is_lowercase_hex("ab cd"));
297 assert!(!is_lowercase_hex("0x12"));
298 assert!(!is_lowercase_hex("ab\u{e9}"));
299 }
300
301 #[cfg(feature = "serde")]
305 #[test]
306 fn deserialize_from_owned_value() {
307 use alloc::string::ToString;
308 let uuid_str = "abcdef01-2345-6789-abcd-ef0123456789";
309 let val = serde_json::Value::String(uuid_str.to_string());
311 let id: Id128 = serde_json::from_value(val).expect("Id128 must deserialize from Value");
312 assert_eq!(format!("{id}"), uuid_str);
313 }
314}