use anyhow::{Result, anyhow};
use crossterm::style::{Color, Stylize, style};
use rand::seq::{IndexedRandom, SliceRandom};
use zeroize::Zeroize;
use super::cli::GenKind;
const LOWERCASE: &str = "abcdefghijklmnopqrstuvwxyz";
const UPPERCASE: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const DIGITS: &str = "0123456789";
const SYMBOLS: &str = "!@#$%^&*()-_=+[]{};:,.<>?";
const WORDLIST: &str = include_str!("eff_large_wordlist.txt");
pub(crate) fn generate(
length: u32,
caps: bool,
numbers: bool,
special: bool,
passphrase: Option<u32>,
kind: GenKind,
) -> Result<String> {
match passphrase {
Some(words) => gen_passphrase(words, kind),
None => gen_password(length, caps, numbers, special),
}
}
fn gen_password(length: u32, caps: bool, numbers: bool, special: bool) -> Result<String> {
let mut rng = rand::rng();
let mut classes: Vec<Vec<char>> = vec![LOWERCASE.chars().collect()];
if caps {
classes.push(UPPERCASE.chars().collect());
}
if numbers {
classes.push(DIGITS.chars().collect());
}
if special {
classes.push(SYMBOLS.chars().collect());
}
let pool: Vec<char> = classes.iter().flatten().copied().collect();
let total = usize::try_from(length).unwrap_or(usize::MAX);
let mut chars: Vec<char> = Vec::with_capacity(total);
for class in &classes {
let chosen = class
.choose(&mut rng)
.copied()
.ok_or_else(|| anyhow!("a character class was unexpectedly empty"))?;
chars.push(chosen);
}
let remaining = total.saturating_sub(chars.len());
for _ in 0..remaining {
let chosen = pool
.choose(&mut rng)
.copied()
.ok_or_else(|| anyhow!("the character pool was unexpectedly empty"))?;
chars.push(chosen);
}
chars.truncate(total);
chars.shuffle(&mut rng);
let password = chars.iter().collect::<String>();
chars.zeroize();
Ok(password)
}
fn gen_passphrase(words: u32, kind: GenKind) -> Result<String> {
let list: Vec<&str> = WORDLIST.lines().filter(|line| !line.is_empty()).collect();
let mut rng = rand::rng();
let count = usize::try_from(words).unwrap_or(usize::MAX);
let mut chosen: Vec<&str> = Vec::with_capacity(count);
for _ in 0..count {
let word = list
.choose(&mut rng)
.copied()
.ok_or_else(|| anyhow!("the passphrase word list was unexpectedly empty"))?;
chosen.push(word);
}
let separator = match kind {
GenKind::Space => " ",
GenKind::Hyphen => "-",
GenKind::Dot => ".",
GenKind::Camel => "",
};
let formatted: Vec<String> = chosen
.iter()
.map(|word| match kind {
GenKind::Camel => capitalize(word),
GenKind::Space | GenKind::Hyphen | GenKind::Dot => (*word).to_string(),
})
.collect();
Ok(formatted.join(separator))
}
fn capitalize(word: &str) -> String {
let mut chars = word.chars();
match chars.next() {
Some(first) => first.to_uppercase().chain(chars).collect(),
None => String::new(),
}
}
pub(crate) fn print_secret(secret: &str) {
println!("{}", "Generated:".green());
println!("{}", style(secret).with(Color::Green).bold());
}
#[cfg(test)]
mod test {
use anyhow::Result;
use super::{DIGITS, SYMBOLS, capitalize, gen_passphrase, gen_password};
use crate::runtime::cli::GenKind;
#[test]
fn password_has_requested_length() -> Result<()> {
let pw = gen_password(30, true, true, true)?;
assert_eq!(pw.chars().count(), 30);
Ok(())
}
#[test]
fn password_honors_short_lengths() -> Result<()> {
let pw = gen_password(8, true, true, true)?;
assert_eq!(pw.chars().count(), 8);
Ok(())
}
#[test]
fn password_lowercase_only_when_classes_disabled() -> Result<()> {
let pw = gen_password(64, false, false, false)?;
assert!(
pw.chars().all(|c| c.is_ascii_lowercase()),
"expected only lowercase letters, got {pw}"
);
Ok(())
}
#[test]
fn password_includes_each_enabled_class() -> Result<()> {
let pw = gen_password(30, true, true, true)?;
assert!(
pw.chars().any(|c| c.is_ascii_lowercase()),
"missing lowercase"
);
assert!(
pw.chars().any(|c| c.is_ascii_uppercase()),
"missing uppercase"
);
assert!(pw.chars().any(|c| DIGITS.contains(c)), "missing digit");
assert!(pw.chars().any(|c| SYMBOLS.contains(c)), "missing symbol");
Ok(())
}
#[test]
fn passphrase_space_has_expected_word_count() -> Result<()> {
let phrase = gen_passphrase(5, GenKind::Space)?;
assert_eq!(phrase.split(' ').count(), 5);
Ok(())
}
#[test]
fn passphrase_dot_has_expected_word_count() -> Result<()> {
let phrase = gen_passphrase(3, GenKind::Dot)?;
assert_eq!(phrase.split('.').count(), 3);
Ok(())
}
#[test]
fn passphrase_camel_capitalizes_and_omits_separators() -> Result<()> {
let phrase = gen_passphrase(4, GenKind::Camel)?;
assert!(!phrase.contains(' '), "camel phrase should have no spaces");
assert!(
phrase.chars().next().is_some_and(char::is_uppercase),
"camel phrase should start uppercase, got {phrase}"
);
Ok(())
}
#[test]
fn capitalize_handles_words_and_empty() {
assert_eq!(capitalize("horse"), "Horse");
assert_eq!(capitalize(""), "");
}
}