1use serde::{Deserialize, Serialize};
8use std::fmt;
9use uuid::Uuid;
10
11const CHARS: &[u8; 64] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_$";
14
15pub const GLOBAL_ID_LEN: usize = 22;
17
18#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
20pub struct GlobalId(String);
21
22#[derive(Debug, thiserror::Error, PartialEq, Eq)]
24pub enum GlobalIdError {
25 #[error("expected {GLOBAL_ID_LEN} characters, got {0}")]
26 WrongLength(usize),
27 #[error("character {0:?} is not in the IFC base-64 alphabet")]
28 InvalidChar(char),
29 #[error("leading character {0:?} would overflow 128 bits")]
31 Overflow(char),
32}
33
34impl GlobalId {
35 pub fn new() -> Self {
37 Self::from_uuid(Uuid::new_v4())
38 }
39
40 pub fn from_uuid(uuid: Uuid) -> Self {
42 let bytes = uuid.as_bytes();
43 let mut out = String::with_capacity(GLOBAL_ID_LEN);
44
45 push_digits(u32::from(bytes[0]), 2, &mut out);
48 for chunk in bytes[1..].chunks_exact(3) {
49 let v = (u32::from(chunk[0]) << 16) | (u32::from(chunk[1]) << 8) | u32::from(chunk[2]);
50 push_digits(v, 4, &mut out);
51 }
52
53 debug_assert_eq!(out.len(), GLOBAL_ID_LEN);
54 GlobalId(out)
55 }
56
57 pub fn from_bytes(bytes: [u8; 16]) -> Self {
63 Self::from_uuid(Uuid::from_bytes(bytes))
64 }
65
66 pub fn parse(s: &str) -> Result<Self, GlobalIdError> {
70 if s.len() != GLOBAL_ID_LEN {
71 return Err(GlobalIdError::WrongLength(s.len()));
72 }
73 for c in s.chars() {
74 if digit_of(c).is_none() {
75 return Err(GlobalIdError::InvalidChar(c));
76 }
77 }
78 let lead = s.chars().next().expect("length checked above");
79 if digit_of(lead).expect("validated above") > 3 {
80 return Err(GlobalIdError::Overflow(lead));
81 }
82 Ok(GlobalId(s.to_owned()))
83 }
84
85 pub fn to_uuid(&self) -> Uuid {
87 let digits: Vec<u32> = self
88 .0
89 .chars()
90 .map(|c| digit_of(c).expect("GlobalId is validated on construction"))
91 .collect();
92
93 let mut bytes = [0u8; 16];
94 bytes[0] = (digits[0] * 64 + digits[1]) as u8;
95 for (group, out) in digits[2..]
96 .chunks_exact(4)
97 .zip(bytes[1..].chunks_exact_mut(3))
98 {
99 let v = group.iter().fold(0u32, |acc, d| acc * 64 + d);
100 out[0] = (v >> 16) as u8;
101 out[1] = (v >> 8) as u8;
102 out[2] = v as u8;
103 }
104 Uuid::from_bytes(bytes)
105 }
106
107 pub fn as_str(&self) -> &str {
108 &self.0
109 }
110}
111
112impl Default for GlobalId {
113 fn default() -> Self {
114 Self::new()
115 }
116}
117
118impl fmt::Display for GlobalId {
119 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120 f.write_str(&self.0)
121 }
122}
123
124fn push_digits(mut v: u32, n: usize, out: &mut String) {
125 let mut buf = [0u8; 4];
126 for slot in buf[..n].iter_mut().rev() {
127 *slot = CHARS[(v % 64) as usize];
128 v /= 64;
129 }
130 out.push_str(std::str::from_utf8(&buf[..n]).expect("alphabet is ASCII"));
131}
132
133fn digit_of(c: char) -> Option<u32> {
134 if !c.is_ascii() {
135 return None;
136 }
137 CHARS.iter().position(|&b| b == c as u8).map(|i| i as u32)
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[test]
145 fn round_trips_through_uuid() {
146 for _ in 0..1000 {
147 let uuid = Uuid::new_v4();
148 let id = GlobalId::from_uuid(uuid);
149 assert_eq!(id.as_str().len(), GLOBAL_ID_LEN);
150 assert_eq!(id.to_uuid(), uuid, "round trip failed for {uuid}");
151 }
152 }
153
154 #[test]
155 fn parses_what_it_generates() {
156 let id = GlobalId::new();
157 assert_eq!(GlobalId::parse(id.as_str()), Ok(id));
158 }
159
160 #[test]
161 fn rejects_malformed_ids() {
162 assert_eq!(
163 GlobalId::parse("too-short"),
164 Err(GlobalIdError::WrongLength(9))
165 );
166 assert_eq!(
168 GlobalId::parse("0*00000000000000000000"),
169 Err(GlobalIdError::InvalidChar('*'))
170 );
171 assert_eq!(
173 GlobalId::parse("9000000000000000000000"),
174 Err(GlobalIdError::Overflow('9'))
175 );
176 }
177
178 #[test]
179 fn derived_ids_are_stable_and_valid() {
180 let a = GlobalId::from_bytes([7; 16]);
181 let b = GlobalId::from_bytes([7; 16]);
182 assert_eq!(a, b, "the same bytes must always give the same identity");
183 assert!(GlobalId::parse(a.as_str()).is_ok());
184 assert_ne!(a, GlobalId::from_bytes([8; 16]));
185 for lead in [0u8, 1, 63, 64, 191, 255] {
187 let mut bytes = [0u8; 16];
188 bytes[0] = lead;
189 let id = GlobalId::from_bytes(bytes);
190 assert!(
191 GlobalId::parse(id.as_str()).is_ok(),
192 "leading byte {lead} failed"
193 );
194 }
195 }
196
197 #[test]
198 fn boundary_uuids_survive() {
199 for uuid in [Uuid::nil(), Uuid::from_bytes([0xff; 16])] {
200 assert_eq!(GlobalId::from_uuid(uuid).to_uuid(), uuid);
201 }
202 }
203
204 #[test]
205 fn ids_are_unique_enough_to_key_a_model() {
206 let ids: std::collections::BTreeSet<_> = (0..10_000).map(|_| GlobalId::new()).collect();
207 assert_eq!(ids.len(), 10_000);
208 }
209}