use std::fmt::Display;
use base64::{engine::general_purpose, Engine};
use rand::RngExt;
use crate::{util::fnv64, Error};
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct ID(String);
impl ID {
pub fn new(msg: &[u8]) -> Self {
let mut checksum = [0u8; 12];
checksum[0..4].copy_from_slice(&(msg.len() as u32).to_le_bytes());
let digest = &fnv64(&checksum[0..4], msg).to_le_bytes();
checksum[4..].copy_from_slice(digest); let checksum_b64 = general_purpose::URL_SAFE.encode(checksum);
let mut rng = rand::rng();
let nonce: u32 = rng.random();
let nonce_b64 = general_purpose::URL_SAFE_NO_PAD.encode(&nonce.to_le_bytes()[..3]);
ID(format!("{}.{}", checksum_b64, nonce_b64))
}
pub fn checksum(&self) -> &str {
&self.0[0..16]
}
pub fn nonce(&self) -> &str {
&self.0[17..]
}
pub fn from_unchecked(value: String) -> Self {
ID(value)
}
}
impl Display for ID {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl TryFrom<String> for ID {
type Error = Error;
fn try_from(value: String) -> Result<Self, Self::Error> {
if let Some((hash, nonce)) = value.split_once('.') {
let h = general_purpose::URL_SAFE.decode(hash);
let n = general_purpose::URL_SAFE_NO_PAD.decode(nonce);
if h.is_ok_and(|h| h.len() == 12) && n.is_ok_and(|n| n.len() == 3) {
return Ok(ID(value.to_string()));
}
}
Err(Error::InvalidMsgID(value))
}
}
impl AsRef<str> for ID {
fn as_ref(&self) -> &str {
self.0.as_str()
}
}