Skip to main content

ps_uuid/implementations/
from_str.rs

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    /// Accept every standard UUID spelling:
11    ///   - canonical 36-byte form           `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`
12    ///   - 32 hex digits without hyphens    `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`
13    ///   - surrounded by braces             `{…}`  (either of the above)
14    ///   - as an URN                        `urn:uuid:<canonical>`
15    fn from_str(mut s: &str) -> Result<Self, Self::Err> {
16        // 1. Strip leading `urn:uuid:` (case-insensitive).
17        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        // 2. Strip optional surrounding braces.
23        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        // 3. Decide expected format.
33        let expect_hyphens = match s.len() {
34            32 => false,
35            36 => true,
36            _ => return Err(UuidParseError::InvalidLength),
37        };
38
39        // 4. Prepare to collect the 32 hexadecimal nibbles.
40        let mut nibbles = [0u8; 32]; // 32 * 4 bit = 128 bit
41        let mut nib_i = 0;
42
43        for (idx, ch) in s.chars().enumerate() {
44            if ch == '-' {
45                // Hyphens allowed only in the canonical positions.
46                if !expect_hyphens || !HYPHEN_POS.contains(&idx) {
47                    return Err(UuidParseError::InvalidHyphenPlacement);
48                }
49                continue;
50            }
51
52            // Convert ASCII hex → value.
53            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        // 5. Pack nibbles into 16 bytes.
71        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    // Same sample used by RFC 4122.
87    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    // ---------------------------------------------------------------------
94    // Happy-path cases
95    // ---------------------------------------------------------------------
96
97    #[test]
98    fn parses_all_standard_encodings() {
99        let variants = [
100            // canonical
101            RFC_SAMPLE_CANON,
102            // no hyphens
103            "6ba7b8109dad11d180b400c04fd430c8",
104            // uppercase
105            "6BA7B810-9DAD-11D1-80B4-00C04FD430C8",
106            // braces
107            "{6ba7b810-9dad-11d1-80b4-00c04fd430c8}",
108            // braces without hyphens
109            "{6ba7b8109dad11d180b400c04fd430c8}",
110            // URN
111            "urn:uuid:6ba7b810-9dad-11d1-80b4-00c04fd430c8",
112            // URN with braces
113            "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    // ---------------------------------------------------------------------
126    // Error cases
127    // ---------------------------------------------------------------------
128
129    #[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"; // 'g'
137        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"; // hyphen missing at 18
146
147        assert_eq!(UUID::from_str(bad), Err(UuidParseError::InvalidLength));
148    }
149
150    // ---------------------------------------------------------------------
151    // Round-trip sanity
152    // ---------------------------------------------------------------------
153
154    #[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        // Assuming you have a `to_hyphenated_string()` or `Display` impl.
159        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    // ---------------------------------------------------------------------
165    // Happy-path: all standard encodings
166    // ---------------------------------------------------------------------
167
168    #[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    // ---------------------------------------------------------------------
232    // Edge cases: whitespace, empty, minimal/maximal values
233    // ---------------------------------------------------------------------
234
235    #[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    // ---------------------------------------------------------------------
267    // Error cases: length, hyphens, braces, invalid chars, overflow
268    // ---------------------------------------------------------------------
269
270    #[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) // because length is not 32 or 36
299        );
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"); // 'G' is not a hex digit
306        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    // ---------------------------------------------------------------------
377    // Pathological: all hyphens, all braces, all colons, etc.
378    // ---------------------------------------------------------------------
379
380    #[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    // ---------------------------------------------------------------------
405    // Round-trip and case-insensitivity
406    // ---------------------------------------------------------------------
407
408    #[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}