use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
use super::wordlist::WORDS;
fn csprng() -> StdRng {
StdRng::from_os_rng()
}
const LOWER_LETTERS: &str = "qwertyuiopasdfghjklzxcvbnm";
const UPPER_LETTERS: &str = "QWERTYUIOPASDFGHJKLZXCVBNM";
const DIGITS: &str = "1234567890";
const SPECIAL_SYMBOLS: &str = "!@#$%^&*()_+-=;:,.?~";
#[derive(Debug, Clone)]
pub struct PasswordOptions {
pub lowercase: bool,
pub uppercase: bool,
pub digits: bool,
pub special: bool,
pub length: usize,
}
impl Default for PasswordOptions {
fn default() -> Self {
Self {
lowercase: true,
uppercase: true,
digits: true,
special: false,
length: 16,
}
}
}
pub fn generate_password(options: &PasswordOptions) -> String {
let mut rng = csprng();
let mut char_pool = String::new();
if options.lowercase {
char_pool.push_str(LOWER_LETTERS);
char_pool.push_str(LOWER_LETTERS);
char_pool.push_str(LOWER_LETTERS);
}
if options.uppercase {
char_pool.push_str(UPPER_LETTERS);
char_pool.push_str(UPPER_LETTERS);
char_pool.push_str(UPPER_LETTERS);
}
if options.digits {
char_pool.push_str(DIGITS);
char_pool.push_str(DIGITS);
}
if options.special {
char_pool.push_str(SPECIAL_SYMBOLS);
}
if char_pool.is_empty() {
char_pool.push_str(LOWER_LETTERS);
}
let chars: Vec<char> = char_pool.chars().collect();
let mut password = String::with_capacity(options.length);
for _ in 0..options.length {
let idx = rng.random_range(0..chars.len());
password.push(chars[idx]);
}
password
}
pub fn generate_clever_password(pattern: &str) -> String {
let mut rng = csprng();
let all_symbols = format!("{}{}{}{}", LOWER_LETTERS, UPPER_LETTERS, DIGITS, SPECIAL_SYMBOLS);
let all_chars: Vec<char> = all_symbols.chars().collect();
let lower_chars: Vec<char> = LOWER_LETTERS.chars().collect();
let upper_chars: Vec<char> = UPPER_LETTERS.chars().collect();
let digit_chars: Vec<char> = DIGITS.chars().collect();
let special_chars: Vec<char> = SPECIAL_SYMBOLS.chars().collect();
let mut password = String::with_capacity(pattern.len());
for ch in pattern.chars() {
let generated_char = if LOWER_LETTERS.contains(ch) {
lower_chars[rng.random_range(0..lower_chars.len())]
} else if UPPER_LETTERS.contains(ch) {
upper_chars[rng.random_range(0..upper_chars.len())]
} else if DIGITS.contains(ch) {
digit_chars[rng.random_range(0..digit_chars.len())]
} else if SPECIAL_SYMBOLS.contains(ch) {
special_chars[rng.random_range(0..special_chars.len())]
} else {
all_chars[rng.random_range(0..all_chars.len())]
};
password.push(generated_char);
}
password
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MemorableCaps {
First,
Last,
}
#[derive(Debug, Clone)]
pub struct MemorableOptions {
pub num_words: usize,
pub digits_per_word: usize,
pub separator: String,
pub prefix: String,
pub caps: MemorableCaps,
}
impl Default for MemorableOptions {
fn default() -> Self {
Self {
num_words: 4,
digits_per_word: 1,
separator: "-".to_string(),
prefix: String::new(),
caps: MemorableCaps::First,
}
}
}
pub fn generate_memorable_password(opts: &MemorableOptions) -> String {
if opts.num_words == 0 {
return String::new();
}
let mut rng = csprng();
let mut segments: Vec<String> = Vec::with_capacity(opts.num_words);
if !opts.prefix.is_empty() {
segments.push(opts.prefix.clone());
}
for _ in 0..opts.num_words {
let word = WORDS[rng.random_range(0..WORDS.len())];
let mut s = apply_caps(word, opts.caps);
for _ in 0..opts.digits_per_word {
let digit = rng.random_range(0..10u32);
s.push(char::from_digit(digit, 10).unwrap());
}
segments.push(s);
}
segments.join(&opts.separator)
}
fn apply_caps(word: &str, caps: MemorableCaps) -> String {
let chars: Vec<char> = word.chars().collect();
if chars.is_empty() {
return String::new();
}
match caps {
MemorableCaps::First => {
let mut out = String::with_capacity(word.len());
out.push(chars[0].to_ascii_uppercase());
for &c in &chars[1..] {
out.push(c);
}
out
}
MemorableCaps::Last => {
let mut out = String::with_capacity(word.len());
for &c in &chars[..chars.len() - 1] {
out.push(c);
}
out.push(chars[chars.len() - 1].to_ascii_uppercase());
out
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
#[test]
fn test_generate_password_default() {
let options = PasswordOptions::default();
let password = generate_password(&options);
assert_eq!(password.len(), 16);
}
#[test]
fn test_generate_password_length() {
let options = PasswordOptions {
length: 32,
..Default::default()
};
let password = generate_password(&options);
assert_eq!(password.len(), 32);
}
#[test]
fn test_generate_password_lowercase_only() {
let options = PasswordOptions {
lowercase: true,
uppercase: false,
digits: false,
special: false,
length: 20,
};
let password = generate_password(&options);
assert_eq!(password.len(), 20);
assert!(password.chars().all(|c| c.is_ascii_lowercase()));
}
#[test]
fn test_generate_password_uppercase_only() {
let options = PasswordOptions {
lowercase: false,
uppercase: true,
digits: false,
special: false,
length: 20,
};
let password = generate_password(&options);
assert_eq!(password.len(), 20);
assert!(password.chars().all(|c| c.is_ascii_uppercase()));
}
#[test]
fn test_generate_password_digits_only() {
let options = PasswordOptions {
lowercase: false,
uppercase: false,
digits: true,
special: false,
length: 20,
};
let password = generate_password(&options);
assert_eq!(password.len(), 20);
assert!(password.chars().all(|c| c.is_ascii_digit()));
}
#[test]
fn test_generate_password_special_only() {
let options = PasswordOptions {
lowercase: false,
uppercase: false,
digits: false,
special: true,
length: 20,
};
let password = generate_password(&options);
assert_eq!(password.len(), 20);
assert!(password.chars().all(|c| SPECIAL_SYMBOLS.contains(c)));
}
#[test]
fn test_generate_password_all_types() {
let options = PasswordOptions {
lowercase: true,
uppercase: true,
digits: true,
special: true,
length: 100,
};
let password = generate_password(&options);
assert_eq!(password.len(), 100);
}
#[test]
fn test_generate_password_empty_options_fallback() {
let options = PasswordOptions {
lowercase: false,
uppercase: false,
digits: false,
special: false,
length: 10,
};
let password = generate_password(&options);
assert_eq!(password.len(), 10);
assert!(password.chars().all(|c| c.is_ascii_lowercase()));
}
#[test]
fn test_generate_password_uniqueness() {
let options = PasswordOptions::default();
let p1 = generate_password(&options);
let p2 = generate_password(&options);
assert_ne!(p1, p2);
}
#[test]
fn test_generate_clever_password_length() {
let password = generate_clever_password("Aaaa0000");
assert_eq!(password.len(), 8);
}
#[test]
fn test_generate_clever_password_pattern() {
let password = generate_clever_password("aaaa");
assert!(password.chars().all(|c| c.is_ascii_lowercase()));
let password = generate_clever_password("AAAA");
assert!(password.chars().all(|c| c.is_ascii_uppercase()));
let password = generate_clever_password("0000");
assert!(password.chars().all(|c| c.is_ascii_digit()));
}
#[test]
fn test_generate_clever_password_mixed() {
let password = generate_clever_password("Aa00");
assert_eq!(password.len(), 4);
let chars: Vec<char> = password.chars().collect();
assert!(chars[0].is_ascii_uppercase());
assert!(chars[1].is_ascii_lowercase());
assert!(chars[2].is_ascii_digit());
assert!(chars[3].is_ascii_digit());
}
#[test]
fn test_generate_clever_password_special() {
let password = generate_clever_password("!@#$");
assert_eq!(password.len(), 4);
assert!(password.chars().all(|c| SPECIAL_SYMBOLS.contains(c)));
}
#[test]
fn test_generate_clever_password_unknown_chars() {
let password = generate_clever_password(" "); assert_eq!(password.len(), 4);
}
#[test]
fn test_generate_clever_password_empty() {
let password = generate_clever_password("");
assert!(password.is_empty());
}
#[test]
fn test_generate_clever_password_uniqueness() {
let p1 = generate_clever_password("Aaaa0000@@");
let p2 = generate_clever_password("Aaaa0000@@");
assert_ne!(p1, p2);
}
fn opts(num_words: usize) -> MemorableOptions {
MemorableOptions {
num_words,
..Default::default()
}
}
fn wordlist_set() -> HashSet<&'static str> {
WORDS.iter().copied().collect()
}
fn strip_digits_and_lower(seg: &str) -> String {
seg.chars()
.filter(|c| !c.is_ascii_digit())
.collect::<String>()
.to_ascii_lowercase()
}
#[test]
fn memorable_zero_words_returns_empty() {
let p = generate_memorable_password(&opts(0));
assert_eq!(p, "");
}
#[test]
fn memorable_one_word_no_separator() {
let p = generate_memorable_password(&opts(1));
assert!(!p.contains('-'), "single-word output should have no separator: {p}");
assert!(p.chars().last().unwrap().is_ascii_digit());
assert!(p.chars().next().unwrap().is_ascii_uppercase());
}
#[test]
fn memorable_default_four_words_three_dashes() {
let p = generate_memorable_password(&opts(4));
let dashes = p.chars().filter(|&c| c == '-').count();
assert_eq!(dashes, 3, "4 words → 3 dashes; got: {p}");
for seg in p.split('-') {
assert!(seg.chars().next().unwrap().is_ascii_uppercase());
assert!(seg.chars().last().unwrap().is_ascii_digit());
}
}
#[test]
fn memorable_custom_separator() {
let mut o = opts(3);
o.separator = "_".to_string();
let p = generate_memorable_password(&o);
assert_eq!(p.matches('_').count(), 2, "3 words → 2 underscores: {p}");
assert!(!p.contains('-'));
}
#[test]
fn memorable_multi_char_separator() {
let mut o = opts(3);
o.separator = "--".to_string();
let p = generate_memorable_password(&o);
assert_eq!(p.matches("--").count(), 2, "3 words joined by '--': {p}");
}
#[test]
fn memorable_zero_digits_per_word() {
let mut o = opts(3);
o.digits_per_word = 0;
let p = generate_memorable_password(&o);
for seg in p.split('-') {
assert!(
!seg.chars().any(|c| c.is_ascii_digit()),
"segment {seg:?} should have no digits"
);
}
}
#[test]
fn memorable_three_digits_per_word() {
let mut o = opts(3);
o.digits_per_word = 3;
let p = generate_memorable_password(&o);
for seg in p.split('-') {
let trailing_digits =
seg.chars().rev().take_while(|c| c.is_ascii_digit()).count();
assert_eq!(trailing_digits, 3, "segment {seg:?} should end with 3 digits");
}
}
#[test]
fn memorable_caps_last() {
let mut o = opts(3);
o.caps = MemorableCaps::Last;
o.digits_per_word = 1;
let p = generate_memorable_password(&o);
for seg in p.split('-') {
let chars: Vec<char> = seg.chars().collect();
assert!(chars[0].is_ascii_lowercase(), "first char should be lowercase: {seg:?}");
assert!(chars.last().unwrap().is_ascii_digit());
let upper = chars[chars.len() - 2];
assert!(upper.is_ascii_uppercase(), "letter before digit should be uppercase: {seg:?}");
}
}
#[test]
fn memorable_with_prefix_uses_separator() {
let mut o = opts(3);
o.prefix = "@home".to_string();
let p = generate_memorable_password(&o);
assert!(p.starts_with("@home-"), "prefix must be joined by separator: {p}");
assert_eq!(p.matches('-').count(), 3, "{p}");
}
#[test]
fn memorable_words_come_from_wordlist() {
let words = wordlist_set();
let p = generate_memorable_password(&opts(4));
for seg in p.split('-') {
let bare = strip_digits_and_lower(seg);
assert!(
words.contains(bare.as_str()),
"segment {seg:?} stripped to {bare:?} not in wordlist"
);
}
}
#[test]
fn memorable_two_calls_produce_different_outputs() {
let p1 = generate_memorable_password(&opts(4));
let p2 = generate_memorable_password(&opts(4));
assert_ne!(p1, p2, "two consecutive calls must differ");
}
#[test]
fn memorable_wordlist_length_is_1024() {
assert_eq!(WORDS.len(), 1024);
}
}