things3_cloud/ids/
things_id.rs1use std::fmt;
2use std::str::FromStr;
3
4use rand::random;
5use serde::de::{self, Visitor};
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7use sha1::{Digest, Sha1};
8use uuid::Uuid;
9
10#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
15pub struct ThingsId([u8; 16]);
16
17impl ThingsId {
18 pub fn random() -> Self {
19 let uuid = Uuid::from_bytes(random());
20 ThingsId(uuid_to_bytes(&uuid))
21 }
22
23 pub fn as_bytes(&self) -> &[u8; 16] {
24 &self.0
25 }
26
27 pub fn starts_with(&self, prefix: &str) -> bool {
28 self.to_string().starts_with(prefix)
29 }
30}
31
32impl fmt::Display for ThingsId {
33 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34 f.write_str(&base58_encode(&self.0))
35 }
36}
37
38impl AsRef<[u8; 16]> for ThingsId {
39 fn as_ref(&self) -> &[u8; 16] {
40 &self.0
41 }
42}
43
44impl From<ThingsId> for String {
45 fn from(id: ThingsId) -> Self {
46 id.to_string()
47 }
48}
49
50impl From<String> for ThingsId {
51 fn from(value: String) -> Self {
52 value
53 .parse::<ThingsId>()
54 .unwrap_or_else(|_| panic!("invalid Things ID: {value:?}"))
55 }
56}
57
58impl From<&str> for ThingsId {
59 fn from(value: &str) -> Self {
60 value
61 .parse::<ThingsId>()
62 .unwrap_or_else(|_| panic!("invalid Things ID: {value:?}"))
63 }
64}
65
66impl Default for ThingsId {
67 fn default() -> Self {
68 Self([0u8; 16])
69 }
70}
71
72impl FromStr for ThingsId {
73 type Err = ParseThingsIdError;
74
75 fn from_str(s: &str) -> Result<Self, Self::Err> {
76 if s.is_empty() {
77 return Err(ParseThingsIdError(s.to_owned()));
78 }
79 if let Ok(uuid) = Uuid::parse_str(s) {
80 return Ok(ThingsId(uuid_to_bytes(&uuid)));
81 }
82 let decoded = base58_decode(s).ok_or_else(|| ParseThingsIdError(s.to_owned()))?;
83 if decoded.len() != 16 {
84 return Err(ParseThingsIdError(s.to_owned()));
85 }
86 let mut bytes = [0u8; 16];
87 bytes.copy_from_slice(&decoded);
88 Ok(ThingsId(bytes))
89 }
90}
91
92impl Serialize for ThingsId {
93 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
94 serializer.serialize_str(&self.to_string())
95 }
96}
97
98impl<'de> Deserialize<'de> for ThingsId {
99 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
100 struct ThingsIdVisitor;
101
102 impl Visitor<'_> for ThingsIdVisitor {
103 type Value = ThingsId;
104
105 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106 write!(f, "a Things ID string (compact base58 or hyphenated UUID)")
107 }
108
109 fn visit_str<E: de::Error>(self, v: &str) -> Result<ThingsId, E> {
110 v.parse().map_err(de::Error::custom)
111 }
112 }
113
114 deserializer.deserialize_str(ThingsIdVisitor)
115 }
116}
117
118#[derive(Debug, Clone, PartialEq, Eq)]
120pub struct ParseThingsIdError(String);
121
122impl fmt::Display for ParseThingsIdError {
123 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124 write!(f, "invalid Things ID: {:?}", self.0)
125 }
126}
127
128impl std::error::Error for ParseThingsIdError {}
129
130const BASE58_ALPHABET: &[u8; 58] = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
131
132pub fn base58_encode_fixed(raw: &[u8; 16]) -> ([u8; 22], usize) {
136 let mut digits = [0u8; 22];
138 let mut len = 0usize;
139
140 for &byte in raw.iter() {
141 let mut carry = byte as u32;
142 for digit in digits[..len].iter_mut() {
143 let value = (*digit as u32) * 256 + carry;
144 *digit = (value % 58) as u8;
145 carry = value / 58;
146 }
147 while carry > 0 {
148 digits[len] = (carry % 58) as u8;
149 len += 1;
150 carry /= 58;
151 }
152 }
153
154 let leading_ones = raw.iter().take_while(|&&b| b == 0).count();
156 let total = leading_ones + len;
159 debug_assert!(
160 total <= 22,
161 "base58_encode_fixed: output length {total} > 22"
162 );
163
164 let mut out = [0u8; 22];
165 for b in out[..leading_ones].iter_mut() {
167 *b = BASE58_ALPHABET[0]; }
169 for (i, &d) in digits[..len].iter().rev().enumerate() {
171 out[leading_ones + i] = BASE58_ALPHABET[d as usize];
172 }
173 (out, total.max(1)) }
175
176fn base58_encode(raw: &[u8]) -> String {
177 if raw.is_empty() {
178 return String::new();
179 }
180
181 let mut leading_zeros = 0usize;
182 for b in raw {
183 if *b == 0 {
184 leading_zeros += 1;
185 } else {
186 break;
187 }
188 }
189
190 let mut digits: Vec<u8> = Vec::new();
191 for &byte in raw.iter().skip(leading_zeros) {
192 let mut carry = byte as u32;
193 for digit in &mut digits {
194 let value = (*digit as u32 * 256) + carry;
195 *digit = (value % 58) as u8;
196 carry = value / 58;
197 }
198 while carry > 0 {
199 digits.push((carry % 58) as u8);
200 carry /= 58;
201 }
202 }
203
204 let mut out = String::with_capacity(leading_zeros + digits.len());
205 for _ in 0..leading_zeros {
206 out.push('1');
207 }
208 if digits.is_empty() {
209 return out;
210 }
211 for digit in digits.iter().rev() {
212 out.push(BASE58_ALPHABET[*digit as usize] as char);
213 }
214 out
215}
216
217fn base58_digit(byte: u8) -> Option<u8> {
218 BASE58_ALPHABET
219 .iter()
220 .position(|&c| c == byte)
221 .map(|i| i as u8)
222}
223
224fn base58_decode(input: &str) -> Option<Vec<u8>> {
225 if input.is_empty() {
226 return Some(Vec::new());
227 }
228
229 let bytes = input.as_bytes();
230 let mut leading_ones = 0usize;
231 for b in bytes {
232 if *b == b'1' {
233 leading_ones += 1;
234 } else {
235 break;
236 }
237 }
238
239 let mut decoded: Vec<u8> = Vec::new();
240 for &ch in bytes.iter().skip(leading_ones) {
241 let mut carry = base58_digit(ch)? as u32;
242 for byte in &mut decoded {
243 let value = (*byte as u32 * 58) + carry;
244 *byte = (value & 0xff) as u8;
245 carry = value >> 8;
246 }
247 while carry > 0 {
248 decoded.push((carry & 0xff) as u8);
249 carry >>= 8;
250 }
251 }
252
253 let mut out = Vec::with_capacity(leading_ones + decoded.len());
254 out.extend(std::iter::repeat_n(0u8, leading_ones));
255 for byte in decoded.iter().rev() {
256 out.push(*byte);
257 }
258 Some(out)
259}
260
261fn uuid_to_bytes(uuid: &Uuid) -> [u8; 16] {
262 let canonical = uuid.to_string().to_uppercase();
263 let digest = Sha1::digest(canonical.as_bytes());
264 let mut bytes = [0u8; 16];
265 bytes.copy_from_slice(&digest[..16]);
266 bytes
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272 use std::collections::HashSet;
273
274 const LEGACY_UUID: &str = "3C6BBD49-8D11-4FFF-8B0E-B8F33FA9C00A";
275 const LEGACY_UUID_LOWER: &str = "3c6bbd49-8d11-4fff-8b0e-b8f33fa9c00a";
276 fn compact_for_legacy() -> String {
277 ThingsId::from_str(LEGACY_UUID).unwrap().to_string()
278 }
279
280 #[test]
281 fn parse_legacy_uuid_uppercase() {
282 let id: ThingsId = LEGACY_UUID.parse().unwrap();
283 assert_eq!(id.to_string(), compact_for_legacy());
284 assert_eq!(id.to_string().len(), 22);
285 }
286
287 #[test]
288 fn parse_legacy_uuid_lowercase() {
289 let upper: ThingsId = LEGACY_UUID.parse().unwrap();
290 let lower: ThingsId = LEGACY_UUID_LOWER.parse().unwrap();
291 assert_eq!(upper, lower, "UUID parsing must be case-insensitive");
292 }
293
294 #[test]
295 fn parse_compact_preserved() {
296 let compact = compact_for_legacy();
297 let id: ThingsId = compact.parse().unwrap();
298 assert_eq!(id.to_string(), compact);
299 }
300
301 #[test]
302 fn empty_string_is_error() {
303 let err = "".parse::<ThingsId>();
304 assert!(err.is_err());
305 }
306
307 #[test]
308 fn display_roundtrip() {
309 let id: ThingsId = LEGACY_UUID.parse().unwrap();
310 let displayed = id.to_string();
311 let reparsed: ThingsId = displayed.parse().unwrap();
312 assert_eq!(id, reparsed);
313 }
314
315 #[test]
316 fn random_is_unique() {
317 let ids: HashSet<String> = (0..20).map(|_| ThingsId::random().to_string()).collect();
318 assert_eq!(ids.len(), 20, "random IDs should be unique");
319 }
320
321 #[test]
322 fn random_is_compact_length() {
323 let id = ThingsId::random();
324 assert_eq!(id.to_string().len(), 22);
325 }
326
327 #[test]
328 fn serde_roundtrip_compact() {
329 let id: ThingsId = LEGACY_UUID.parse().unwrap();
330 let json = serde_json::to_string(&id).unwrap();
331 let back: ThingsId = serde_json::from_str(&json).unwrap();
332 assert_eq!(id, back);
333 }
334
335 #[test]
336 fn serde_deserialize_from_legacy_uuid() {
337 let json = format!("\"{}\"", LEGACY_UUID);
338 let id: ThingsId = serde_json::from_str(&json).unwrap();
339 assert_eq!(id.to_string().len(), 22);
340 assert_eq!(id.to_string(), compact_for_legacy());
341 }
342
343 #[test]
344 fn into_string() {
345 let id: ThingsId = LEGACY_UUID.parse().unwrap();
346 let s: String = id.clone().into();
347 assert_eq!(s, id.to_string());
348 }
349
350 #[test]
351 fn as_ref_bytes() {
352 let id: ThingsId = LEGACY_UUID.parse().unwrap();
353 let r: &[u8; 16] = id.as_ref();
354 assert_eq!(r, id.as_bytes());
355 }
356
357 #[test]
358 fn rejects_invalid_compact_id() {
359 assert!("not-a-things-id".parse::<ThingsId>().is_err());
360 assert!("0OIl".parse::<ThingsId>().is_err());
361 }
362
363 #[test]
364 fn base58_roundtrip_for_internal_bytes() {
365 let samples = [
366 [0u8; 16],
367 [255u8; 16],
368 uuid_to_bytes(&Uuid::parse_str(LEGACY_UUID).unwrap()),
369 ];
370 for sample in samples {
371 let encoded = base58_encode(&sample);
372 let decoded = base58_decode(&encoded).unwrap();
373 assert_eq!(decoded, sample);
374 }
375 }
376
377 #[test]
378 fn base58_encode_fixed_matches_to_string() {
379 let mut samples: Vec<ThingsId> = vec![
381 ThingsId([0u8; 16]),
382 ThingsId([255u8; 16]),
383 LEGACY_UUID.parse().unwrap(),
384 ];
385 for _ in 0..20 {
386 samples.push(ThingsId::random());
387 }
388 for id in &samples {
389 let (buf, len) = base58_encode_fixed(id.as_bytes());
390 let fixed = std::str::from_utf8(&buf[..len]).unwrap();
391 let expected = id.to_string();
392 assert_eq!(fixed, expected, "mismatch for {:?}", id.as_bytes());
393 }
394 }
395
396 #[test]
397 fn base58_encode_fixed_preserves_sort_order() {
398 let ids: Vec<ThingsId> = (0..50).map(|_| ThingsId::random()).collect();
400 let mut by_fixed: Vec<String> = ids
401 .iter()
402 .map(|id| {
403 let (buf, len) = base58_encode_fixed(id.as_bytes());
404 std::str::from_utf8(&buf[..len]).unwrap().to_owned()
405 })
406 .collect();
407 by_fixed.sort();
408 let mut by_string: Vec<String> = ids.iter().map(|id| id.to_string()).collect();
409 by_string.sort();
410 assert_eq!(
411 by_fixed, by_string,
412 "fixed-encode sort order != to_string sort order"
413 );
414 }
415}