use crate::core::encoding::{
EncodingFormat, encode_base32, encode_base58, encode_base64, encode_base64_url, encode_bits,
encode_bytes_spaced, encode_hex, encode_hex_upper,
};
use crate::core::error::{IdtError, Result};
use crate::core::id::{
IdEncodings, IdGenerator, IdKind, InspectionResult, ParsedId, Timestamp, ValidationResult,
};
use rand::Rng;
use serde_json::json;
use std::sync::OnceLock;
use std::sync::atomic::{AtomicU32, Ordering};
const XID_ALPHABET: &[u8] = b"0123456789abcdefghijklmnopqrstuv";
static MACHINE_ID: OnceLock<[u8; 3]> = OnceLock::new();
static XID_COUNTER: AtomicU32 = AtomicU32::new(0);
static XID_COUNTER_INIT: OnceLock<()> = OnceLock::new();
fn machine_id() -> &'static [u8; 3] {
MACHINE_ID.get_or_init(|| {
let mut rng = rand::thread_rng();
let mut buf = [0u8; 3];
rng.fill(&mut buf);
buf
})
}
fn next_xid_counter() -> u32 {
XID_COUNTER_INIT.get_or_init(|| {
let mut rng = rand::thread_rng();
XID_COUNTER.store(rng.r#gen::<u32>() & 0xFF_FFFF, Ordering::SeqCst);
});
XID_COUNTER.fetch_add(1, Ordering::SeqCst) & 0xFF_FFFF
}
pub struct XidGenerator;
impl Default for XidGenerator {
fn default() -> Self {
Self
}
}
impl XidGenerator {
pub fn new() -> Self {
Self
}
}
impl IdGenerator for XidGenerator {
fn generate(&self) -> Result<String> {
let now = chrono::Utc::now().timestamp() as u32;
let mid = machine_id();
let pid = (std::process::id() & 0xFFFF) as u16;
let counter = next_xid_counter();
let mut bytes = [0u8; 12];
bytes[0..4].copy_from_slice(&now.to_be_bytes());
bytes[4..7].copy_from_slice(mid);
bytes[7..9].copy_from_slice(&pid.to_be_bytes());
bytes[9] = ((counter >> 16) & 0xFF) as u8;
bytes[10] = ((counter >> 8) & 0xFF) as u8;
bytes[11] = (counter & 0xFF) as u8;
Ok(xid_encode(&bytes))
}
}
fn xid_encode(bytes: &[u8; 12]) -> String {
let mut dst = [0u8; 20];
dst[19] = XID_ALPHABET[(bytes[11] & 0x1f) as usize];
dst[18] = XID_ALPHABET[((bytes[11] >> 5) | (bytes[10] << 3)) as usize & 0x1f];
dst[17] = XID_ALPHABET[((bytes[10] >> 2) & 0x1f) as usize];
dst[16] = XID_ALPHABET[((bytes[10] >> 7) | (bytes[9] << 1)) as usize & 0x1f];
dst[15] = XID_ALPHABET[((bytes[9] >> 4) | (bytes[8] << 4)) as usize & 0x1f];
dst[14] = XID_ALPHABET[(bytes[8] >> 1 & 0x1f) as usize];
dst[13] = XID_ALPHABET[((bytes[8] >> 6) | (bytes[7] << 2)) as usize & 0x1f];
dst[12] = XID_ALPHABET[((bytes[7] >> 3) & 0x1f) as usize];
dst[11] = XID_ALPHABET[(bytes[6] & 0x1f) as usize];
dst[10] = XID_ALPHABET[((bytes[6] >> 5) | (bytes[5] << 3)) as usize & 0x1f];
dst[9] = XID_ALPHABET[((bytes[5] >> 2) & 0x1f) as usize];
dst[8] = XID_ALPHABET[((bytes[5] >> 7) | (bytes[4] << 1)) as usize & 0x1f];
dst[7] = XID_ALPHABET[((bytes[4] >> 4) | (bytes[3] << 4)) as usize & 0x1f];
dst[6] = XID_ALPHABET[((bytes[3] >> 1) & 0x1f) as usize];
dst[5] = XID_ALPHABET[((bytes[3] >> 6) | (bytes[2] << 2)) as usize & 0x1f];
dst[4] = XID_ALPHABET[((bytes[2] >> 3) & 0x1f) as usize];
dst[3] = XID_ALPHABET[(bytes[1] & 0x1f) as usize];
dst[2] = XID_ALPHABET[((bytes[1] >> 5) | (bytes[0] << 3)) as usize & 0x1f];
dst[1] = XID_ALPHABET[((bytes[0] >> 2) & 0x1f) as usize];
dst[0] = XID_ALPHABET[(bytes[0] >> 7) as usize];
String::from_utf8(dst.to_vec()).unwrap()
}
fn xid_decode(s: &str) -> Result<[u8; 12]> {
if s.len() != 20 {
return Err(IdtError::ParseError(
"Xid must be 20 characters".to_string(),
));
}
let src: Vec<u8> = s
.chars()
.map(|c| {
xid_char_value(c)
.ok_or_else(|| IdtError::ParseError(format!("Invalid xid character: '{}'", c)))
})
.collect::<Result<Vec<u8>>>()?;
let mut bytes = [0u8; 12];
bytes[11] = (src[19]) | (src[18] << 5);
bytes[10] = (src[18] >> 3) | (src[17] << 2) | (src[16] << 7);
bytes[9] = (src[16] >> 1) | (src[15] << 4);
bytes[8] = (src[15] >> 4) | (src[14] << 1) | (src[13] << 6);
bytes[7] = (src[13] >> 2) | (src[12] << 3);
bytes[6] = (src[11]) | (src[10] << 5);
bytes[5] = (src[10] >> 3) | (src[9] << 2) | (src[8] << 7);
bytes[4] = (src[8] >> 1) | (src[7] << 4);
bytes[3] = (src[7] >> 4) | (src[6] << 1) | (src[5] << 6);
bytes[2] = (src[5] >> 2) | (src[4] << 3);
bytes[1] = (src[3]) | (src[2] << 5);
bytes[0] = (src[2] >> 3) | (src[1] << 2) | (src[0] << 7);
Ok(bytes)
}
fn xid_char_value(c: char) -> Option<u8> {
match c {
'0'..='9' => Some(c as u8 - b'0'),
'a'..='v' => Some(c as u8 - b'a' + 10),
_ => None,
}
}
pub struct ParsedXid {
bytes: [u8; 12],
input: String,
}
impl ParsedXid {
pub fn parse(input: &str) -> Result<Self> {
let input_trimmed = input.trim();
let bytes = xid_decode(input_trimmed)?;
Ok(Self {
bytes,
input: input_trimmed.to_string(),
})
}
fn timestamp_secs(&self) -> u32 {
u32::from_be_bytes([self.bytes[0], self.bytes[1], self.bytes[2], self.bytes[3]])
}
fn machine_id_bytes(&self) -> &[u8] {
&self.bytes[4..7]
}
fn process_id(&self) -> u16 {
u16::from_be_bytes([self.bytes[7], self.bytes[8]])
}
fn counter(&self) -> u32 {
((self.bytes[9] as u32) << 16) | ((self.bytes[10] as u32) << 8) | (self.bytes[11] as u32)
}
}
impl ParsedId for ParsedXid {
fn kind(&self) -> IdKind {
IdKind::Xid
}
fn canonical(&self) -> String {
xid_encode(&self.bytes)
}
fn as_bytes(&self) -> Vec<u8> {
self.bytes.to_vec()
}
fn timestamp(&self) -> Option<Timestamp> {
Some(Timestamp::from_secs(self.timestamp_secs() as u64))
}
fn inspect(&self) -> InspectionResult {
let bytes = self.as_bytes();
let timestamp = self.timestamp().unwrap();
let components = json!({
"timestamp_secs": self.timestamp_secs(),
"machine_id_hex": encode_hex(self.machine_id_bytes()),
"process_id": self.process_id(),
"counter": self.counter(),
});
InspectionResult {
id_type: "xid".to_string(),
input: self.input.clone(),
canonical: self.canonical(),
valid: true,
timestamp: Some(timestamp),
timestamp_iso: Some(timestamp.to_iso8601()),
timestamp_local_iso: None,
version: None,
variant: None,
random_bits: None,
components: Some(components),
encodings: IdEncodings {
hex: encode_hex(&bytes),
base32: encode_base32(&bytes),
base58: encode_base58(&bytes),
base64: encode_base64(&bytes),
int: None,
},
}
}
fn validate(&self) -> ValidationResult {
let ts = self.timestamp_secs();
let now = chrono::Utc::now().timestamp() as u32;
if ts > now + 86400 {
ValidationResult::invalid("Timestamp is in the future")
} else {
ValidationResult::valid("xid")
}
}
fn encode(&self, format: EncodingFormat) -> String {
let bytes = self.as_bytes();
match format {
EncodingFormat::Canonical => self.canonical(),
EncodingFormat::Hex => encode_hex(&bytes),
EncodingFormat::HexUpper => encode_hex_upper(&bytes),
EncodingFormat::Base32 => encode_base32(&bytes),
EncodingFormat::Base32Hex => encode_base32(&bytes),
EncodingFormat::Base58 => encode_base58(&bytes),
EncodingFormat::Base64 => encode_base64(&bytes),
EncodingFormat::Base64Url => encode_base64_url(&bytes),
EncodingFormat::Binary => String::from_utf8_lossy(&bytes).to_string(),
EncodingFormat::Bits => encode_bits(&bytes),
EncodingFormat::Int => {
let mut val: u128 = 0;
for &b in &bytes {
val = (val << 8) | b as u128;
}
val.to_string()
}
EncodingFormat::Bytes => encode_bytes_spaced(&bytes),
}
}
}
pub fn is_xid(input: &str) -> bool {
ParsedXid::parse(input).is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate() {
let generator = XidGenerator::new();
let id = generator.generate().unwrap();
assert_eq!(id.len(), 20);
assert!(id.chars().all(|c| matches!(c, '0'..='9' | 'a'..='v')));
}
#[test]
fn test_roundtrip() {
let generator = XidGenerator::new();
let id = generator.generate().unwrap();
let parsed = ParsedXid::parse(&id).unwrap();
assert_eq!(parsed.canonical(), id);
}
#[test]
fn test_encode_decode_roundtrip() {
let mut bytes = [0u8; 12];
for (i, byte) in bytes.iter_mut().enumerate() {
*byte = (i * 17 + 3) as u8;
}
let encoded = xid_encode(&bytes);
let decoded = xid_decode(&encoded).unwrap();
assert_eq!(bytes, decoded);
}
#[test]
fn test_has_timestamp() {
let generator = XidGenerator::new();
let id = generator.generate().unwrap();
let parsed = ParsedXid::parse(&id).unwrap();
let ts = parsed.timestamp().unwrap();
let now = chrono::Utc::now().timestamp() as u64;
assert!((now * 1000).abs_diff(ts.millis) < 10_000);
}
}