use crate::{
decode_crockford32, encode_crockford32, Error, ALPHABET, COUNTER_MAX, EPOCH, ID_LENGTH,
TIMESTAMP_MAX, WORKER_MAX,
};
use core::fmt;
use core::str::FromStr;
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
pub struct SigId26 {
bytes: [u8; ID_LENGTH],
}
impl SigId26 {
pub fn from_bytes(bytes: [u8; ID_LENGTH]) -> Result<Self, Error> {
for &b in &bytes {
match b {
b'0'..=b'9' => {}
b'A'..=b'Z' => {
let mut found = false;
for &ch in ALPHABET {
if ch == b {
found = true;
break;
}
}
if !found {
return Err(Error::InvalidCharacter);
}
}
_ => return Err(Error::InvalidCharacter),
}
}
Ok(Self { bytes })
}
pub fn from_raw_bytes(raw: [u8; 16]) -> Self {
let mut encoded = [0u8; 26];
encode_crockford32(&raw, &mut encoded);
Self { bytes: encoded }
}
pub fn raw_new(timestamp: u64, counter: u16, random: u64) -> Self {
Self::raw_new_with_worker(timestamp, 0, counter, random)
}
pub fn raw_new_with_worker(timestamp: u64, worker_id: u16, counter: u16, random: u64) -> Self {
let ts = timestamp & TIMESTAMP_MAX; let worker = worker_id & WORKER_MAX; let cnt = counter & COUNTER_MAX; let rand = random & 0x3FFFFFFFFFFFF;
let mut raw = [0u8; 16];
raw[0] = ((ts >> 40) & 0xFF) as u8;
raw[1] = ((ts >> 32) & 0xFF) as u8;
raw[2] = ((ts >> 24) & 0xFF) as u8;
raw[3] = ((ts >> 16) & 0xFF) as u8;
raw[4] = ((ts >> 8) & 0xFF) as u8;
raw[5] = (ts & 0xFF) as u8;
raw[6] = ((worker >> 8) & 0xFF) as u8;
raw[7] = (worker & 0xFF) as u8;
raw[8] = ((cnt >> 6) & 0xFF) as u8;
raw[9] = ((cnt & 0x3F) << 2) as u8;
raw[9] |= ((rand >> 48) & 0x03) as u8;
raw[10] = ((rand >> 40) & 0xFF) as u8;
raw[11] = ((rand >> 32) & 0xFF) as u8;
raw[12] = ((rand >> 24) & 0xFF) as u8;
raw[13] = ((rand >> 16) & 0xFF) as u8;
raw[14] = ((rand >> 8) & 0xFF) as u8;
raw[15] = (rand & 0xFF) as u8;
Self::from_raw_bytes(raw)
}
pub const fn as_bytes(&self) -> &[u8; ID_LENGTH] {
&self.bytes
}
pub fn as_raw_bytes(&self) -> [u8; 16] {
let mut raw = [0u8; 16];
let _ = decode_crockford32(&self.bytes, &mut raw);
raw
}
pub fn timestamp(&self) -> u64 {
let raw = self.as_raw_bytes();
((raw[0] as u64) << 40)
| ((raw[1] as u64) << 32)
| ((raw[2] as u64) << 24)
| ((raw[3] as u64) << 16)
| ((raw[4] as u64) << 8)
| (raw[5] as u64)
}
pub fn timestamp_ms(&self) -> u64 {
self.timestamp() + EPOCH
}
pub fn counter(&self) -> u16 {
let raw = self.as_raw_bytes();
let high = (raw[8] as u16) << 6;
let low = (raw[9] >> 2) as u16;
high | low
}
pub fn random(&self) -> u64 {
let raw = self.as_raw_bytes();
let b9 = (raw[9] & 0x03) as u64;
(b9 << 48)
| ((raw[10] as u64) << 40)
| ((raw[11] as u64) << 32)
| ((raw[12] as u64) << 24)
| ((raw[13] as u64) << 16)
| ((raw[14] as u64) << 8)
| (raw[15] as u64)
}
pub fn worker_id(&self) -> u16 {
let raw = self.as_raw_bytes();
((raw[6] as u16) << 8) | (raw[7] as u16)
}
pub fn is_valid(&self) -> bool {
for &b in &self.bytes {
match b {
b'0'..=b'9' => {}
b'A'..=b'Z' => {
let mut found = false;
for &ch in ALPHABET {
if ch == b {
found = true;
break;
}
}
if !found {
return false;
}
}
_ => return false,
}
}
true
}
}
impl fmt::Display for SigId26 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = core::str::from_utf8(&self.bytes).map_err(|_| fmt::Error)?;
f.write_str(s)
}
}
impl fmt::Debug for SigId26 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "SigId26(")?;
fmt::Display::fmt(self, f)?;
write!(f, ")")
}
}
impl FromStr for SigId26 {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let cleaned = s.trim();
if cleaned.len() != ID_LENGTH {
return Err(Error::InvalidLength);
}
let mut raw = [0u8; 16];
decode_crockford32(cleaned.as_bytes(), &mut raw)?;
let mut encoded = [0u8; 26];
encode_crockford32(&raw, &mut encoded);
Ok(Self { bytes: encoded })
}
}
impl AsRef<[u8]> for SigId26 {
fn as_ref(&self) -> &[u8] {
&self.bytes
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::string::ToString;
#[test]
fn test_raw_new_simple() {
let ts = 1234567890;
let counter = 42;
let random = 0x123456789abcd;
let id = SigId26::raw_new(ts, counter, random);
assert_eq!(id.timestamp(), ts, "timestamp mismatch");
assert_eq!(id.worker_id(), 0, "worker_id should be 0 for Simple");
assert_eq!(id.counter(), counter, "counter mismatch");
assert_eq!(id.random(), random, "random mismatch");
}
#[test]
fn test_raw_new_with_worker() {
let ts = 1234567890;
let worker = 0x1234;
let counter = 42;
let random = 0x123456789abcd;
let id = SigId26::raw_new_with_worker(ts, worker, counter, random);
assert_eq!(id.timestamp(), ts, "timestamp mismatch");
assert_eq!(id.worker_id(), worker, "worker_id mismatch");
assert_eq!(id.counter(), counter, "counter mismatch");
assert_eq!(id.random(), random, "random mismatch");
}
#[test]
fn test_worker_id_bounds() {
let id = SigId26::raw_new_with_worker(1234567890, u16::MAX, 0, 0);
assert_eq!(id.worker_id(), u16::MAX);
}
#[test]
fn test_counter_bounds() {
let id = SigId26::raw_new_with_worker(1234567890, 0, 16383, 0);
assert_eq!(id.counter(), 16383);
let id = SigId26::raw_new_with_worker(1234567890, 0, 16384, 0);
assert_eq!(id.counter(), 0);
}
#[test]
fn test_random_bounds() {
let max_random = 0x3FFFFFFFFFFFF;
let id = SigId26::raw_new_with_worker(1234567890, 0, 0, max_random);
assert_eq!(id.random(), max_random);
let id = SigId26::raw_new_with_worker(1234567890, 0, 0, 0xFFFFFFFFFFFFFFFF);
assert_eq!(id.random(), 0x3FFFFFFFFFFFF);
}
#[test]
fn test_from_str_roundtrip() {
let id1 = SigId26::raw_new(1234567890, 42, 0x123456789abcd);
let s = id1.to_string();
let id2 = SigId26::from_str(&s).unwrap();
assert_eq!(id1.as_bytes(), id2.as_bytes());
}
#[test]
fn test_enterprise_roundtrip() {
let ts = 1234567890;
let worker = 0x1234;
let counter = 42;
let random = 0x123456789abcd;
let id1 = SigId26::raw_new_with_worker(ts, worker, counter, random);
let s = id1.to_string();
let id2 = SigId26::from_str(&s).unwrap();
assert_eq!(id1.as_bytes(), id2.as_bytes(), "bytes mismatch");
assert_eq!(
id2.worker_id(),
worker,
"worker_id mismatch after roundtrip"
);
assert_eq!(id2.counter(), counter, "counter mismatch after roundtrip");
assert_eq!(id2.random(), random, "random mismatch after roundtrip");
}
#[test]
fn test_all_max() {
let max_random = 0x3FFFFFFFFFFFF;
let id = SigId26::raw_new_with_worker(TIMESTAMP_MAX, u16::MAX, COUNTER_MAX, max_random);
assert_eq!(id.timestamp(), TIMESTAMP_MAX);
assert_eq!(id.worker_id(), u16::MAX);
assert_eq!(id.counter(), COUNTER_MAX);
assert_eq!(id.random(), max_random);
}
#[test]
fn test_all_zero() {
let id = SigId26::raw_new_with_worker(0, 0, 0, 0);
assert_eq!(id.timestamp(), 0);
assert_eq!(id.worker_id(), 0);
assert_eq!(id.counter(), 0);
assert_eq!(id.random(), 0);
}
#[test]
fn test_simple_vs_enterprise_consistency() {
let simple = SigId26::raw_new(1234567890, 42, 0x123456789abcd);
let enterprise = SigId26::raw_new_with_worker(1234567890, 0, 42, 0x123456789abcd);
assert_eq!(
simple.as_bytes(),
enterprise.as_bytes(),
"Simple and Enterprise with worker_id=0 should be identical"
);
}
}