use std::str::FromStr;
use crate::{error::UuidParseError, UUID};
const HYPHEN_POS: [usize; 4] = [8, 13, 18, 23];
impl FromStr for UUID {
type Err = UuidParseError;
fn from_str(mut s: &str) -> Result<Self, Self::Err> {
const URN: &str = "urn:uuid:";
if s.len() >= URN.len() && s[..URN.len()].eq_ignore_ascii_case(URN) {
s = &s[URN.len()..];
}
if s.starts_with('{') {
if !s.ends_with('}') {
return Err(UuidParseError::InvalidBraces);
}
s = &s[1..s.len() - 1];
} else if s.ends_with('}') {
return Err(UuidParseError::InvalidBraces);
}
let expect_hyphens = match s.len() {
32 => false,
36 => true,
_ => return Err(UuidParseError::InvalidLength),
};
let mut nibbles = [0u8; 32]; let mut nib_i = 0;
for (idx, ch) in s.chars().enumerate() {
if ch == '-' {
if !expect_hyphens || !HYPHEN_POS.contains(&idx) {
return Err(UuidParseError::InvalidHyphenPlacement);
}
continue;
}
let val = match ch {
'0'..='9' => ch as u8 - b'0',
'a'..='f' => ch as u8 - b'a' + 10,
'A'..='F' => ch as u8 - b'A' + 10,
_ => return Err(UuidParseError::InvalidCharacter { ch, idx }),
};
if nib_i >= 32 {
return Err(UuidParseError::InvalidLength);
}
nibbles[nib_i] = val;
nib_i += 1;
}
if nib_i != 32 {
return Err(UuidParseError::InvalidLength);
}
let mut bytes = [0u8; 16];
for i in 0..16 {
bytes[i] = (nibbles[2 * i] << 4) | nibbles[2 * i + 1];
}
Ok(Self { bytes })
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used)]
use super::*;
use core::str::FromStr;
const RFC_SAMPLE_CANON: &str = "6ba7b810-9dad-11d1-80b4-00c04fd430c8";
const RFC_SAMPLE_BYTES: [u8; 16] = [
0x6b, 0xa7, 0xb8, 0x10, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30,
0xc8,
];
#[test]
fn parses_all_standard_encodings() {
let variants = [
RFC_SAMPLE_CANON,
"6ba7b8109dad11d180b400c04fd430c8",
"6BA7B810-9DAD-11D1-80B4-00C04FD430C8",
"{6ba7b810-9dad-11d1-80b4-00c04fd430c8}",
"{6ba7b8109dad11d180b400c04fd430c8}",
"urn:uuid:6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"URN:UUID:{6BA7B810-9DAD-11D1-80B4-00C04FD430C8}",
];
for s in variants {
let uuid = UUID::from_str(s).expect("must parse");
assert_eq!(
uuid.bytes, RFC_SAMPLE_BYTES,
"parsing failed for variant: {s}"
);
}
}
#[test]
fn rejects_wrong_length() {
assert_eq!(UUID::from_str("123456"), Err(UuidParseError::InvalidLength));
}
#[test]
fn rejects_invalid_hex() {
let bad = "6ba7b810-9dad-11d1-80b4-00c04fd430cg"; match UUID::from_str(bad) {
Err(UuidParseError::InvalidCharacter { ch: 'g', idx }) => assert_eq!(idx, 35),
other => panic!("unexpected result: {other:?}"),
}
}
#[test]
fn rejects_bad_hyphen_positions() {
let bad = "6ba7b810-9dad11d1-80b4-00c04fd430c8";
assert_eq!(UUID::from_str(bad), Err(UuidParseError::InvalidLength));
}
#[test]
fn round_trip_hyphenated() {
let uuid =
UUID::from_str(RFC_SAMPLE_CANON).expect("failed to parse UUID in positive test case");
let s = format!("{uuid}");
let again = UUID::from_str(&s).expect("failed to parse UUID in positive test case");
assert_eq!(uuid.bytes, again.bytes);
}
#[test]
fn parses_canonical() {
let uuid =
UUID::from_str(RFC_SAMPLE_CANON).expect("failed to parse UUID in positive test case");
assert_eq!(uuid.bytes, RFC_SAMPLE_BYTES);
}
#[test]
fn parses_no_hyphens() {
let uuid = UUID::from_str("6ba7b8109dad11d180b400c04fd430c8")
.expect("failed to parse UUID in positive test case");
assert_eq!(uuid.bytes, RFC_SAMPLE_BYTES);
}
#[test]
fn parses_uppercase() {
let uuid = UUID::from_str("6BA7B810-9DAD-11D1-80B4-00C04FD430C8")
.expect("failed to parse UUID in positive test case");
assert_eq!(uuid.bytes, RFC_SAMPLE_BYTES);
}
#[test]
fn parses_braces_canonical() {
let uuid = UUID::from_str("{6ba7b810-9dad-11d1-80b4-00c04fd430c8}")
.expect("failed to parse UUID in positive test case");
assert_eq!(uuid.bytes, RFC_SAMPLE_BYTES);
}
#[test]
fn parses_braces_no_hyphens() {
let uuid = UUID::from_str("{6ba7b8109dad11d180b400c04fd430c8}")
.expect("failed to parse UUID in positive test case");
assert_eq!(uuid.bytes, RFC_SAMPLE_BYTES);
}
#[test]
fn parses_urn_canonical() {
let uuid = UUID::from_str("urn:uuid:6ba7b810-9dad-11d1-80b4-00c04fd430c8")
.expect("failed to parse UUID in positive test case");
assert_eq!(uuid.bytes, RFC_SAMPLE_BYTES);
}
#[test]
fn parses_urn_braces() {
let uuid = UUID::from_str("urn:uuid:{6ba7b810-9dad-11d1-80b4-00c04fd430c8}")
.expect("failed to parse UUID in positive test case");
assert_eq!(uuid.bytes, RFC_SAMPLE_BYTES);
}
#[test]
fn parses_urn_uppercase() {
let uuid = UUID::from_str("URN:UUID:6BA7B810-9DAD-11D1-80B4-00C04FD430C8")
.expect("failed to parse UUID in positive test case");
assert_eq!(uuid.bytes, RFC_SAMPLE_BYTES);
}
#[test]
fn parses_urn_braces_uppercase() {
let uuid = UUID::from_str("URN:UUID:{6BA7B810-9DAD-11D1-80B4-00C04FD430C8}")
.expect("failed to parse UUID in positive test case");
assert_eq!(uuid.bytes, RFC_SAMPLE_BYTES);
}
#[test]
fn rejects_leading_trailing_whitespace() {
assert_eq!(
UUID::from_str(" 6ba7b810-9dad-11d1-80b4-00c04fd430c8"),
Err(UuidParseError::InvalidLength)
);
assert_eq!(
UUID::from_str("6ba7b810-9dad-11d1-80b4-00c04fd430c8 "),
Err(UuidParseError::InvalidLength)
);
}
#[test]
fn rejects_empty_string() {
assert_eq!(UUID::from_str(""), Err(UuidParseError::InvalidLength));
}
#[test]
fn parses_all_zero_uuid() {
let uuid = UUID::from_str("00000000-0000-0000-0000-000000000000")
.expect("failed to parse UUID in positive test case");
assert_eq!(uuid.bytes, [0u8; 16]);
}
#[test]
fn parses_all_ff_uuid() {
let uuid = UUID::from_str("ffffffff-ffff-ffff-ffff-ffffffffffff")
.expect("failed to parse UUID in positive test case");
assert_eq!(uuid.bytes, [0xFFu8; 16]);
}
#[test]
fn rejects_too_short() {
assert_eq!(UUID::from_str("1234"), Err(UuidParseError::InvalidLength));
}
#[test]
fn rejects_too_long() {
let s = format!("{RFC_SAMPLE_CANON}00");
assert_eq!(UUID::from_str(&s), Err(UuidParseError::InvalidLength));
}
#[test]
fn rejects_missing_hyphens_in_canonical() {
let s = "6ba7b8109dad-11d1-80b4-00c04fd430c8";
assert_eq!(UUID::from_str(s), Err(UuidParseError::InvalidLength));
}
#[test]
fn rejects_extra_hyphens() {
let s = "6ba7b810--9dad-11d1-80b4-00c04fd430c8";
assert_eq!(UUID::from_str(s), Err(UuidParseError::InvalidLength));
}
#[test]
fn rejects_hyphens_in_no_hyphen_form() {
let s = "6ba7b8109dad11d1-80b4-00c04fd430c8";
assert_eq!(
UUID::from_str(s),
Err(UuidParseError::InvalidLength) );
}
#[test]
fn rejects_invalid_hex_digit() {
let mut bad = RFC_SAMPLE_CANON.to_string();
bad.replace_range(0..1, "G"); assert_eq!(
UUID::from_str(&bad),
Err(UuidParseError::InvalidCharacter { ch: 'G', idx: 0 })
);
}
#[test]
fn rejects_invalid_hex_digit_in_no_hyphen() {
let mut bad = "6ba7b8109dad11d180b400c04fd430c8".to_string();
bad.replace_range(31..32, "Z");
assert_eq!(
UUID::from_str(&bad),
Err(UuidParseError::InvalidCharacter { ch: 'Z', idx: 31 })
);
}
#[test]
fn rejects_mismatched_braces() {
assert_eq!(
UUID::from_str("{6ba7b810-9dad-11d1-80b4-00c04fd430c8"),
Err(UuidParseError::InvalidBraces)
);
assert_eq!(
UUID::from_str("6ba7b810-9dad-11d1-80b4-00c04fd430c8}"),
Err(UuidParseError::InvalidBraces)
);
assert_eq!(
UUID::from_str("{6ba7b810-9dad-11d1-80b4-00c04fd430c8}}"),
Err(UuidParseError::InvalidLength)
);
}
#[test]
fn rejects_double_braces() {
assert_eq!(
UUID::from_str("{{6ba7b810-9dad-11d1-80b4-00c04fd430c8}}"),
Err(UuidParseError::InvalidLength)
);
}
#[test]
fn rejects_urn_with_invalid_uuid() {
let s = "urn:uuid:6ba7b810-9dad-11d1-80b4-00c04fd4308Z";
assert_eq!(
UUID::from_str(s),
Err(UuidParseError::InvalidCharacter { ch: 'Z', idx: 35 })
);
}
#[test]
fn rejects_urn_with_braces_and_invalid_uuid() {
let s = "urn:uuid:{6ba7b810-9dad-11d1-80b4-00c04fd430cZ}";
assert_eq!(
UUID::from_str(s),
Err(UuidParseError::InvalidCharacter { ch: 'Z', idx: 35 })
);
}
#[test]
fn rejects_urn_with_mismatched_braces() {
let s = "urn:uuid:{6ba7b810-9dad-11d1-80b4-00c04fd430c8";
assert_eq!(UUID::from_str(s), Err(UuidParseError::InvalidBraces));
}
#[test]
fn rejects_urn_with_extra_characters() {
let s = "urn:uuid:6ba7b810-9dad-11d1-80b4-00c04fd430c8extra";
assert_eq!(UUID::from_str(s), Err(UuidParseError::InvalidLength));
}
#[test]
fn rejects_all_hyphens() {
let s = "------------------------------------";
assert_eq!(
UUID::from_str(s),
Err(UuidParseError::InvalidHyphenPlacement)
);
}
#[test]
fn rejects_all_braces() {
let s = "{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{";
assert_eq!(UUID::from_str(s), Err(UuidParseError::InvalidBraces));
}
#[test]
fn rejects_all_colons() {
let s = "::::::::::::::::::::::::::::::::::::";
assert_eq!(
UUID::from_str(s),
Err(UuidParseError::InvalidCharacter { ch: ':', idx: 0 })
);
}
#[test]
fn round_trip_canonical() {
let uuid =
UUID::from_str(RFC_SAMPLE_CANON).expect("failed to parse UUID in positive test case");
let s = format!("{uuid}");
let again = UUID::from_str(&s).expect("failed to parse UUID in positive test case");
assert_eq!(uuid.bytes, again.bytes);
}
#[test]
fn accepts_mixed_case() {
let s = "6Ba7B810-9dAD-11D1-80b4-00C04fD430C8";
let uuid = UUID::from_str(s).expect("failed to parse UUID in positive test case");
assert_eq!(uuid.bytes, RFC_SAMPLE_BYTES);
}
#[test]
fn accepts_urn_with_mixed_case_prefix() {
let s = "UrN:UuId:6ba7b810-9dad-11d1-80b4-00c04fd430c8";
let uuid = UUID::from_str(s).expect("failed to parse UUID in positive test case");
assert_eq!(uuid.bytes, RFC_SAMPLE_BYTES);
}
}