1use core::fmt;
10use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
11use thiserror::Error;
12
13use crate::entropy::Entropy;
14
15pub const TOKEN_ALPHABET: &[u8; 58] = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
17
18const REJECTION_BOUND: u8 = 232;
20
21const MAX_LEN: usize = 23;
22const DEFAULT_ALLOC: usize = 64;
23
24#[derive(Debug, Error, PartialEq, Eq)]
26pub enum TokenError {
27 #[error("token length {0} outside 1..=23")]
29 Length(usize),
30 #[error("token contains a character outside the base58 alphabet")]
32 Alphabet,
33 #[error("entropy source failed while generating a token: {0}")]
35 Entropy(String),
36}
37
38#[derive(Clone, Copy, PartialEq, Eq, Hash)]
44pub struct Token {
45 len: u8,
46 buf: [u8; MAX_LEN],
47}
48
49impl Token {
50 pub fn parse(s: &str) -> Result<Self, TokenError> {
52 let bytes = s.as_bytes();
53 if bytes.is_empty() || bytes.len() > MAX_LEN {
54 return Err(TokenError::Length(bytes.len()));
55 }
56 if !bytes.iter().all(|b| TOKEN_ALPHABET.contains(b)) {
57 return Err(TokenError::Alphabet);
58 }
59 let mut buf = [0u8; MAX_LEN];
60 buf[..bytes.len()].copy_from_slice(bytes);
61 #[allow(clippy::cast_possible_truncation)] Ok(Self {
63 len: bytes.len() as u8,
64 buf,
65 })
66 }
67
68 pub fn generate(len: usize, entropy: &mut impl Entropy) -> Result<Self, TokenError> {
73 if len == 0 || len > MAX_LEN {
74 return Err(TokenError::Length(len));
75 }
76 let mut buf = [0u8; MAX_LEN];
77 let mut filled = 0usize;
78 let mut pool = [0u8; DEFAULT_ALLOC];
79 while filled < len {
80 entropy
81 .fill(&mut pool)
82 .map_err(|e| TokenError::Entropy(e.to_string()))?;
83 for &b in &pool {
84 if b < REJECTION_BOUND {
85 buf[filled] = TOKEN_ALPHABET[(b % 58) as usize];
86 filled += 1;
87 if filled == len {
88 break;
89 }
90 }
91 }
92 }
93 #[allow(clippy::cast_possible_truncation)] Ok(Self {
95 len: len as u8,
96 buf,
97 })
98 }
99
100 #[must_use]
102 pub fn as_str(&self) -> &str {
103 core::str::from_utf8(&self.buf[..self.len as usize]).unwrap_or("")
107 }
108}
109
110impl PartialOrd for Token {
111 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
112 Some(self.cmp(other))
113 }
114}
115
116impl Ord for Token {
117 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
120 self.as_str().cmp(other.as_str())
121 }
122}
123
124impl fmt::Display for Token {
125 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126 f.write_str(self.as_str())
127 }
128}
129
130impl fmt::Debug for Token {
131 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132 write!(f, "Token({})", self.as_str())
133 }
134}
135
136impl Serialize for Token {
137 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
138 serializer.serialize_str(self.as_str())
139 }
140}
141
142impl<'de> Deserialize<'de> for Token {
143 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
144 struct TokenVisitor;
149 impl de::Visitor<'_> for TokenVisitor {
150 type Value = Token;
151 fn expecting(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
152 f.write_str("a waggle token string")
153 }
154 fn visit_str<E: de::Error>(self, s: &str) -> Result<Token, E> {
155 Token::parse(s).map_err(de::Error::custom)
156 }
157 }
158 deserializer.deserialize_str(TokenVisitor)
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 fn counting_entropy() -> impl FnMut(&mut [u8]) -> Result<(), crate::EntropyError> {
167 let mut state = 0u8;
168 move |buf: &mut [u8]| {
169 for b in buf.iter_mut() {
170 state = state.wrapping_add(37);
171 *b = state;
172 }
173 Ok(())
174 }
175 }
176
177 fn assert_copy<T: Copy>() {}
178
179 #[test]
180 fn size_is_inline_and_copy() {
181 assert_eq!(core::mem::size_of::<Token>(), 24);
182 assert_copy::<Token>();
183 }
184
185 #[test]
186 fn generate_parse_roundtrip() {
187 let mut entropy = counting_entropy();
188 let token = Token::generate(8, &mut entropy).unwrap();
189 assert_eq!(token.as_str().len(), 8);
190 let reparsed = Token::parse(token.as_str()).unwrap();
191 assert_eq!(reparsed, token);
192 }
193
194 #[test]
195 fn parse_rejects_bad_lengths_and_alphabet() {
196 assert_eq!(Token::parse(""), Err(TokenError::Length(0)));
197 assert_eq!(
198 Token::parse("aaaaaaaaaaaaaaaaaaaaaaaa"), Err(TokenError::Length(24))
200 );
201 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));
204 }
205
206 #[test]
207 fn generate_bounds_are_enforced() {
208 let mut entropy = counting_entropy();
209 assert_eq!(Token::generate(0, &mut entropy), Err(TokenError::Length(0)));
210 assert_eq!(
211 Token::generate(24, &mut entropy),
212 Err(TokenError::Length(24))
213 );
214 assert!(Token::generate(MAX_LEN, &mut entropy).is_ok());
215 }
216
217 #[test]
218 fn rejection_sampling_skips_biased_bytes() {
219 let mut phase = 0usize;
222 let mut entropy = move |buf: &mut [u8]| {
223 for b in buf.iter_mut() {
224 *b = if phase % 2 == 0 { 250 } else { 1 };
225 phase += 1;
226 }
227 Ok(())
228 };
229 let token = Token::generate(4, &mut entropy).unwrap();
230 assert_eq!(token.as_str(), "2222");
232 }
233
234 #[test]
235 fn uniform_distribution_smoke() {
236 let mut state = 0x2026_0707_u32;
241 let mut entropy = move |buf: &mut [u8]| {
242 for b in buf.iter_mut() {
243 state ^= state << 13;
244 state ^= state >> 17;
245 state ^= state << 5;
246 *b = (state & 0xFF) as u8;
247 }
248 Ok(())
249 };
250 let mut seen = [0u32; 58];
251 for _ in 0..2_000 {
252 let t = Token::generate(8, &mut entropy).unwrap();
253 for b in t.as_str().bytes() {
254 let idx = TOKEN_ALPHABET.iter().position(|&a| a == b).unwrap();
255 seen[idx] += 1;
256 }
257 }
258 assert!(seen.iter().all(|&c| c > 0), "alphabet bucket never hit");
259 }
260
261 #[test]
262 fn entropy_failure_surfaces() {
263 let mut broken = |_: &mut [u8]| Err(crate::EntropyError("dead".into()));
264 let err = Token::generate(8, &mut broken).unwrap_err();
265 assert!(matches!(err, TokenError::Entropy(_)));
266 }
267
268 #[test]
269 fn serde_roundtrip_and_validation() {
270 let mut entropy = counting_entropy();
271 let token = Token::generate(8, &mut entropy).unwrap();
272 let json = serde_json::to_string(&token).unwrap();
273 let back: Token = serde_json::from_str(&json).unwrap();
274 assert_eq!(back, token);
275 assert!(serde_json::from_str::<Token>("\"bad token!\"").is_err());
276 }
277}