Skip to main content

bwx/
uuid.rs

1use rand::RngCore as _;
2
3#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
4pub struct Uuid([u8; 16]);
5
6#[derive(Debug, PartialEq, Eq)]
7pub struct ParseUuidError;
8
9impl std::fmt::Display for ParseUuidError {
10    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
11        write!(f, "invalid uuid")
12    }
13}
14
15impl std::error::Error for ParseUuidError {}
16
17pub fn new_v4() -> Uuid {
18    let mut bytes = [0_u8; 16];
19    let mut rng = rand::rng();
20    rng.fill_bytes(&mut bytes);
21    // RFC 4122 version 4 and variant bits
22    bytes[6] = 0x40 | (bytes[6] & 0x0F);
23    bytes[8] = 0x80 | (bytes[8] & 0x3F);
24    Uuid(bytes)
25}
26
27impl Uuid {
28    pub fn as_bytes(&self) -> &[u8; 16] {
29        &self.0
30    }
31}
32
33impl std::fmt::Display for Uuid {
34    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35        let b = &self.0;
36        write!(
37            f,
38            "{:02x}{:02x}{:02x}{:02x}-\
39             {:02x}{:02x}-\
40             {:02x}{:02x}-\
41             {:02x}{:02x}-\
42             {:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
43            b[0],
44            b[1],
45            b[2],
46            b[3],
47            b[4],
48            b[5],
49            b[6],
50            b[7],
51            b[8],
52            b[9],
53            b[10],
54            b[11],
55            b[12],
56            b[13],
57            b[14],
58            b[15],
59        )
60    }
61}
62
63impl std::str::FromStr for Uuid {
64    type Err = ParseUuidError;
65
66    fn from_str(s: &str) -> Result<Self, Self::Err> {
67        let bytes = s.as_bytes();
68        if bytes.len() != 36 {
69            return Err(ParseUuidError);
70        }
71        // hyphen positions per RFC 4122 canonical form
72        if bytes[8] != b'-'
73            || bytes[13] != b'-'
74            || bytes[18] != b'-'
75            || bytes[23] != b'-'
76        {
77            return Err(ParseUuidError);
78        }
79        let hex_positions =
80            [0, 2, 4, 6, 9, 11, 14, 16, 19, 21, 24, 26, 28, 30, 32, 34];
81        let mut out = [0_u8; 16];
82        for (i, &pos) in hex_positions.iter().enumerate() {
83            let hi = from_hex(bytes[pos])?;
84            let lo = from_hex(bytes[pos + 1])?;
85            out[i] = (hi << 4) | lo;
86        }
87        Ok(Self(out))
88    }
89}
90
91fn from_hex(b: u8) -> Result<u8, ParseUuidError> {
92    match b {
93        b'0'..=b'9' => Ok(b - b'0'),
94        b'a'..=b'f' => Ok(b - b'a' + 10),
95        b'A'..=b'F' => Ok(b - b'A' + 10),
96        _ => Err(ParseUuidError),
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use std::str::FromStr as _;
104
105    #[test]
106    fn version_and_variant_bits() {
107        for _ in 0..64 {
108            let u = new_v4();
109            let b = u.as_bytes();
110            assert_eq!(b[6] & 0xF0, 0x40, "version nibble must be 4");
111            assert_eq!(b[8] & 0xC0, 0x80, "variant must be RFC 4122");
112        }
113    }
114
115    #[test]
116    fn round_trip() {
117        let u = new_v4();
118        let s = u.to_string();
119        let parsed = Uuid::from_str(&s).unwrap();
120        assert_eq!(u, parsed);
121        assert_eq!(s.len(), 36);
122    }
123
124    #[test]
125    fn pinned_format() {
126        let bytes = [
127            0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0x4c, 0xde, 0x8f, 0x01, 0x23,
128            0x45, 0x67, 0x89, 0xab, 0xcd,
129        ];
130        let u = Uuid(bytes);
131        assert_eq!(u.to_string(), "01234567-89ab-4cde-8f01-23456789abcd");
132        let parsed =
133            Uuid::from_str("01234567-89ab-4cde-8f01-23456789abcd").unwrap();
134        assert_eq!(parsed, u);
135    }
136
137    #[test]
138    fn rejects_malformed() {
139        assert!(Uuid::from_str("").is_err());
140        assert!(Uuid::from_str("not-a-uuid").is_err());
141        assert!(
142            Uuid::from_str("01234567-89ab-4cde-8f01-23456789abcdx").is_err()
143        );
144        assert!(
145            Uuid::from_str("01234567x89ab-4cde-8f01-23456789abcd").is_err()
146        );
147        assert!(
148            Uuid::from_str("0123456g-89ab-4cde-8f01-23456789abcd").is_err()
149        );
150    }
151
152    #[test]
153    fn accepts_uppercase_outputs_lowercase() {
154        let u =
155            Uuid::from_str("01234567-89AB-4CDE-8F01-23456789ABCD").unwrap();
156        assert_eq!(u.to_string(), "01234567-89ab-4cde-8f01-23456789abcd");
157    }
158}