1use std::str::FromStr;
2
3use crate::{error::UuidParseError, UUID};
4
5const HYPHEN_POS: [usize; 4] = [8, 13, 18, 23];
6
7impl FromStr for UUID {
8 type Err = UuidParseError;
9
10 fn from_str(mut s: &str) -> Result<Self, Self::Err> {
16 const URN: &str = "urn:uuid:";
18 if s.len() >= URN.len() && s[..URN.len()].eq_ignore_ascii_case(URN) {
19 s = &s[URN.len()..];
20 }
21
22 if s.starts_with('{') {
24 if !s.ends_with('}') {
25 return Err(UuidParseError::InvalidBraces);
26 }
27 s = &s[1..s.len() - 1];
28 } else if s.ends_with('}') {
29 return Err(UuidParseError::InvalidBraces);
30 }
31
32 let expect_hyphens = match s.len() {
34 32 => false,
35 36 => true,
36 _ => return Err(UuidParseError::InvalidLength),
37 };
38
39 let mut nibbles = [0u8; 32]; let mut nib_i = 0;
42
43 for (idx, ch) in s.chars().enumerate() {
44 if ch == '-' {
45 if !expect_hyphens || !HYPHEN_POS.contains(&idx) {
47 return Err(UuidParseError::InvalidHyphenPlacement);
48 }
49 continue;
50 }
51
52 let val = match ch {
54 '0'..='9' => ch as u8 - b'0',
55 'a'..='f' => ch as u8 - b'a' + 10,
56 'A'..='F' => ch as u8 - b'A' + 10,
57 _ => return Err(UuidParseError::InvalidCharacter { ch, idx }),
58 };
59 if nib_i >= 32 {
60 return Err(UuidParseError::InvalidLength);
61 }
62 nibbles[nib_i] = val;
63 nib_i += 1;
64 }
65
66 if nib_i != 32 {
67 return Err(UuidParseError::InvalidLength);
68 }
69
70 let mut bytes = [0u8; 16];
72 for i in 0..16 {
73 bytes[i] = (nibbles[2 * i] << 4) | nibbles[2 * i + 1];
74 }
75
76 Ok(Self { bytes })
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 #![allow(clippy::expect_used)]
83 use super::*;
84 use core::str::FromStr;
85
86 const RFC_SAMPLE_CANON: &str = "6ba7b810-9dad-11d1-80b4-00c04fd430c8";
88 const RFC_SAMPLE_BYTES: [u8; 16] = [
89 0x6b, 0xa7, 0xb8, 0x10, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30,
90 0xc8,
91 ];
92
93 #[test]
98 fn parses_all_standard_encodings() {
99 let variants = [
100 RFC_SAMPLE_CANON,
102 "6ba7b8109dad11d180b400c04fd430c8",
104 "6BA7B810-9DAD-11D1-80B4-00C04FD430C8",
106 "{6ba7b810-9dad-11d1-80b4-00c04fd430c8}",
108 "{6ba7b8109dad11d180b400c04fd430c8}",
110 "urn:uuid:6ba7b810-9dad-11d1-80b4-00c04fd430c8",
112 "URN:UUID:{6BA7B810-9DAD-11D1-80B4-00C04FD430C8}",
114 ];
115
116 for s in variants {
117 let uuid = UUID::from_str(s).expect("must parse");
118 assert_eq!(
119 uuid.bytes, RFC_SAMPLE_BYTES,
120 "parsing failed for variant: {s}"
121 );
122 }
123 }
124
125 #[test]
130 fn rejects_wrong_length() {
131 assert_eq!(UUID::from_str("123456"), Err(UuidParseError::InvalidLength));
132 }
133
134 #[test]
135 fn rejects_invalid_hex() {
136 let bad = "6ba7b810-9dad-11d1-80b4-00c04fd430cg"; match UUID::from_str(bad) {
138 Err(UuidParseError::InvalidCharacter { ch: 'g', idx }) => assert_eq!(idx, 35),
139 other => panic!("unexpected result: {other:?}"),
140 }
141 }
142
143 #[test]
144 fn rejects_bad_hyphen_positions() {
145 let bad = "6ba7b810-9dad11d1-80b4-00c04fd430c8"; assert_eq!(UUID::from_str(bad), Err(UuidParseError::InvalidLength));
148 }
149
150 #[test]
155 fn round_trip_hyphenated() {
156 let uuid =
157 UUID::from_str(RFC_SAMPLE_CANON).expect("failed to parse UUID in positive test case");
158 let s = format!("{uuid}");
160 let again = UUID::from_str(&s).expect("failed to parse UUID in positive test case");
161 assert_eq!(uuid.bytes, again.bytes);
162 }
163
164 #[test]
169 fn parses_canonical() {
170 let uuid =
171 UUID::from_str(RFC_SAMPLE_CANON).expect("failed to parse UUID in positive test case");
172 assert_eq!(uuid.bytes, RFC_SAMPLE_BYTES);
173 }
174
175 #[test]
176 fn parses_no_hyphens() {
177 let uuid = UUID::from_str("6ba7b8109dad11d180b400c04fd430c8")
178 .expect("failed to parse UUID in positive test case");
179 assert_eq!(uuid.bytes, RFC_SAMPLE_BYTES);
180 }
181
182 #[test]
183 fn parses_uppercase() {
184 let uuid = UUID::from_str("6BA7B810-9DAD-11D1-80B4-00C04FD430C8")
185 .expect("failed to parse UUID in positive test case");
186 assert_eq!(uuid.bytes, RFC_SAMPLE_BYTES);
187 }
188
189 #[test]
190 fn parses_braces_canonical() {
191 let uuid = UUID::from_str("{6ba7b810-9dad-11d1-80b4-00c04fd430c8}")
192 .expect("failed to parse UUID in positive test case");
193 assert_eq!(uuid.bytes, RFC_SAMPLE_BYTES);
194 }
195
196 #[test]
197 fn parses_braces_no_hyphens() {
198 let uuid = UUID::from_str("{6ba7b8109dad11d180b400c04fd430c8}")
199 .expect("failed to parse UUID in positive test case");
200 assert_eq!(uuid.bytes, RFC_SAMPLE_BYTES);
201 }
202
203 #[test]
204 fn parses_urn_canonical() {
205 let uuid = UUID::from_str("urn:uuid:6ba7b810-9dad-11d1-80b4-00c04fd430c8")
206 .expect("failed to parse UUID in positive test case");
207 assert_eq!(uuid.bytes, RFC_SAMPLE_BYTES);
208 }
209
210 #[test]
211 fn parses_urn_braces() {
212 let uuid = UUID::from_str("urn:uuid:{6ba7b810-9dad-11d1-80b4-00c04fd430c8}")
213 .expect("failed to parse UUID in positive test case");
214 assert_eq!(uuid.bytes, RFC_SAMPLE_BYTES);
215 }
216
217 #[test]
218 fn parses_urn_uppercase() {
219 let uuid = UUID::from_str("URN:UUID:6BA7B810-9DAD-11D1-80B4-00C04FD430C8")
220 .expect("failed to parse UUID in positive test case");
221 assert_eq!(uuid.bytes, RFC_SAMPLE_BYTES);
222 }
223
224 #[test]
225 fn parses_urn_braces_uppercase() {
226 let uuid = UUID::from_str("URN:UUID:{6BA7B810-9DAD-11D1-80B4-00C04FD430C8}")
227 .expect("failed to parse UUID in positive test case");
228 assert_eq!(uuid.bytes, RFC_SAMPLE_BYTES);
229 }
230
231 #[test]
236 fn rejects_leading_trailing_whitespace() {
237 assert_eq!(
238 UUID::from_str(" 6ba7b810-9dad-11d1-80b4-00c04fd430c8"),
239 Err(UuidParseError::InvalidLength)
240 );
241 assert_eq!(
242 UUID::from_str("6ba7b810-9dad-11d1-80b4-00c04fd430c8 "),
243 Err(UuidParseError::InvalidLength)
244 );
245 }
246
247 #[test]
248 fn rejects_empty_string() {
249 assert_eq!(UUID::from_str(""), Err(UuidParseError::InvalidLength));
250 }
251
252 #[test]
253 fn parses_all_zero_uuid() {
254 let uuid = UUID::from_str("00000000-0000-0000-0000-000000000000")
255 .expect("failed to parse UUID in positive test case");
256 assert_eq!(uuid.bytes, [0u8; 16]);
257 }
258
259 #[test]
260 fn parses_all_ff_uuid() {
261 let uuid = UUID::from_str("ffffffff-ffff-ffff-ffff-ffffffffffff")
262 .expect("failed to parse UUID in positive test case");
263 assert_eq!(uuid.bytes, [0xFFu8; 16]);
264 }
265
266 #[test]
271 fn rejects_too_short() {
272 assert_eq!(UUID::from_str("1234"), Err(UuidParseError::InvalidLength));
273 }
274
275 #[test]
276 fn rejects_too_long() {
277 let s = format!("{RFC_SAMPLE_CANON}00");
278 assert_eq!(UUID::from_str(&s), Err(UuidParseError::InvalidLength));
279 }
280
281 #[test]
282 fn rejects_missing_hyphens_in_canonical() {
283 let s = "6ba7b8109dad-11d1-80b4-00c04fd430c8";
284 assert_eq!(UUID::from_str(s), Err(UuidParseError::InvalidLength));
285 }
286
287 #[test]
288 fn rejects_extra_hyphens() {
289 let s = "6ba7b810--9dad-11d1-80b4-00c04fd430c8";
290 assert_eq!(UUID::from_str(s), Err(UuidParseError::InvalidLength));
291 }
292
293 #[test]
294 fn rejects_hyphens_in_no_hyphen_form() {
295 let s = "6ba7b8109dad11d1-80b4-00c04fd430c8";
296 assert_eq!(
297 UUID::from_str(s),
298 Err(UuidParseError::InvalidLength) );
300 }
301
302 #[test]
303 fn rejects_invalid_hex_digit() {
304 let mut bad = RFC_SAMPLE_CANON.to_string();
305 bad.replace_range(0..1, "G"); assert_eq!(
307 UUID::from_str(&bad),
308 Err(UuidParseError::InvalidCharacter { ch: 'G', idx: 0 })
309 );
310 }
311
312 #[test]
313 fn rejects_invalid_hex_digit_in_no_hyphen() {
314 let mut bad = "6ba7b8109dad11d180b400c04fd430c8".to_string();
315 bad.replace_range(31..32, "Z");
316 assert_eq!(
317 UUID::from_str(&bad),
318 Err(UuidParseError::InvalidCharacter { ch: 'Z', idx: 31 })
319 );
320 }
321
322 #[test]
323 fn rejects_mismatched_braces() {
324 assert_eq!(
325 UUID::from_str("{6ba7b810-9dad-11d1-80b4-00c04fd430c8"),
326 Err(UuidParseError::InvalidBraces)
327 );
328 assert_eq!(
329 UUID::from_str("6ba7b810-9dad-11d1-80b4-00c04fd430c8}"),
330 Err(UuidParseError::InvalidBraces)
331 );
332 assert_eq!(
333 UUID::from_str("{6ba7b810-9dad-11d1-80b4-00c04fd430c8}}"),
334 Err(UuidParseError::InvalidLength)
335 );
336 }
337
338 #[test]
339 fn rejects_double_braces() {
340 assert_eq!(
341 UUID::from_str("{{6ba7b810-9dad-11d1-80b4-00c04fd430c8}}"),
342 Err(UuidParseError::InvalidLength)
343 );
344 }
345
346 #[test]
347 fn rejects_urn_with_invalid_uuid() {
348 let s = "urn:uuid:6ba7b810-9dad-11d1-80b4-00c04fd4308Z";
349 assert_eq!(
350 UUID::from_str(s),
351 Err(UuidParseError::InvalidCharacter { ch: 'Z', idx: 35 })
352 );
353 }
354
355 #[test]
356 fn rejects_urn_with_braces_and_invalid_uuid() {
357 let s = "urn:uuid:{6ba7b810-9dad-11d1-80b4-00c04fd430cZ}";
358 assert_eq!(
359 UUID::from_str(s),
360 Err(UuidParseError::InvalidCharacter { ch: 'Z', idx: 35 })
361 );
362 }
363
364 #[test]
365 fn rejects_urn_with_mismatched_braces() {
366 let s = "urn:uuid:{6ba7b810-9dad-11d1-80b4-00c04fd430c8";
367 assert_eq!(UUID::from_str(s), Err(UuidParseError::InvalidBraces));
368 }
369
370 #[test]
371 fn rejects_urn_with_extra_characters() {
372 let s = "urn:uuid:6ba7b810-9dad-11d1-80b4-00c04fd430c8extra";
373 assert_eq!(UUID::from_str(s), Err(UuidParseError::InvalidLength));
374 }
375
376 #[test]
381 fn rejects_all_hyphens() {
382 let s = "------------------------------------";
383 assert_eq!(
384 UUID::from_str(s),
385 Err(UuidParseError::InvalidHyphenPlacement)
386 );
387 }
388
389 #[test]
390 fn rejects_all_braces() {
391 let s = "{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{";
392 assert_eq!(UUID::from_str(s), Err(UuidParseError::InvalidBraces));
393 }
394
395 #[test]
396 fn rejects_all_colons() {
397 let s = "::::::::::::::::::::::::::::::::::::";
398 assert_eq!(
399 UUID::from_str(s),
400 Err(UuidParseError::InvalidCharacter { ch: ':', idx: 0 })
401 );
402 }
403
404 #[test]
409 fn round_trip_canonical() {
410 let uuid =
411 UUID::from_str(RFC_SAMPLE_CANON).expect("failed to parse UUID in positive test case");
412 let s = format!("{uuid}");
413 let again = UUID::from_str(&s).expect("failed to parse UUID in positive test case");
414 assert_eq!(uuid.bytes, again.bytes);
415 }
416
417 #[test]
418 fn accepts_mixed_case() {
419 let s = "6Ba7B810-9dAD-11D1-80b4-00C04fD430C8";
420 let uuid = UUID::from_str(s).expect("failed to parse UUID in positive test case");
421 assert_eq!(uuid.bytes, RFC_SAMPLE_BYTES);
422 }
423
424 #[test]
425 fn accepts_urn_with_mixed_case_prefix() {
426 let s = "UrN:UuId:6ba7b810-9dad-11d1-80b4-00c04fd430c8";
427 let uuid = UUID::from_str(s).expect("failed to parse UUID in positive test case");
428 assert_eq!(uuid.bytes, RFC_SAMPLE_BYTES);
429 }
430}