use core::fmt;
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use thiserror::Error;
use crate::entropy::Entropy;
pub const TOKEN_ALPHABET: &[u8; 58] = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
const REJECTION_BOUND: u8 = 232;
const MAX_LEN: usize = 23;
const DEFAULT_ALLOC: usize = 64;
#[derive(Debug, Error, PartialEq, Eq)]
pub enum TokenError {
#[error("token length {0} outside 1..=23")]
Length(usize),
#[error("token contains a character outside the base58 alphabet")]
Alphabet,
#[error("entropy source failed while generating a token: {0}")]
Entropy(String),
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct Token {
len: u8,
buf: [u8; MAX_LEN],
}
impl Token {
pub fn parse(s: &str) -> Result<Self, TokenError> {
let bytes = s.as_bytes();
if bytes.is_empty() || bytes.len() > MAX_LEN {
return Err(TokenError::Length(bytes.len()));
}
if !bytes.iter().all(|b| TOKEN_ALPHABET.contains(b)) {
return Err(TokenError::Alphabet);
}
let mut buf = [0u8; MAX_LEN];
buf[..bytes.len()].copy_from_slice(bytes);
#[allow(clippy::cast_possible_truncation)] Ok(Self {
len: bytes.len() as u8,
buf,
})
}
pub fn generate(len: usize, entropy: &mut impl Entropy) -> Result<Self, TokenError> {
if len == 0 || len > MAX_LEN {
return Err(TokenError::Length(len));
}
let mut buf = [0u8; MAX_LEN];
let mut filled = 0usize;
let mut pool = [0u8; DEFAULT_ALLOC];
while filled < len {
entropy
.fill(&mut pool)
.map_err(|e| TokenError::Entropy(e.to_string()))?;
for &b in &pool {
if b < REJECTION_BOUND {
buf[filled] = TOKEN_ALPHABET[(b % 58) as usize];
filled += 1;
if filled == len {
break;
}
}
}
}
#[allow(clippy::cast_possible_truncation)] Ok(Self {
len: len as u8,
buf,
})
}
#[must_use]
pub fn as_str(&self) -> &str {
core::str::from_utf8(&self.buf[..self.len as usize]).unwrap_or("")
}
}
impl PartialOrd for Token {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Token {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
self.as_str().cmp(other.as_str())
}
}
impl fmt::Display for Token {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl fmt::Debug for Token {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Token({})", self.as_str())
}
}
impl Serialize for Token {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for Token {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct TokenVisitor;
impl de::Visitor<'_> for TokenVisitor {
type Value = Token;
fn expecting(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.write_str("a waggle token string")
}
fn visit_str<E: de::Error>(self, s: &str) -> Result<Token, E> {
Token::parse(s).map_err(de::Error::custom)
}
}
deserializer.deserialize_str(TokenVisitor)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn counting_entropy() -> impl FnMut(&mut [u8]) -> Result<(), crate::EntropyError> {
let mut state = 0u8;
move |buf: &mut [u8]| {
for b in buf.iter_mut() {
state = state.wrapping_add(37);
*b = state;
}
Ok(())
}
}
fn assert_copy<T: Copy>() {}
#[test]
fn size_is_inline_and_copy() {
assert_eq!(core::mem::size_of::<Token>(), 24);
assert_copy::<Token>();
}
#[test]
fn generate_parse_roundtrip() {
let mut entropy = counting_entropy();
let token = Token::generate(8, &mut entropy).unwrap();
assert_eq!(token.as_str().len(), 8);
let reparsed = Token::parse(token.as_str()).unwrap();
assert_eq!(reparsed, token);
}
#[test]
fn parse_rejects_bad_lengths_and_alphabet() {
assert_eq!(Token::parse(""), Err(TokenError::Length(0)));
assert_eq!(
Token::parse("aaaaaaaaaaaaaaaaaaaaaaaa"), Err(TokenError::Length(24))
);
assert_eq!(Token::parse("abc0def"), Err(TokenError::Alphabet)); assert_eq!(Token::parse("abcOdef"), Err(TokenError::Alphabet)); assert_eq!(Token::parse("abc def"), Err(TokenError::Alphabet));
}
#[test]
fn generate_bounds_are_enforced() {
let mut entropy = counting_entropy();
assert_eq!(Token::generate(0, &mut entropy), Err(TokenError::Length(0)));
assert_eq!(
Token::generate(24, &mut entropy),
Err(TokenError::Length(24))
);
assert!(Token::generate(MAX_LEN, &mut entropy).is_ok());
}
#[test]
fn rejection_sampling_skips_biased_bytes() {
let mut phase = 0usize;
let mut entropy = move |buf: &mut [u8]| {
for b in buf.iter_mut() {
*b = if phase % 2 == 0 { 250 } else { 1 };
phase += 1;
}
Ok(())
};
let token = Token::generate(4, &mut entropy).unwrap();
assert_eq!(token.as_str(), "2222");
}
#[test]
fn uniform_distribution_smoke() {
let mut state = 0x2026_0707_u32;
let mut entropy = move |buf: &mut [u8]| {
for b in buf.iter_mut() {
state ^= state << 13;
state ^= state >> 17;
state ^= state << 5;
*b = (state & 0xFF) as u8;
}
Ok(())
};
let mut seen = [0u32; 58];
for _ in 0..2_000 {
let t = Token::generate(8, &mut entropy).unwrap();
for b in t.as_str().bytes() {
let idx = TOKEN_ALPHABET.iter().position(|&a| a == b).unwrap();
seen[idx] += 1;
}
}
assert!(seen.iter().all(|&c| c > 0), "alphabet bucket never hit");
}
#[test]
fn entropy_failure_surfaces() {
let mut broken = |_: &mut [u8]| Err(crate::EntropyError("dead".into()));
let err = Token::generate(8, &mut broken).unwrap_err();
assert!(matches!(err, TokenError::Entropy(_)));
}
#[test]
fn serde_roundtrip_and_validation() {
let mut entropy = counting_entropy();
let token = Token::generate(8, &mut entropy).unwrap();
let json = serde_json::to_string(&token).unwrap();
let back: Token = serde_json::from_str(&json).unwrap();
assert_eq!(back, token);
assert!(serde_json::from_str::<Token>("\"bad token!\"").is_err());
}
}