use crate::utils;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ParseUuidStringError {
WrongLength(usize),
MissingHyphen(usize),
InvalidHexChar {
position: usize,
character: char,
},
}
impl std::fmt::Display for ParseUuidStringError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::WrongLength(len) => {
write!(f, "UUID string must be 36 characters, got {len}")
}
Self::MissingHyphen(pos) => {
write!(f, "missing hyphen at position {pos}")
}
Self::InvalidHexChar {
position,
character,
} => {
write!(
f,
"invalid hex character '{}' (U+{:04X}) at position {position}",
character, *character as u32
)
}
}
}
}
impl std::error::Error for ParseUuidStringError {}
const UUID_HYPHEN_POSITIONS: [usize; 4] = [8, 13, 18, 23];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Case {
Lower,
Upper,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct UuidLike(u128);
impl std::fmt::Debug for UuidLike {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.to_uuid_string(Case::Lower))
}
}
impl UuidLike {
pub fn as_u128(&self) -> u128 {
self.0
}
pub fn new(id: u128) -> Self {
Self(id)
}
pub fn to_uuid_string(&self, case: Case) -> String {
utils::u128_to_uuid_string(self.0, case)
}
pub fn parse_uuid_string(uuid_string: &str) -> Result<Self, ParseUuidStringError> {
let bytes = uuid_string.as_bytes();
if bytes.len() != 36 {
return Err(ParseUuidStringError::WrongLength(uuid_string.len()));
}
for &pos in &UUID_HYPHEN_POSITIONS {
if bytes.get(pos) != Some(&b'-') {
return Err(ParseUuidStringError::MissingHyphen(pos));
}
}
let mut iter = bytes
.iter()
.enumerate()
.filter(|(i, _)| !UUID_HYPHEN_POSITIONS.contains(i));
let mut id: u128 = 0;
for _ in 0..16 {
let (i1, b1) = iter
.next()
.ok_or(ParseUuidStringError::WrongLength(uuid_string.len()))?;
let (i2, b2) = iter
.next()
.ok_or(ParseUuidStringError::WrongLength(uuid_string.len()))?;
let Some(h) = utils::hex_char_to_nibble(*b1) else {
return Err(ParseUuidStringError::InvalidHexChar {
position: i1,
character: *b1 as char,
});
};
let Some(l) = utils::hex_char_to_nibble(*b2) else {
return Err(ParseUuidStringError::InvalidHexChar {
position: i2,
character: *b2 as char,
});
};
let byte = (h << 4) | l;
id = (id << 8) | (byte as u128);
}
if iter.next().is_some() {
return Err(ParseUuidStringError::WrongLength(uuid_string.len()));
}
Ok(Self(id))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_lowercase() {
let result = UuidLike::parse_uuid_string("ffffffff-ffff-ffff-ffff-ffffffffffff");
assert_eq!(result.expect("valid UUID").as_u128(), u128::MAX);
}
#[test]
fn parse_uppercase() {
let result = UuidLike::parse_uuid_string("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF");
assert_eq!(result.expect("valid UUID").as_u128(), u128::MAX);
}
#[test]
fn parse_mixed_case() {
let result = UuidLike::parse_uuid_string("AaBbCcDd-1234-5678-90aB-cDeF01234567");
assert!(result.is_ok());
}
#[test]
fn parse_all_zeros() {
let result = UuidLike::parse_uuid_string("00000000-0000-0000-0000-000000000000");
assert_eq!(result.expect("valid UUID").as_u128(), 0);
}
}