Skip to main content

ucal_core/
ident.rs

1//! Canonical binary (§7.1) and UCID (§7.2).
2//!
3//! # Two encodings, one ordering property
4//!
5//! Rule S: lexicographic order equals chronological order for the binary form and
6//! for UCID, and **not** for the text forms unless they are zero-padded to a
7//! fixed tier width. Both encodings here are fixed-width and big-endian, which is
8//! the whole reason that property holds: byte order *is* numeric order, so the
9//! encoding is directly usable as a database key or a sort key without a
10//! comparator.
11//!
12//! Rule B additionally forbids length-prefixed, minimal, and varint encodings as
13//! canonical forms. All three would break the ordering property, and a minimal
14//! encoding would also make the wire format depend on the value's magnitude
15//! rather than on the profile — which is failure mode F5.
16//!
17//! # What UCID is not
18//!
19//! Rule I is emphatic and this module's documentation repeats it: **UCID contains
20//! no randomness.** It is a pure function of the instant, so two events at the
21//! same tick receive the same UCID. Worse, §2.4 guarantees that an instant read
22//! from a nanosecond clock has at least 21 trailing base-5 zeros, so the low
23//! digits are not merely non-random but structurally constrained — consecutive
24//! nanoseconds share more than twenty leading characters. UCID MUST NOT be used
25//! as a unique identifier for concurrent events, and `ucid_has_no_entropy`
26//! measures exactly how badly it would fail if it were.
27
28use 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
35/// Crockford base-32, in ascending value order.
36///
37/// `I`, `L`, `O` and `U` are absent: the first three because they are confusable
38/// with `1` and `0`, and `U` to avoid accidental obscenity. The alphabet is
39/// strictly ascending in ASCII, which is what makes lexicographic order equal
40/// numeric order (Rule S) — `alphabet_is_ascii_ascending` checks it rather than
41/// trusting it.
42pub const CROCKFORD: &[u8; 32] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ";
43
44/// UCID length in characters (§7.2).
45///
46/// 52 characters of 5 bits is 260 bits, against a 256-bit value, so the leading
47/// character encodes only one significant bit and is always `0` or `1`.
48pub const UCID_LEN: usize = 52;
49
50/// UCID is defined only for instants below `2^256` (Rule I).
51///
52/// That ceiling is about 1.978×10²⁶ years — past the end of the stelliferous era,
53/// and 30 orders of magnitude beyond the present epoch, but far short of the
54/// profile domain's 2.29×10¹⁰³ years. Outside it, `UCAL-E0031`.
55pub const UCID_BITS: u32 = 256;
56
57/// The fixed-width sortable text identifier of an instant (§7.2).
58///
59/// Stored as ASCII bytes rather than a `String`, so the type is `Copy` and needs
60/// no allocator — UCID has to work in the `no_std` builds GE-5 targets.
61#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
62pub struct Ucid([u8; UCID_LEN]);
63
64impl Ucid {
65    /// The identifier as a string slice. Always 52 uppercase Crockford
66    /// characters, with no checksum and no separators.
67    pub fn as_str(&self) -> &str {
68        // The buffer is only ever filled from CROCKFORD, which is ASCII.
69        core::str::from_utf8(&self.0).expect("UCID is ASCII by construction")
70    }
71
72    /// The raw ASCII bytes.
73    pub fn as_bytes(&self) -> &[u8; UCID_LEN] {
74        &self.0
75    }
76
77    /// Encode a tick count. `UCAL-E0031` at or above `2^256` (Rule I).
78    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            // Character 0 is the most significant, covering bits 255..259.
89            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    /// Decode to a tick count.
103    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    /// Parse a UCID.
123    ///
124    /// Accepts the standard Crockford input leniencies — case-insensitive, with
125    /// `I`/`L` read as `1` and `O` as `0`, and hyphens ignored — while emitting
126    /// only the strict canonical form. Being lenient on input and strict on
127    /// output is the right way round: a UCID that has been read aloud, retyped or
128    /// line-wrapped should still resolve, but nothing this library writes should
129    /// need that forgiveness.
130    ///
131    /// `UCAL-E0032` on an invalid character or a wrong length.
132    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
171/// Bit `i` of a big-endian byte array, counting `0` as the least significant.
172fn 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
177/// Crockford decoding, with the standard substitutions.
178fn 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    /// The instant's UCID (§7.2). `UCAL-E0031` above `2^256` (Rule I).
197    ///
198    /// Not a unique identifier: see the module documentation and Rule I.
199    pub fn to_ucid(&self) -> Result<Ucid> {
200        Ucid::from_ticks(self.ticks())
201    }
202
203    /// Recover an instant from its UCID.
204    pub fn from_ucid(u: &Ucid) -> Result<Self> {
205        Self::from_ticks(u.to_ticks()?)
206    }
207
208    /// Decode the canonical binary form from a slice, checking the width.
209    ///
210    /// `UCAL-E0030` if the slice is not exactly 64 bytes. The array-typed
211    /// [`Instant::from_bytes`] makes that unrepresentable; this exists for the
212    /// boundary where a length arrives from outside the type system — a socket, a
213    /// column, a file — which is precisely where Rule B's fixed width needs
214    /// enforcing rather than assuming.
215    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        /// A value below 2^256, i.e. inside the UCID range.
252        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        /// A value anywhere in the 512-bit domain.
260        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    // ---- the alphabet ----
270
271    #[test]
272    fn alphabet_is_ascii_ascending() {
273        // This is the load-bearing property behind Rule S for UCID: if the
274        // alphabet were not monotonic in ASCII, lexicographic order would not be
275        // numeric order and the identifier would silently stop being sortable.
276        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        // The confusable and unfortunate letters are absent.
281        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    // ---- Rule I: range ----
288
289    #[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        // ...and well above it.
310        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        // 52 characters of 5 bits is 260 bits against a 256-bit value, so the
317        // leading character carries exactly one significant bit.
318        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    // ---- round trips ----
327
328    #[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            // Text round trip too.
347            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        // The UC-P0 harness already checks these against the RFC; repeating two
360        // here keeps the library honest independently of the harness.
361        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    // ---- parsing leniency and strictness ----
382
383    #[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        // Lowercase.
389        assert_eq!(Ucid::parse(&canonical.to_lowercase()).unwrap(), u);
390        // Hyphens for readability are ignored.
391        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        // I, L and O substitute for 1, 1 and 0.
403        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        // Wrong length, both ways.
412        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        // U is excluded from the alphabet and is not a substitution.
417        let bad = alloc::format!("U{}", &s[1..]);
418        assert_eq!(Ucid::parse(&bad).unwrap_err().code, Code::E0032);
419        // Non-alphabet characters.
420        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        // A syntactically valid 52-char string can still exceed 2^256.
425        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    // ---- Rule S: the ordering proofs, fuzzed ----
431
432    #[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        // Include the extremes and some near-boundary values.
439        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        // And pairwise, which catches an ordering bug the sort could mask.
451        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    // ---- Rule I: UCID is not an identifier ----
484
485    #[test]
486    fn ucid_has_no_entropy() {
487        // Rule I says UCID "contains no randomness" and must not be used to
488        // identify concurrent events. Two separate claims, both checkable.
489
490        // 1. It is a pure function of the instant, so concurrent events collide.
491        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        // 2. The low digits are structurally constrained, not merely non-random.
496        //    §2.4 guarantees a nanosecond-clock reading has at least 21 trailing
497        //    base-5 zeros, so successive nanoseconds are a tiny step across the
498        //    2^256 range and share a long prefix.
499        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        // Named `earlier`/`later` rather than `first`/`second`: in a time library
505        // an ordinal `second` is genuinely ambiguous, which is exactly what the
506        // foreign-unit lint objects to.
507        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        // 3. Present-epoch instants all share a long zero prefix, because the
527        //    epoch occupies a narrow band of the UCID range.
528        assert!(earlier.as_str().starts_with("00000000000"));
529    }
530
531    // ---- Rule B: the binary form ----
532
533    #[test]
534    fn binary_form_is_fixed_width_and_not_minimal() {
535        // Rule B forbids minimal encodings: a small value must still occupy the
536        // full 64 bytes, or the width would depend on the value (F5).
537        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        // The two encodings are of the same number, so a UCID-range value's
579        // binary form must have 32 zero bytes on the left.
580        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}