use rand::{Rng, RngCore};
pub const ALPHANUM_CHARS: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
#[must_use]
pub fn get_random_string(length: usize, allowed_chars: &str) -> String {
assert!(
!allowed_chars.is_empty(),
"get_random_string requires a non-empty `allowed_chars` alphabet"
);
let chars: Vec<char> = allowed_chars.chars().collect();
let mut rng = rand::thread_rng();
(0..length)
.map(|_| chars[rng.gen_range(0..chars.len())])
.collect()
}
#[must_use]
pub fn get_random_string_default(length: usize) -> String {
get_random_string(length, ALPHANUM_CHARS)
}
pub const URL_SAFE_CHARS: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
pub const DIGITS_CHARS: &str = "0123456789";
pub const HEX_CHARS: &str = "0123456789abcdef";
pub const UPPERCASE_HEX_CHARS: &str = "0123456789ABCDEF";
pub const LOWERCASE_CHARS: &str = "abcdefghijklmnopqrstuvwxyz";
pub const UPPERCASE_CHARS: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
pub const LETTERS_CHARS: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
#[must_use]
pub fn get_random_token_urlsafe(length: usize) -> String {
let mut buf = vec![0u8; length];
rand::rngs::OsRng.fill_bytes(&mut buf);
let alphabet: Vec<char> = URL_SAFE_CHARS.chars().collect();
buf.into_iter()
.map(|b| alphabet[(b & 0b0011_1111) as usize])
.collect()
}
#[must_use]
pub fn random_hex(length: usize) -> String {
get_random_string(length, HEX_CHARS)
}
#[must_use]
pub fn random_alphanum(length: usize) -> String {
get_random_string(length, ALPHANUM_CHARS)
}
#[must_use]
pub fn random_letters(length: usize) -> String {
get_random_string(length, LETTERS_CHARS)
}
#[must_use]
pub fn random_lowercase(length: usize) -> String {
get_random_string(length, LOWERCASE_CHARS)
}
#[must_use]
pub fn random_uppercase(length: usize) -> String {
get_random_string(length, UPPERCASE_CHARS)
}
#[must_use]
pub fn random_digits(length: usize) -> String {
get_random_string(length, DIGITS_CHARS)
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
#[test]
fn get_random_string_returns_requested_length() {
for n in [0, 1, 8, 32, 256] {
let s = get_random_string(n, ALPHANUM_CHARS);
assert_eq!(s.chars().count(), n);
}
}
#[test]
fn get_random_string_default_uses_alphanum_alphabet() {
let s = get_random_string_default(128);
assert_eq!(s.chars().count(), 128);
assert!(s.chars().all(|c| ALPHANUM_CHARS.contains(c)));
}
#[test]
fn get_random_string_respects_custom_alphabet() {
let pin = get_random_string(32, "0123456789");
assert!(pin.chars().all(|c| c.is_ascii_digit()));
}
#[test]
fn get_random_string_each_call_is_different() {
let a = get_random_string(32, ALPHANUM_CHARS);
let b = get_random_string(32, ALPHANUM_CHARS);
assert_ne!(a, b, "two consecutive calls returned identical strings");
}
#[test]
fn get_random_string_distributes_across_alphabet() {
let s = get_random_string(1024, "abcd");
let unique: HashSet<char> = s.chars().collect();
assert_eq!(unique.len(), 4);
}
#[test]
#[should_panic(expected = "non-empty")]
fn get_random_string_empty_alphabet_panics() {
let _ = get_random_string(10, "");
}
#[test]
fn get_random_string_handles_unicode_alphabet() {
let s = get_random_string(64, "αβγδ");
assert_eq!(s.chars().count(), 64);
assert!(s.chars().all(|c| "αβγδ".contains(c)));
}
#[test]
fn token_urlsafe_returns_requested_length() {
for n in [0, 1, 22, 32, 64] {
let t = get_random_token_urlsafe(n);
assert_eq!(t.chars().count(), n);
}
}
#[test]
fn token_urlsafe_uses_url_safe_alphabet() {
let t = get_random_token_urlsafe(256);
assert!(t.chars().all(|c| URL_SAFE_CHARS.contains(c)));
}
#[test]
fn token_urlsafe_distinct_calls_are_distinct() {
let a = get_random_token_urlsafe(32);
let b = get_random_token_urlsafe(32);
assert_ne!(a, b);
}
#[test]
fn url_safe_alphabet_has_64_chars() {
assert_eq!(URL_SAFE_CHARS.chars().count(), 64);
}
#[test]
fn random_hex_returns_lowercase_hex_string() {
for n in [1, 8, 32, 64] {
let s = random_hex(n);
assert_eq!(s.chars().count(), n);
assert!(
s.chars()
.all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase()),
"got: {s}"
);
}
}
#[test]
fn random_hex_distinct_calls_are_distinct() {
let a = random_hex(32);
let b = random_hex(32);
assert_ne!(a, b);
}
#[test]
fn random_alphanum_returns_alphanumeric_only() {
let s = random_alphanum(22);
assert_eq!(s.chars().count(), 22);
assert!(s.chars().all(|c| c.is_ascii_alphanumeric()));
}
#[test]
fn random_digits_returns_digits_only() {
let s = random_digits(6);
assert_eq!(s.chars().count(), 6);
assert!(s.chars().all(|c| c.is_ascii_digit()));
}
#[test]
fn random_helpers_zero_length_returns_empty() {
assert!(random_hex(0).is_empty());
assert!(random_alphanum(0).is_empty());
assert!(random_digits(0).is_empty());
assert!(random_letters(0).is_empty());
assert!(random_lowercase(0).is_empty());
assert!(random_uppercase(0).is_empty());
}
#[test]
fn random_letters_mixed_case_only() {
let s = random_letters(40);
assert_eq!(s.chars().count(), 40);
assert!(s.chars().all(|c| c.is_ascii_alphabetic()));
}
#[test]
fn random_lowercase_only() {
let s = random_lowercase(40);
assert_eq!(s.chars().count(), 40);
assert!(s.chars().all(|c| c.is_ascii_lowercase()));
}
#[test]
fn random_uppercase_only() {
let s = random_uppercase(40);
assert_eq!(s.chars().count(), 40);
assert!(s.chars().all(|c| c.is_ascii_uppercase()));
}
#[test]
fn letter_alphabet_counts() {
assert_eq!(LOWERCASE_CHARS.chars().count(), 26);
assert_eq!(UPPERCASE_CHARS.chars().count(), 26);
assert_eq!(LETTERS_CHARS.chars().count(), 52);
}
}