use std::fs;
use std::io::Write;
use std::path::Path;
use std::result::Result as StdResult;
use minijinja::{Error as MinijinjaError, ErrorKind as MinijinjaErrorKind, Value, value::Kwargs};
use rand::SeedableRng;
use rand::prelude::*;
use rand::rngs::StdRng;
const DEFAULT_LENGTH: usize = 20;
const ASCII_LOWERCASE: &str = "abcdefghijklmnopqrstuvwxyz";
const ASCII_UPPERCASE: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const DIGITS: &str = "0123456789";
const PUNCTUATION: &str = ".,:-_";
pub fn function(path: String, options: Kwargs) -> StdResult<Value, MinijinjaError> {
if path != "/dev/null" && Path::new(&path).exists() {
match fs::read_to_string(&path) {
Ok(content) => return Ok(Value::from(content.trim_end())),
Err(e) => {
return Err(MinijinjaError::new(
MinijinjaErrorKind::InvalidOperation,
format!("Failed to read password file '{path}': {e}"),
));
}
}
}
let length: usize = options
.get::<Option<usize>>("length")?
.unwrap_or(DEFAULT_LENGTH);
let chars: Option<Vec<String>> = options.get("chars")?;
let seed: Option<String> = options.get("seed")?;
let charset = build_charset(chars.as_ref())?;
let password = if let Some(seed_value) = seed {
generate_seeded_password(&charset, length, &seed_value)?
} else {
generate_random_password(&charset, length)?
};
if path != "/dev/null" {
if let Some(parent) = Path::new(&path).parent()
&& !parent.exists()
{
fs::create_dir_all(parent).map_err(|e| {
MinijinjaError::new(
MinijinjaErrorKind::InvalidOperation,
format!("Failed to create directory for password file: {e}"),
)
})?;
}
let mut file = fs::File::create(&path).map_err(|e| {
MinijinjaError::new(
MinijinjaErrorKind::InvalidOperation,
format!("Failed to create password file '{path}': {e}"),
)
})?;
file.write_all(password.as_bytes()).map_err(|e| {
MinijinjaError::new(
MinijinjaErrorKind::InvalidOperation,
format!("Failed to write password to file '{path}': {e}"),
)
})?;
}
options.assert_all_used()?;
Ok(Value::from(password))
}
fn build_charset(chars: Option<&Vec<String>>) -> StdResult<String, MinijinjaError> {
let default_chars = vec![
"ascii_letters".to_string(),
"digits".to_string(),
"punctuation".to_string(),
];
let char_sets = chars.unwrap_or(&default_chars);
let mut charset = String::new();
for char_set in char_sets {
match char_set.as_str() {
"ascii_lowercase" => charset.push_str(ASCII_LOWERCASE),
"ascii_uppercase" => charset.push_str(ASCII_UPPERCASE),
"ascii_letters" => {
charset.push_str(ASCII_LOWERCASE);
charset.push_str(ASCII_UPPERCASE);
}
"digits" => charset.push_str(DIGITS),
"punctuation" => charset.push_str(PUNCTUATION),
other => {
charset.push_str(other);
}
}
}
if charset.is_empty() {
return Err(MinijinjaError::new(
MinijinjaErrorKind::InvalidOperation,
"No valid characters specified for password generation",
));
}
Ok(charset)
}
fn generate_random_password(charset: &str, length: usize) -> StdResult<String, MinijinjaError> {
let mut rng = rand::rng();
let chars: Vec<char> = charset.chars().collect();
let password: String = (0..length)
.map(|_| *chars.choose(&mut rng).unwrap())
.collect();
Ok(password)
}
fn generate_seeded_password(
charset: &str,
length: usize,
seed: &str,
) -> StdResult<String, MinijinjaError> {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
std::hash::Hash::hash(seed, &mut hasher);
let seed_value = std::hash::Hasher::finish(&hasher);
let mut rng = StdRng::seed_from_u64(seed_value);
let chars: Vec<char> = charset.chars().collect();
let password: String = (0..length)
.map(|_| *chars.choose(&mut rng).unwrap())
.collect();
Ok(password)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_charset_default() {
let charset = build_charset(None).unwrap();
assert!(charset.contains("a")); assert!(charset.contains("A")); assert!(charset.contains("0")); assert!(charset.contains(".")); }
#[test]
fn test_build_charset_digits_only() {
let chars = vec!["digits".to_string()];
let charset = build_charset(Some(&chars)).unwrap();
assert_eq!(charset, DIGITS);
}
#[test]
fn test_build_charset_ascii_letters() {
let chars = vec!["ascii_letters".to_string()];
let charset = build_charset(Some(&chars)).unwrap();
assert_eq!(charset, format!("{ASCII_LOWERCASE}{ASCII_UPPERCASE}"));
}
#[test]
fn test_build_charset_custom() {
let chars = vec!["xyz123".to_string()];
let charset = build_charset(Some(&chars)).unwrap();
assert_eq!(charset, "xyz123");
}
#[test]
fn test_build_charset_empty() {
let chars = vec!["".to_string()];
let result = build_charset(Some(&chars));
assert!(result.is_err());
}
#[test]
fn test_generate_seeded_password_consistent() {
let charset = "abc123";
let password1 = generate_seeded_password(charset, 10, "test-seed").unwrap();
let password2 = generate_seeded_password(charset, 10, "test-seed").unwrap();
assert_eq!(password1, password2);
assert_eq!(password1.len(), 10);
}
#[test]
fn test_generate_seeded_password_different_seeds() {
let charset = "abc123";
let password1 = generate_seeded_password(charset, 10, "seed1").unwrap();
let password2 = generate_seeded_password(charset, 10, "seed2").unwrap();
assert_ne!(password1, password2);
}
#[test]
fn test_generate_random_password_length() {
let charset = "abcdefghijklmnopqrstuvwxyz";
let password = generate_random_password(charset, 15).unwrap();
assert_eq!(password.len(), 15);
assert!(password.chars().all(|c| charset.contains(c)));
}
#[test]
fn test_generate_random_password_charset() {
let charset = "ABC";
let password = generate_random_password(charset, 100).unwrap();
assert!(password.chars().all(|c| "ABC".contains(c)));
}
}