1use core::fmt;
29
30use crate::backend::{TickInt, Ticks, CANONICAL_BYTES};
31use crate::error::{Code, Result, TimeError};
32use crate::profile::Profile;
33use crate::value::Instant;
34
35pub const CROCKFORD: &[u8; 32] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ";
43
44pub const UCID_LEN: usize = 52;
49
50pub const UCID_BITS: u32 = 256;
56
57#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
62pub struct Ucid([u8; UCID_LEN]);
63
64impl Ucid {
65 pub fn as_str(&self) -> &str {
68 core::str::from_utf8(&self.0).expect("UCID is ASCII by construction")
70 }
71
72 pub fn as_bytes(&self) -> &[u8; UCID_LEN] {
74 &self.0
75 }
76
77 pub fn from_ticks(t: &Ticks) -> Result<Ucid> {
79 if t.bit_len() > UCID_BITS {
80 return Err(TimeError::with_context(
81 Code::E0031,
82 "UCID is defined only below 2^256",
83 ));
84 }
85 let be = t.to_canonical_bytes();
86 let mut out = [b'0'; UCID_LEN];
87 for (c, slot) in out.iter_mut().enumerate() {
88 let shift = 5 * (UCID_LEN - 1 - c) as u32;
90 let mut v = 0u8;
91 for j in 0..5u32 {
92 let bit = shift + j;
93 if bit < UCID_BITS && bit_at(&be, bit) {
94 v |= 1 << j;
95 }
96 }
97 *slot = CROCKFORD[v as usize];
98 }
99 Ok(Ucid(out))
100 }
101
102 pub fn to_ticks(&self) -> Result<Ticks> {
104 let thirty_two = <Ticks as TickInt>::from_u64(32);
105 let mut acc = <Ticks as TickInt>::zero();
106 for c in self.0.iter() {
107 let d = decode_char(*c)?;
108 acc = acc
109 .try_mul(&thirty_two)
110 .and_then(|v| v.try_add(&<Ticks as TickInt>::from_u64(d as u64)))
111 .ok_or(TimeError::new(Code::E0021))?;
112 }
113 if acc.bit_len() > UCID_BITS {
114 return Err(TimeError::with_context(
115 Code::E0031,
116 "decoded value is at or above 2^256",
117 ));
118 }
119 Ok(acc)
120 }
121
122 pub fn parse(s: &str) -> Result<Ucid> {
133 let mut buf = [b'0'; UCID_LEN];
134 let mut n = 0usize;
135 for c in s.bytes() {
136 if c == b'-' {
137 continue;
138 }
139 if n >= UCID_LEN {
140 return Err(TimeError::with_context(
141 Code::E0032,
142 "UCID is exactly 52 characters",
143 ));
144 }
145 let d = decode_char(c)?;
146 buf[n] = CROCKFORD[d as usize];
147 n += 1;
148 }
149 if n != UCID_LEN {
150 return Err(TimeError::with_context(
151 Code::E0032,
152 "UCID is exactly 52 characters",
153 ));
154 }
155 Ok(Ucid(buf))
156 }
157}
158
159impl fmt::Display for Ucid {
160 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161 f.write_str(self.as_str())
162 }
163}
164
165impl fmt::Debug for Ucid {
166 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167 write!(f, "Ucid({})", self.as_str())
168 }
169}
170
171fn bit_at(be: &[u8; CANONICAL_BYTES], i: u32) -> bool {
173 let byte = be[CANONICAL_BYTES - 1 - (i / 8) as usize];
174 (byte >> (i % 8)) & 1 == 1
175}
176
177fn decode_char(c: u8) -> Result<u8> {
179 let up = c.to_ascii_uppercase();
180 match up {
181 b'O' => return Ok(0),
182 b'I' | b'L' => return Ok(1),
183 _ => {}
184 }
185 CROCKFORD
186 .iter()
187 .position(|x| *x == up)
188 .map(|p| p as u8)
189 .ok_or(TimeError::with_context(
190 Code::E0032,
191 "not a Crockford base-32 character",
192 ))
193}
194
195impl<P: Profile> Instant<P> {
196 pub fn to_ucid(&self) -> Result<Ucid> {
200 Ucid::from_ticks(self.ticks())
201 }
202
203 pub fn from_ucid(u: &Ucid) -> Result<Self> {
205 Self::from_ticks(u.to_ticks()?)
206 }
207
208 pub fn from_bytes_slice(bytes: &[u8]) -> Result<Self> {
216 if bytes.len() != CANONICAL_BYTES {
217 return Err(TimeError::with_context(
218 Code::E0030,
219 "canonical binary is exactly 64 bytes (Rule B)",
220 ));
221 }
222 let mut buf = [0u8; CANONICAL_BYTES];
223 buf.copy_from_slice(bytes);
224 Self::from_bytes(&buf)
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231 use crate::profile::UC1;
232 use alloc::string::String;
233 use alloc::vec::Vec;
234
235 type I = Instant<UC1>;
236
237 fn at(n: u64) -> I {
238 I::from_u64(n).unwrap()
239 }
240
241 struct Rng(u64);
242 impl Rng {
243 fn next_u64(&mut self) -> u64 {
244 let mut x = self.0;
245 x ^= x << 13;
246 x ^= x >> 7;
247 x ^= x << 17;
248 self.0 = x;
249 x
250 }
251 fn next_ucid_range(&mut self) -> Ticks {
253 let mut bytes = [0u8; CANONICAL_BYTES];
254 for chunk in bytes[32..].chunks_mut(8) {
255 chunk.copy_from_slice(&self.next_u64().to_be_bytes());
256 }
257 <Ticks as TickInt>::from_canonical_bytes(&bytes).unwrap()
258 }
259 fn next_domain(&mut self) -> Ticks {
261 let mut bytes = [0u8; CANONICAL_BYTES];
262 for chunk in bytes.chunks_mut(8) {
263 chunk.copy_from_slice(&self.next_u64().to_be_bytes());
264 }
265 <Ticks as TickInt>::from_canonical_bytes(&bytes).unwrap()
266 }
267 }
268
269 #[test]
272 fn alphabet_is_ascii_ascending() {
273 assert_eq!(CROCKFORD.len(), 32);
277 for w in CROCKFORD.windows(2) {
278 assert!(w[0] < w[1], "alphabet not ascending at {:?}", w);
279 }
280 for c in [b'I', b'L', b'O', b'U'] {
282 assert!(!CROCKFORD.contains(&c), "{} must be excluded", c as char);
283 }
284 assert!(CROCKFORD.iter().all(|c| c.is_ascii_uppercase() || c.is_ascii_digit()));
285 }
286
287 #[test]
290 fn ucid_range_boundary_is_two_to_the_256() {
291 let max = <Ticks as TickInt>::pow2(UCID_BITS)
292 .unwrap()
293 .try_sub(&<Ticks as TickInt>::one())
294 .unwrap();
295 assert_eq!(max.bit_len(), 256);
296 let inside = I::from_ticks(max).unwrap();
297 assert!(inside.to_ucid().is_ok());
298
299 let boundary = <Ticks as TickInt>::pow2(UCID_BITS).unwrap();
300 assert_eq!(boundary.bit_len(), 257);
301 let outside = I::from_ticks(boundary).unwrap();
302 assert_eq!(outside.to_ucid().unwrap_err().code, Code::E0031);
303 assert_eq!(
304 outside.to_ucid().unwrap_err().code.exit_code(),
305 3,
306 "a domain error"
307 );
308
309 let far = I::from_ticks(<Ticks as TickInt>::domain_max()).unwrap();
311 assert_eq!(far.to_ucid().unwrap_err().code, Code::E0031);
312 }
313
314 #[test]
315 fn leading_character_is_always_zero_or_one() {
316 let mut rng = Rng(0xA11CE);
319 for _ in 0..256 {
320 let u = Ucid::from_ticks(&rng.next_ucid_range()).unwrap();
321 let c = u.as_bytes()[0];
322 assert!(c == b'0' || c == b'1', "leading char was {}", c as char);
323 }
324 }
325
326 #[test]
329 fn ucid_round_trips() {
330 let mut vals: Vec<Ticks> = alloc::vec![
331 <Ticks as TickInt>::zero(),
332 <Ticks as TickInt>::one(),
333 UC1::origin_offset(),
334 UC1::beat(),
335 ];
336 let mut rng = Rng(0xB0B);
337 for _ in 0..512 {
338 vals.push(rng.next_ucid_range());
339 }
340 for v in vals {
341 let inst = I::from_ticks(v.clone()).unwrap();
342 let u = inst.to_ucid().unwrap();
343 assert_eq!(u.as_str().len(), UCID_LEN);
344 assert_eq!(u.to_ticks().unwrap(), v);
345 assert_eq!(I::from_ucid(&u).unwrap(), inst);
346 assert_eq!(Ucid::parse(u.as_str()).unwrap(), u);
348 }
349 }
350
351 #[test]
352 fn zero_is_all_zeros() {
353 let u = I::zero().to_ucid().unwrap();
354 assert_eq!(u.as_str(), "0".repeat(UCID_LEN));
355 }
356
357 #[test]
358 fn reproduces_appendix_c_ucids() {
359 let cases = [
362 (
363 "8070204002895596515944343085635637180530466139316558837890625",
364 "0000000000050PM5TBHF4BFKRZC1KVN566SZGWG5DZ0SSBM29FJ1",
365 ),
366 (
367 "8070205189123984864657505252035637180530466139316558837890625",
368 "0000000000050PM6K45HH4YGQJ6SEDGDDZ1NKFHD32F2XBM29FJ1",
369 ),
370 (
371 "222432546681680327568000000000000000000000000000000000000",
372 "000000000000004H4KEWEGEB5M995XKBZHX3425VFFD900000000",
373 ),
374 ];
375 for (ticks, want) in cases {
376 let t = <Ticks as TickInt>::from_dec_str(ticks).unwrap();
377 assert_eq!(I::from_ticks(t).unwrap().to_ucid().unwrap().as_str(), want);
378 }
379 }
380
381 #[test]
384 fn parse_accepts_crockford_leniencies() {
385 let u = I::from_ticks(UC1::origin_offset()).unwrap().to_ucid().unwrap();
386 let canonical = String::from(u.as_str());
387
388 assert_eq!(Ucid::parse(&canonical.to_lowercase()).unwrap(), u);
390 let hyphenated = {
392 let mut s = String::new();
393 for (i, c) in canonical.chars().enumerate() {
394 if i > 0 && i % 4 == 0 {
395 s.push('-');
396 }
397 s.push(c);
398 }
399 s
400 };
401 assert_eq!(Ucid::parse(&hyphenated).unwrap(), u);
402 let substituted = canonical.replace('0', "O").replace('1', "L");
404 assert_eq!(Ucid::parse(&substituted).unwrap(), u);
405 }
406
407 #[test]
408 fn parse_rejects_bad_input() {
409 let good = I::zero().to_ucid().unwrap();
410 let s = String::from(good.as_str());
411 assert_eq!(Ucid::parse(&s[..51]).unwrap_err().code, Code::E0032);
413 let mut long = s.clone();
414 long.push('0');
415 assert_eq!(Ucid::parse(&long).unwrap_err().code, Code::E0032);
416 let bad = alloc::format!("U{}", &s[1..]);
418 assert_eq!(Ucid::parse(&bad).unwrap_err().code, Code::E0032);
419 for c in ['!', ' ', '\u{00B7}'] {
421 let bad = alloc::format!("{c}{}", &s[1..]);
422 assert_eq!(Ucid::parse(&bad).unwrap_err().code, Code::E0032);
423 }
424 let too_big = "Z".repeat(UCID_LEN);
426 let parsed = Ucid::parse(&too_big).unwrap();
427 assert_eq!(parsed.to_ticks().unwrap_err().code, Code::E0031);
428 }
429
430 #[test]
433 fn binary_order_equals_numeric_order_fuzzed() {
434 let mut rng = Rng(0xD1CE_0001);
435 let mut vals: Vec<I> = (0..2048)
436 .map(|_| I::from_ticks(rng.next_domain()).unwrap())
437 .collect();
438 vals.push(I::zero());
440 vals.push(at(1));
441 vals.push(I::from_ticks(<Ticks as TickInt>::domain_max()).unwrap());
442 vals.push(I::from_ticks(UC1::origin_offset()).unwrap());
443 vals.sort();
444
445 let encoded: Vec<[u8; CANONICAL_BYTES]> = vals.iter().map(|v| v.to_bytes()).collect();
446 let mut resorted = encoded.clone();
447 resorted.sort();
448 assert_eq!(resorted, encoded, "byte order diverges from numeric order");
449
450 for w in vals.windows(2) {
452 assert_eq!(
453 w[0].cmp(&w[1]),
454 w[0].to_bytes().cmp(&w[1].to_bytes()),
455 "pairwise order disagrees"
456 );
457 }
458 }
459
460 #[test]
461 fn ucid_order_equals_numeric_order_fuzzed() {
462 let mut rng = Rng(0xD1CE_0002);
463 let mut vals: Vec<I> = (0..2048)
464 .map(|_| I::from_ticks(rng.next_ucid_range()).unwrap())
465 .collect();
466 vals.push(I::zero());
467 vals.push(at(1));
468 vals.push(I::from_ticks(UC1::origin_offset()).unwrap());
469 vals.sort();
470
471 let ids: Vec<Ucid> = vals.iter().map(|v| v.to_ucid().unwrap()).collect();
472 let strings: Vec<&str> = ids.iter().map(|u| u.as_str()).collect();
473 let mut resorted = strings.clone();
474 resorted.sort();
475 assert_eq!(resorted, strings, "UCID order diverges from numeric order");
476
477 for w in vals.windows(2) {
478 let (a, b) = (w[0].to_ucid().unwrap(), w[1].to_ucid().unwrap());
479 assert_eq!(w[0].cmp(&w[1]), a.as_str().cmp(b.as_str()));
480 }
481 }
482
483 #[test]
486 fn ucid_has_no_entropy() {
487 let a = at(1_234_567);
492 let b = at(1_234_567);
493 assert_eq!(a.to_ucid().unwrap(), b.to_ucid().unwrap());
494
495 let ns = UC1::bridge()
500 .ticks
501 .quot_rem(&<Ticks as TickInt>::from_u64(1_000_000_000))
502 .0;
503 let base = UC1::origin_offset();
504 let earlier = I::from_ticks(base.clone()).unwrap().to_ucid().unwrap();
508 let later = I::from_ticks(base.try_add(&ns).unwrap())
509 .unwrap()
510 .to_ucid()
511 .unwrap();
512 assert_ne!(earlier, later);
513
514 let shared = earlier
515 .as_bytes()
516 .iter()
517 .zip(later.as_bytes().iter())
518 .take_while(|(x, y)| x == y)
519 .count();
520 assert!(
521 shared >= 20,
522 "consecutive nanoseconds shared only {shared} of {UCID_LEN} characters; \
523 the entropy claim in Rule I depends on this being large"
524 );
525
526 assert!(earlier.as_str().starts_with("00000000000"));
529 }
530
531 #[test]
534 fn binary_form_is_fixed_width_and_not_minimal() {
535 assert_eq!(at(1).to_bytes().len(), CANONICAL_BYTES);
538 assert_eq!(at(1).to_bytes()[..63], [0u8; 63]);
539 assert_eq!(at(1).to_bytes()[63], 1);
540 assert_eq!(I::zero().to_bytes(), [0u8; CANONICAL_BYTES]);
541 assert_eq!(
542 I::from_ticks(<Ticks as TickInt>::domain_max())
543 .unwrap()
544 .to_bytes(),
545 [0xffu8; CANONICAL_BYTES]
546 );
547 }
548
549 #[test]
550 fn binary_slice_decoding_checks_the_width() {
551 let v = at(42);
552 let b = v.to_bytes();
553 assert_eq!(I::from_bytes_slice(&b).unwrap(), v);
554 assert_eq!(
555 I::from_bytes_slice(&b[..63]).unwrap_err().code,
556 Code::E0030
557 );
558 let mut too_long = alloc::vec::Vec::from(&b[..]);
559 too_long.push(0);
560 assert_eq!(
561 I::from_bytes_slice(&too_long).unwrap_err().code,
562 Code::E0030
563 );
564 assert_eq!(I::from_bytes_slice(&[]).unwrap_err().code, Code::E0030);
565 }
566
567 #[test]
568 fn binary_round_trips_over_the_domain() {
569 let mut rng = Rng(0xFEED_BEEF);
570 for _ in 0..1024 {
571 let v = I::from_ticks(rng.next_domain()).unwrap();
572 assert_eq!(I::from_bytes(&v.to_bytes()).unwrap(), v);
573 }
574 }
575
576 #[test]
577 fn ucid_and_binary_agree_on_the_low_256_bits() {
578 let mut rng = Rng(0x5A5A);
581 for _ in 0..128 {
582 let v = I::from_ticks(rng.next_ucid_range()).unwrap();
583 assert_eq!(&v.to_bytes()[..32], &[0u8; 32]);
584 assert_eq!(v.to_ucid().unwrap().to_ticks().unwrap(), *v.ticks());
585 }
586 }
587}