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