Skip to main content

khive_types/
id.rs

1//! 128-bit identifier — universal ID primitive.
2//!
3//! Wire format: canonical hyphenated UUID (8-4-4-4-12).
4//! Parses both hyphenated (`xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`)
5//! and simple (`xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`) forms.
6//! Hashable, totally ordered, has a nil sentinel.
7
8#![allow(clippy::manual_range_contains)]
9
10use core::fmt;
11use core::str::FromStr;
12
13#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
14pub struct Id128([u8; 16]);
15
16impl Id128 {
17    pub const NIL: Self = Self([0; 16]);
18
19    #[inline]
20    pub const fn from_bytes(bytes: [u8; 16]) -> Self {
21        Self(bytes)
22    }
23
24    #[inline]
25    pub const fn as_bytes(&self) -> &[u8; 16] {
26        &self.0
27    }
28
29    #[inline]
30    pub const fn is_nil(&self) -> bool {
31        let b = &self.0;
32        let mut i = 0;
33        while i < 16 {
34            if b[i] != 0 {
35                return false;
36            }
37            i += 1;
38        }
39        true
40    }
41
42    #[inline]
43    pub const fn from_u128(v: u128) -> Self {
44        Self(v.to_be_bytes())
45    }
46
47    #[inline]
48    pub const fn to_u128(&self) -> u128 {
49        u128::from_be_bytes(self.0)
50    }
51}
52
53const HEX_CHARS: &[u8; 16] = b"0123456789abcdef";
54
55impl fmt::Display for Id128 {
56    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57        let b = &self.0;
58        let mut buf = [0u8; 36];
59        let mut pos = 0;
60
61        // Groups: 4 bytes, 2 bytes, 2 bytes, 2 bytes, 6 bytes
62        let groups: &[(usize, usize)] = &[(0, 4), (4, 6), (6, 8), (8, 10), (10, 16)];
63        for (gi, &(start, end)) in groups.iter().enumerate() {
64            if gi > 0 {
65                buf[pos] = b'-';
66                pos += 1;
67            }
68            for i in start..end {
69                buf[pos] = HEX_CHARS[(b[i] >> 4) as usize];
70                buf[pos + 1] = HEX_CHARS[(b[i] & 0x0f) as usize];
71                pos += 2;
72            }
73        }
74        f.write_str(core::str::from_utf8(&buf[..pos]).expect("hex chars are valid utf8"))
75    }
76}
77
78impl fmt::Debug for Id128 {
79    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80        write!(f, "Id128({self})")
81    }
82}
83
84#[derive(Clone, Copy, Debug, PartialEq, Eq)]
85pub enum ParseIdError {
86    InvalidLength,
87    InvalidHex,
88}
89
90impl fmt::Display for ParseIdError {
91    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        match self {
93            Self::InvalidLength => f.write_str("expected UUID: 32 hex chars or 36 with hyphens"),
94            Self::InvalidHex => f.write_str("invalid hex character in UUID"),
95        }
96    }
97}
98
99#[cfg(feature = "std")]
100impl std::error::Error for ParseIdError {}
101
102fn hex_val(c: u8) -> Option<u8> {
103    match c {
104        b'0'..=b'9' => Some(c - b'0'),
105        b'a'..=b'f' => Some(c - b'a' + 10),
106        b'A'..=b'F' => Some(c - b'A' + 10),
107        _ => None,
108    }
109}
110
111fn parse_hex_bytes(hex: &[u8]) -> Result<[u8; 16], ParseIdError> {
112    if hex.len() != 32 {
113        return Err(ParseIdError::InvalidLength);
114    }
115    let mut bytes = [0u8; 16];
116    for i in 0..16 {
117        let hi = hex_val(hex[i * 2]).ok_or(ParseIdError::InvalidHex)?;
118        let lo = hex_val(hex[i * 2 + 1]).ok_or(ParseIdError::InvalidHex)?;
119        bytes[i] = (hi << 4) | lo;
120    }
121    Ok(bytes)
122}
123
124impl FromStr for Id128 {
125    type Err = ParseIdError;
126
127    fn from_str(s: &str) -> Result<Self, Self::Err> {
128        let b = s.as_bytes();
129        match b.len() {
130            32 => Ok(Self(parse_hex_bytes(b)?)),
131            36 => {
132                // Strip hyphens at positions 8, 13, 18, 23
133                if b[8] != b'-' || b[13] != b'-' || b[18] != b'-' || b[23] != b'-' {
134                    return Err(ParseIdError::InvalidHex);
135                }
136                let mut hex = [0u8; 32];
137                hex[..8].copy_from_slice(&b[..8]);
138                hex[8..12].copy_from_slice(&b[9..13]);
139                hex[12..16].copy_from_slice(&b[14..18]);
140                hex[16..20].copy_from_slice(&b[19..23]);
141                hex[20..32].copy_from_slice(&b[24..36]);
142                Ok(Self(parse_hex_bytes(&hex)?))
143            }
144            _ => Err(ParseIdError::InvalidLength),
145        }
146    }
147}
148
149impl Default for Id128 {
150    #[inline]
151    fn default() -> Self {
152        Self::NIL
153    }
154}
155
156#[cfg(feature = "serde")]
157impl serde::Serialize for Id128 {
158    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
159        use alloc::string::ToString;
160        serializer.serialize_str(&self.to_string())
161    }
162}
163
164#[cfg(feature = "serde")]
165impl<'de> serde::Deserialize<'de> for Id128 {
166    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
167        // Deserialize into owned String so this works when the deserializer
168        // holds owned data (e.g. serde_json::Value) and cannot lend a &str.
169        let s = alloc::string::String::deserialize(deserializer)?;
170        s.parse().map_err(serde::de::Error::custom)
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177    use alloc::format;
178
179    #[test]
180    fn nil() {
181        assert!(Id128::NIL.is_nil());
182        assert!(!Id128::from_u128(1).is_nil());
183    }
184
185    #[test]
186    fn roundtrip_u128() {
187        let v: u128 = 0xdeadbeef_12345678_9abcdef0_11223344;
188        let id = Id128::from_u128(v);
189        assert_eq!(id.to_u128(), v);
190    }
191
192    #[test]
193    fn display_is_hyphenated_uuid() {
194        let id = Id128::from_u128(0xabcdef0123456789abcdef0123456789);
195        let s = format!("{id}");
196        assert_eq!(s.len(), 36);
197        assert_eq!(s, "abcdef01-2345-6789-abcd-ef0123456789");
198    }
199
200    #[test]
201    fn parse_hyphenated() {
202        let id: Id128 = "abcdef01-2345-6789-abcd-ef0123456789".parse().unwrap();
203        assert_eq!(id.to_u128(), 0xabcdef0123456789abcdef0123456789);
204    }
205
206    #[test]
207    fn parse_simple() {
208        let id: Id128 = "abcdef0123456789abcdef0123456789".parse().unwrap();
209        assert_eq!(id.to_u128(), 0xabcdef0123456789abcdef0123456789);
210    }
211
212    #[test]
213    fn display_parse_roundtrip() {
214        let id = Id128::from_u128(0xabcdef0123456789abcdef0123456789);
215        let s = format!("{id}");
216        let parsed: Id128 = s.parse().unwrap();
217        assert_eq!(parsed, id);
218    }
219
220    #[test]
221    fn parse_errors() {
222        assert_eq!("abc".parse::<Id128>(), Err(ParseIdError::InvalidLength));
223        assert_eq!(
224            "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz".parse::<Id128>(),
225            Err(ParseIdError::InvalidHex)
226        );
227        // Wrong hyphen positions
228        assert_eq!(
229            "abcdef01-2345-6789-abcd-ef012345678".parse::<Id128>(),
230            Err(ParseIdError::InvalidLength)
231        );
232    }
233
234    #[test]
235    fn ordering() {
236        let a = Id128::from_u128(1);
237        let b = Id128::from_u128(2);
238        assert!(a < b);
239    }
240
241    /// C1 regression: Id128 must deserialize from an owned serde_json::Value string,
242    /// not only from a borrowed &str.  Previously used `<&str>::deserialize` which
243    /// fails when the deserializer holds owned data (e.g. Value-backed deserializer).
244    #[cfg(feature = "serde")]
245    #[test]
246    fn deserialize_from_owned_value() {
247        use alloc::string::ToString;
248        let uuid_str = "abcdef01-2345-6789-abcd-ef0123456789";
249        // serde_json::from_value takes a Value (owned), exercising the owned-string path.
250        let val = serde_json::Value::String(uuid_str.to_string());
251        let id: Id128 = serde_json::from_value(val).expect("Id128 must deserialize from Value");
252        assert_eq!(format!("{id}"), uuid_str);
253    }
254}