use crate::rng::RngSource;
pub fn generate<R: RngSource + ?Sized>(rng: &mut R, charset: &[u8], length: usize) -> String {
assert!(
!charset.is_empty(),
"secure::generate requires non-empty charset"
);
let mut out = Vec::with_capacity(length);
for _ in 0..length {
let idx = rng.gen_range(charset.len() as u32) as usize;
out.push(charset[idx]);
}
String::from_utf8(out).expect("charset is ASCII-only per FR-010")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rng::OsRngSource;
#[test]
fn length_matches_request() {
let mut rng = OsRngSource::new();
let charset = b"abcdefghij";
for n in 1..50 {
let s = generate(&mut rng, charset, n);
assert_eq!(s.len(), n);
}
}
#[test]
fn output_drawn_only_from_charset() {
let mut rng = OsRngSource::new();
let charset = b"ABC";
for _ in 0..100 {
let s = generate(&mut rng, charset, 16);
assert!(s.bytes().all(|b| charset.contains(&b)));
}
}
#[test]
fn ambiguous_filter_no_lone_zero_or_one() {
let mut rng = OsRngSource::new();
let charset = crate::charset::build(
crate::charset::CharSetFlags {
ambiguous_filter: true,
..Default::default()
},
&[],
);
for _ in 0..100 {
let s = generate(&mut rng, &charset, 16);
for byte in s.bytes() {
assert!(!b"l1O0I".contains(&byte), "ambiguous char in output: {s:?}");
}
}
}
#[test]
fn no_vowels_no_vowels() {
let mut rng = OsRngSource::new();
let charset = crate::charset::build(
crate::charset::CharSetFlags {
no_vowels: true,
..Default::default()
},
&[],
);
for _ in 0..100 {
let s = generate(&mut rng, &charset, 16);
for byte in s.bytes() {
assert!(!b"aeiouAEIOU".contains(&byte), "vowel in output: {s:?}");
}
}
}
#[test]
fn deterministic_with_seeded_rng() {
use crate::rng::SeededSource;
let seed = [7u8; 32];
let mut a = SeededSource::from_seed(seed);
let mut b = SeededSource::from_seed(seed);
let cs = b"abcdef";
for _ in 0..10 {
assert_eq!(generate(&mut a, cs, 16), generate(&mut b, cs, 16));
}
}
}