1const ALPHABET: &[u8; 64] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_$";
17
18const fn reverse_table() -> [i8; 256] {
20 let mut table = [-1i8; 256];
21 let mut i = 0;
22 while i < 64 {
23 table[ALPHABET[i] as usize] = i as i8;
24 i += 1;
25 }
26 table
27}
28
29const REVERSE: [i8; 256] = reverse_table();
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
33pub struct Guid([u8; 22]);
34
35impl Guid {
36 pub fn parse(text: &str) -> Option<Self> {
44 let bytes = text.as_bytes();
45 if bytes.len() != 22 {
46 return None;
47 }
48 if bytes.iter().any(|&b| REVERSE[b as usize] < 0) {
49 return None;
50 }
51 if REVERSE[bytes[0] as usize] > 3 {
52 return None;
53 }
54 let mut buf = [0u8; 22];
55 buf.copy_from_slice(bytes);
56 Some(Self(buf))
57 }
58
59 pub fn as_str(&self) -> &str {
61 std::str::from_utf8(&self.0).expect("alphabet is ASCII by construction")
64 }
65
66 pub fn from_uuid(uuid: [u8; 16]) -> Self {
68 let mut num = 0u128;
69 for b in uuid {
70 num = (num << 8) | b as u128;
71 }
72 let mut out = [b'0'; 22];
75 let mut n = num;
76 for slot in out.iter_mut().rev() {
77 *slot = ALPHABET[(n & 0x3f) as usize];
78 n >>= 6;
79 }
80 Self(out)
81 }
82
83 pub fn to_uuid(self) -> [u8; 16] {
85 let mut num = 0u128;
86 for &b in &self.0 {
87 num = (num << 6) | (REVERSE[b as usize] as u128);
88 }
89 let mut out = [0u8; 16];
90 for (i, slot) in out.iter_mut().enumerate() {
91 *slot = ((num >> (8 * (15 - i))) & 0xff) as u8;
92 }
93 out
94 }
95}
96
97impl std::fmt::Display for Guid {
98 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99 f.write_str(self.as_str())
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 #[test]
108 fn rejects_wrong_length_and_foreign_characters() {
109 assert!(Guid::parse("tooshort").is_none());
110 assert!(Guid::parse("0123456789ABCDEFGHIJ+/").is_none());
112 }
113
114 #[test]
115 fn accepts_a_real_globalid_from_the_fixture_corpus() {
116 assert!(Guid::parse("2O2Fr$t4X7Zf8NOew3FLOH").is_some());
117 }
118
119 #[test]
122 fn uuid_roundtrip_is_lossless() {
123 let uuid: [u8; 16] = [
124 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54,
125 0x32, 0x10,
126 ];
127 assert_eq!(Guid::from_uuid(uuid).to_uuid(), uuid);
128 }
129
130 #[test]
133 fn rejects_a_leading_digit_beyond_the_uuid_range() {
134 for leading in ALPHABET.iter().skip(4) {
135 let mut text = [b'0'; 22];
136 text[0] = *leading;
137 let text = std::str::from_utf8(&text).unwrap();
138 assert!(Guid::parse(text).is_none(), "{text} was accepted");
139 }
140 for leading in b"0123" {
141 let mut text = [b'$'; 22];
142 text[0] = *leading;
143 assert!(Guid::parse(std::str::from_utf8(&text).unwrap()).is_some());
144 }
145 assert!(Guid::parse("4000000000000000000000").is_none());
146 assert!(Guid::parse("$$$$$$$$$$$$$$$$$$$$$$").is_none());
147 }
148
149 #[test]
151 fn the_uuid_range_ends_at_the_leading_digit_three() {
152 assert_eq!(Guid::from_uuid([0; 16]).as_str(), "0000000000000000000000");
153 assert_eq!(
154 Guid::from_uuid([0xff; 16]).as_str(),
155 "3$$$$$$$$$$$$$$$$$$$$$"
156 );
157 }
158
159 #[test]
162 fn every_accepted_text_roundtrips_through_the_uuid() {
163 let base = *b"2O2Fr$t4X7Zf8NOew3FLOH";
164 let mut checked = 0;
165 for position in 0..22 {
166 for &digit in ALPHABET {
167 let mut text = base;
168 text[position] = digit;
169 let text = std::str::from_utf8(&text).unwrap();
170 if let Some(guid) = Guid::parse(text) {
171 assert_eq!(Guid::from_uuid(guid.to_uuid()), guid, "{text}");
172 checked += 1;
173 }
174 }
175 }
176 assert_eq!(checked, 21 * 64 + 4);
178 }
179
180 #[test]
181 fn text_roundtrip_is_lossless() {
182 let g = Guid::parse("2O2Fr$t4X7Zf8NOew3FLOH").unwrap();
183 assert_eq!(Guid::from_uuid(g.to_uuid()), g);
184 }
185}