use std::time::{SystemTime, UNIX_EPOCH};
#[cfg(test)]
mod tests;
pub(crate) const EPOCH_MS: u64 = 1_400_000_000_000;
pub(crate) const MAX_TIME_MS: u64 = 1_000 * 4_294_967_295 + EPOCH_MS;
const ALPHABET: &[u8; 62] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
const ENCODED_LEN: usize = 27;
fn base62(bytes: &[u8; 20]) -> [u8; ENCODED_LEN] {
let mut words = [0_u32; 5];
let (chunks, _) = bytes.as_chunks::<4>();
for (word, chunk) in words.iter_mut().zip(chunks) {
*word = u32::from_be_bytes(*chunk);
}
let mut digits = [b'0'; ENCODED_LEN];
for digit in digits.iter_mut().rev() {
let mut remainder = 0_u64;
for word in &mut words {
let value = (remainder << 32) | u64::from(*word);
*word = u32::try_from(value / 62).unwrap_or(0);
remainder = value % 62;
}
*digit = ALPHABET[usize::try_from(remainder).unwrap_or(0)];
}
digits
}
pub(crate) fn encode(prefix: &str, seconds: u64, payload: &[u8; 16]) -> String {
let mut bytes = [0_u8; 20];
bytes[..4].copy_from_slice(&seconds.to_be_bytes()[4..]);
bytes[4..].copy_from_slice(payload);
let mut id = String::with_capacity(prefix.len() + 1 + ENCODED_LEN);
id.push_str(prefix);
id.push('_');
id.extend(base62(&bytes).iter().map(|&b| char::from(b)));
id
}
pub(crate) fn from_parts(prefix: &str, time_ms: u64, payload: &[u8; 16]) -> Option<String> {
if !(EPOCH_MS..=MAX_TIME_MS).contains(&time_ms) {
return None;
}
Some(encode(prefix, (time_ms - EPOCH_MS) / 1_000, payload))
}
pub(crate) fn random(prefix: &str) -> Option<String> {
let now_ms = SystemTime::now()
.duration_since(UNIX_EPOCH)
.ok()
.and_then(|elapsed| u64::try_from(elapsed.as_millis()).ok());
random_at(prefix, now_ms, |buf| getrandom::fill(buf))
}
fn random_at(
prefix: &str,
now_ms: Option<u64>,
fill: impl FnOnce(&mut [u8; 16]) -> Result<(), getrandom::Error>,
) -> Option<String> {
let now_ms = now_ms?;
let mut payload = [0_u8; 16];
fill(&mut payload).ok()?;
from_parts(prefix, now_ms, &payload)
}