uselesskey_core_base62/
lib.rs1#![forbid(unsafe_code)]
2
3use rand_chacha10::ChaCha20Rng;
9use rand_core10::{Rng, SeedableRng};
10use uselesskey_core_seed::Seed;
11
12pub const BASE62_ALPHABET: &[u8; 62] =
14 b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
15
16const ACCEPT_MAX: u8 = 248; pub fn random_base62(seed: Seed, len: usize) -> String {
24 let mut rng = ChaCha20Rng::from_seed(*seed.bytes());
25 random_base62_with_rng(&mut rng, len)
26}
27
28fn random_base62_with_rng(rng: &mut impl Rng, len: usize) -> String {
29 let mut out = String::with_capacity(len);
30 let mut buf = [0u8; 64];
31
32 while out.len() < len {
33 rng.fill_bytes(&mut buf);
34 let before = out.len();
35
36 for &b in &buf {
37 if b < ACCEPT_MAX {
38 out.push(BASE62_ALPHABET[(b % 62) as usize] as char);
39 if out.len() == len {
40 break;
41 }
42 }
43 }
44
45 if out.len() == before {
46 for &b in &buf {
47 out.push(BASE62_ALPHABET[(b as usize) % 62] as char);
48 if out.len() == len {
49 break;
50 }
51 }
52 }
53 }
54
55 out
56}
57
58#[cfg(test)]
59mod tests {
60 use super::{BASE62_ALPHABET, random_base62, random_base62_with_rng};
61 use rand_chacha10::ChaCha20Rng;
62 use rand_core10::{Infallible, SeedableRng, TryRng};
63 use uselesskey_core_seed::Seed;
64
65 #[test]
66 fn generates_requested_length() {
67 assert_eq!(random_base62(Seed::new([1u8; 32]), 0).len(), 0);
68 assert_eq!(random_base62(Seed::new([1u8; 32]), 73).len(), 73);
69 }
70
71 #[test]
72 fn uses_only_base62_chars() {
73 let value = random_base62(Seed::new([2u8; 32]), 256);
74 assert!(value.bytes().all(|b| BASE62_ALPHABET.contains(&b)));
75 }
76
77 #[test]
78 fn deterministic_for_seeded_rng() {
79 let seed = [7u8; 32];
80 let a = random_base62(Seed::new(seed), 96);
81 let b = random_base62(Seed::new(seed), 96);
82 assert_eq!(a, b);
83 }
84
85 #[test]
86 fn fallback_path_terminates_for_constant_rng() {
87 struct ConstantRng;
88
89 impl TryRng for ConstantRng {
90 type Error = Infallible;
91
92 fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
93 Ok(u32::from_le_bytes([255, 255, 255, 255]))
94 }
95
96 fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
97 Ok(u64::from_le_bytes([255, 255, 255, 255, 255, 255, 255, 255]))
98 }
99
100 fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Self::Error> {
101 self.fill_bytes(dest);
102 Ok(())
103 }
104 }
105
106 impl ConstantRng {
107 fn fill_bytes(&mut self, dest: &mut [u8]) {
108 dest.fill(255);
109 }
110 }
111
112 let mut rng = ConstantRng;
113 let value = random_base62_with_rng(&mut rng, 32);
114 assert_eq!(value.len(), 32);
115 assert!(value.chars().all(|c| c.is_ascii_alphanumeric()));
116 }
117
118 #[test]
119 fn output_uses_diverse_alphabet() {
120 let mut rng = ChaCha20Rng::from_seed([42u8; 32]);
123 let out = random_base62_with_rng(&mut rng, 256);
124 let unique: std::collections::HashSet<char> = out.chars().collect();
125 assert!(
126 unique.len() > 10,
127 "expected diverse output, got {} unique chars",
128 unique.len()
129 );
130 }
131
132 #[test]
133 fn fallback_path_uses_modulo_not_division() {
134 struct AllMaxRng;
138
139 impl TryRng for AllMaxRng {
140 type Error = Infallible;
141
142 fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
143 Ok(u32::MAX)
144 }
145
146 fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
147 Ok(u64::MAX)
148 }
149
150 fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Self::Error> {
151 self.fill_bytes(dest);
152 Ok(())
153 }
154 }
155
156 impl AllMaxRng {
157 fn fill_bytes(&mut self, dest: &mut [u8]) {
158 dest.fill(255);
159 }
160 }
161
162 let mut rng = AllMaxRng;
163 let out = random_base62_with_rng(&mut rng, 4);
164 assert!(
166 out.chars().all(|c| c == 'H'),
167 "expected all 'H' from fallback, got {out}"
168 );
169 }
170}