use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Uuid([u8; 16]);
impl Uuid {
pub fn v4() -> Self {
let mut bytes = random();
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
Self(bytes)
}
pub fn v7() -> Self {
Self(*rustclamp_core::Reference::new().as_bytes())
}
pub fn parse(text: &str) -> Option<Self> {
if text.len() != 36
|| [8, 13, 18, 23]
.iter()
.any(|&dash| text.as_bytes()[dash] != b'-')
{
return None;
}
let hex: String = text.chars().filter(|&c| c != '-').collect();
let mut bytes = [0; 16];
for (index, byte) in bytes.iter_mut().enumerate() {
*byte = u8::from_str_radix(hex.get(index * 2..index * 2 + 2)?, 16).ok()?;
}
Some(Self(bytes))
}
pub fn version(&self) -> u8 {
self.0[6] >> 4
}
pub fn as_bytes(&self) -> &[u8; 16] {
&self.0
}
}
impl fmt::Display for Uuid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (index, byte) in self.0.iter().enumerate() {
if matches!(index, 4 | 6 | 8 | 10) {
f.write_str("-")?;
}
write!(f, "{byte:02x}")?;
}
Ok(())
}
}
fn random() -> [u8; 16] {
let mut bytes = [0; 16];
std::fs::File::open("/dev/urandom")
.and_then(|mut source| std::io::Read::read_exact(&mut source, &mut bytes))
.expect("UUIDs need the operating system random source /dev/urandom");
bytes
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn v4_has_version_and_variant() {
let id = Uuid::v4();
assert_eq!(id.version(), 4);
assert_eq!(id.as_bytes()[8] >> 6, 0b10);
assert_ne!(id, Uuid::v4());
}
#[test]
fn v7_carries_the_time_and_stays_ordered() {
use std::time::{SystemTime, UNIX_EPOCH};
let before = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis() as u64;
let ids: Vec<Uuid> = (0..1000).map(|_| Uuid::v7()).collect();
assert!(ids.windows(2).all(|pair| pair[0] < pair[1]));
let mut millis = [0; 8];
millis[2..].copy_from_slice(&ids[0].as_bytes()[..6]);
assert!(u64::from_be_bytes(millis) >= before);
assert!(
ids.iter()
.all(|id| id.version() == 7 && id.as_bytes()[8] >> 6 == 0b10)
);
}
#[test]
fn parse_round_trips_and_rejects_junk() {
let text = "0192f1c2-3b4a-7c5d-8e6f-a1b2c3d4e5f6";
assert_eq!(Uuid::parse(text).unwrap().to_string(), text);
assert_eq!(Uuid::parse(&text.to_uppercase()).unwrap().to_string(), text);
assert_eq!(Uuid::parse("0192f1c2-3b4a-7c5d-8e6f-a1b2c3d4e5f"), None);
assert_eq!(Uuid::parse("0192f1c2x3b4a-7c5d-8e6f-a1b2c3d4e5f6"), None);
assert_eq!(Uuid::parse("zz92f1c2-3b4a-7c5d-8e6f-a1b2c3d4e5f6"), None);
}
}