84 lines
2.4 KiB
Rust
84 lines
2.4 KiB
Rust
use std::sync::atomic::{AtomicU32, Ordering};
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
|
|
/// Base32-sort character set used by ATProto TIDs
|
|
const BASE32_SORT: &[u8; 32] = b"234567abcdefghijklmnopqrstuvwxyz";
|
|
|
|
/// Atomic counter for clock ID to avoid collisions within the same microsecond
|
|
static CLOCK_ID: AtomicU32 = AtomicU32::new(0);
|
|
|
|
/// Generate a TID (Timestamp Identifier) per the ATProto specification.
|
|
///
|
|
/// Format: 13 characters of base32-sort encoding
|
|
/// - Bits 63..10: microsecond timestamp (54 bits)
|
|
/// - Bits 9..0: clock ID (10 bits, wrapping counter)
|
|
///
|
|
/// The high bit (bit 63) is always 0 to keep the value positive.
|
|
pub fn generate_tid() -> String {
|
|
let micros = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.expect("system clock before UNIX epoch")
|
|
.as_micros() as u64;
|
|
|
|
let clk = CLOCK_ID.fetch_add(1, Ordering::Relaxed) & 0x3FF; // 10-bit wrap
|
|
|
|
// Combine: timestamp in upper 54 bits, clock ID in lower 10 bits
|
|
let tid_value: u64 = (micros << 10) | (clk as u64);
|
|
|
|
encode_base32_sort(tid_value)
|
|
}
|
|
|
|
/// Encode a u64 into a 13-character base32-sort string (big-endian, zero-padded).
|
|
fn encode_base32_sort(mut value: u64) -> String {
|
|
let mut buf = [b'2'; 13]; // '2' is 0 in base32-sort
|
|
|
|
for i in (0..13).rev() {
|
|
buf[i] = BASE32_SORT[(value & 0x1F) as usize];
|
|
value >>= 5;
|
|
}
|
|
|
|
// Safety: all chars are ASCII
|
|
String::from_utf8(buf.to_vec()).expect("base32-sort is always valid UTF-8")
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn tid_length() {
|
|
let tid = generate_tid();
|
|
assert_eq!(tid.len(), 13);
|
|
}
|
|
|
|
#[test]
|
|
fn tid_charset() {
|
|
let tid = generate_tid();
|
|
let valid: &str = "234567abcdefghijklmnopqrstuvwxyz";
|
|
for c in tid.chars() {
|
|
assert!(valid.contains(c), "invalid char in TID: {}", c);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn tid_monotonic() {
|
|
let a = generate_tid();
|
|
let b = generate_tid();
|
|
// TIDs generated in sequence should sort correctly
|
|
assert!(a < b || a == b, "TIDs should be monotonically increasing: {} >= {}", a, b);
|
|
}
|
|
|
|
#[test]
|
|
fn encode_zero() {
|
|
let encoded = encode_base32_sort(0);
|
|
assert_eq!(encoded, "2222222222222");
|
|
}
|
|
|
|
#[test]
|
|
fn encode_known_value() {
|
|
// Verify encoding produces consistent results
|
|
let encoded = encode_base32_sort(1);
|
|
assert_eq!(encoded, "2222222222223");
|
|
}
|
|
}
|