Skip to main content

rok_ids/
ulid.rs

1use std::fmt;
2use std::str::FromStr;
3use std::sync::Mutex;
4use std::time::{SystemTime, UNIX_EPOCH};
5
6use rand::RngCore;
7use serde::{Deserialize, Serialize};
8
9use crate::IdError;
10
11const CROCKFORD: &[u8; 32] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ";
12const CROCKFORD_DECODE: [i8; 128] = build_decode_table();
13
14const fn build_decode_table() -> [i8; 128] {
15    let mut table = [-1i8; 128];
16    let alpha = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ";
17    let mut i = 0usize;
18    while i < 32 {
19        table[alpha[i] as usize] = i as i8;
20        // also accept lowercase
21        if alpha[i].is_ascii_uppercase() {
22            table[(alpha[i] + 32) as usize] = i as i8;
23        }
24        i += 1;
25    }
26    table
27}
28
29fn encode_ulid(ts_ms: u64, random: &[u8; 10]) -> [u8; 26] {
30    let mut chars = [0u8; 26];
31
32    // 48-bit timestamp into 10 chars (50-bit slot, 2 MSBs always 0)
33    chars[0] = CROCKFORD[((ts_ms >> 45) & 0x1F) as usize];
34    chars[1] = CROCKFORD[((ts_ms >> 40) & 0x1F) as usize];
35    chars[2] = CROCKFORD[((ts_ms >> 35) & 0x1F) as usize];
36    chars[3] = CROCKFORD[((ts_ms >> 30) & 0x1F) as usize];
37    chars[4] = CROCKFORD[((ts_ms >> 25) & 0x1F) as usize];
38    chars[5] = CROCKFORD[((ts_ms >> 20) & 0x1F) as usize];
39    chars[6] = CROCKFORD[((ts_ms >> 15) & 0x1F) as usize];
40    chars[7] = CROCKFORD[((ts_ms >> 10) & 0x1F) as usize];
41    chars[8] = CROCKFORD[((ts_ms >> 5) & 0x1F) as usize];
42    chars[9] = CROCKFORD[(ts_ms & 0x1F) as usize];
43
44    // 80-bit randomness into 16 chars
45    let mut r: u128 = 0;
46    for &b in random.iter() {
47        r = (r << 8) | b as u128;
48    }
49    for i in 0..16usize {
50        chars[25 - i] = CROCKFORD[((r >> (5 * i)) & 0x1F) as usize];
51    }
52
53    chars
54}
55
56struct MonotonicState {
57    last_ms: u64,
58    last_random: [u8; 10],
59}
60
61static MONOTONIC: Mutex<Option<MonotonicState>> = Mutex::new(None);
62
63/// A ULID — 26-char Crockford base32, lexicographically sortable by time.
64#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
65pub struct Ulid(String);
66
67impl Ulid {
68    /// Generate a new ULID using the current timestamp and random bits.
69    pub fn generate() -> Self {
70        let ts = SystemTime::now()
71            .duration_since(UNIX_EPOCH)
72            .expect("time went backwards")
73            .as_millis() as u64;
74
75        let mut rnd = [0u8; 10];
76        rand::thread_rng().fill_bytes(&mut rnd);
77
78        let chars = encode_ulid(ts, &rnd);
79        Self(String::from_utf8(chars.to_vec()).unwrap())
80    }
81
82    /// Generate a monotonically increasing ULID: within the same millisecond
83    /// the random component is incremented instead of re-randomised.
84    pub fn monotonic() -> Self {
85        let ts = SystemTime::now()
86            .duration_since(UNIX_EPOCH)
87            .expect("time went backwards")
88            .as_millis() as u64;
89
90        let mut guard = MONOTONIC.lock().unwrap();
91        let random = match &mut *guard {
92            Some(state) if state.last_ms == ts => {
93                // increment least-significant byte of random part
94                let mut i = 9usize;
95                loop {
96                    let (val, overflow) = state.last_random[i].overflowing_add(1);
97                    state.last_random[i] = val;
98                    if !overflow {
99                        break;
100                    }
101                    if i == 0 {
102                        // full overflow — just randomise again
103                        rand::thread_rng().fill_bytes(&mut state.last_random);
104                        break;
105                    }
106                    i -= 1;
107                }
108                state.last_random
109            }
110            _ => {
111                let mut rnd = [0u8; 10];
112                rand::thread_rng().fill_bytes(&mut rnd);
113                *guard = Some(MonotonicState {
114                    last_ms: ts,
115                    last_random: rnd,
116                });
117                rnd
118            }
119        };
120
121        let chars = encode_ulid(ts, &random);
122        Self(String::from_utf8(chars.to_vec()).unwrap())
123    }
124
125    pub fn as_str(&self) -> &str {
126        &self.0
127    }
128
129    /// Extract the timestamp component in milliseconds.
130    pub fn timestamp_ms(&self) -> u64 {
131        let bytes = self.0.as_bytes();
132        let mut ts: u64 = 0;
133        for &byte in bytes.iter().take(10) {
134            let ch = byte as usize;
135            let v = if ch < 128 { CROCKFORD_DECODE[ch] } else { -1 };
136            ts = (ts << 5) | v as u64;
137        }
138        ts
139    }
140}
141
142impl fmt::Display for Ulid {
143    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144        f.write_str(&self.0)
145    }
146}
147
148impl FromStr for Ulid {
149    type Err = IdError;
150
151    fn from_str(s: &str) -> Result<Self, Self::Err> {
152        if s.len() != 26 {
153            return Err(IdError::InvalidFormat("ulid", "expected 26 chars"));
154        }
155        for ch in s.chars() {
156            let idx = ch as usize;
157            if idx >= 128 || CROCKFORD_DECODE[idx] < 0 {
158                return Err(IdError::InvalidFormat(
159                    "ulid",
160                    "invalid Crockford base32 char",
161                ));
162            }
163        }
164        Ok(Self(s.to_ascii_uppercase()))
165    }
166}
167
168impl AsRef<str> for Ulid {
169    fn as_ref(&self) -> &str {
170        &self.0
171    }
172}
173
174#[cfg(feature = "sqlx-postgres")]
175mod sqlx_impl {
176    use super::Ulid;
177    use sqlx::{
178        encode::IsNull,
179        error::BoxDynError,
180        postgres::{PgArgumentBuffer, PgTypeInfo, PgValueRef},
181    };
182
183    impl sqlx::Type<sqlx::Postgres> for Ulid {
184        fn type_info() -> PgTypeInfo {
185            <String as sqlx::Type<sqlx::Postgres>>::type_info()
186        }
187    }
188
189    impl<'q> sqlx::Encode<'q, sqlx::Postgres> for Ulid {
190        fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
191            <String as sqlx::Encode<'q, sqlx::Postgres>>::encode_by_ref(&self.0, buf)
192        }
193    }
194
195    impl<'r> sqlx::Decode<'r, sqlx::Postgres> for Ulid {
196        fn decode(value: PgValueRef<'r>) -> Result<Self, BoxDynError> {
197            let s = <String as sqlx::Decode<sqlx::Postgres>>::decode(value)?;
198            Ok(Self(s))
199        }
200    }
201}