#[cfg(test)]
mod test;
use clap::{Parser, ValueEnum};
use cli_clipboard::{ClipboardContext, ClipboardProvider};
use self_update::backends::github::Update;
use std::fs::File;
use std::io::Write;
use std::path::Path;
use std::process::Command;
use std::{env, iter};
use strum_macros::{Display, EnumProperty, EnumString};
#[derive(Parser, Debug)]
#[command(
name = "Password Generator",
version = env!("CARGO_PKG_VERSION"),
about = "Generates passwords with various complexities",
author = "ideatopia"
)]
#[command(help_template = "\
{before-help}{name} {version}
{about}
by {author} https://github.com/ideatopia
{usage-heading} {usage}
{all-args}{after-help}
")]
struct Args {
#[arg(short, long, default_value_t = 12)]
length: usize,
#[arg(short, long, default_value_t = 1)]
quantity: usize,
#[arg(short, long, default_value_t = ComplexityEnum::Secure, value_enum)]
complexity: ComplexityEnum,
#[arg(short, long)]
special: bool,
#[arg(long)]
hide: bool,
#[arg(long)]
copy: bool,
#[arg(long, default_value = "", hide_default_value = true)]
export: String,
#[arg(long)]
update: bool,
}
#[derive(Debug, EnumString, Display, Clone, EnumProperty, ValueEnum, PartialEq)]
#[strum(serialize_all = "lowercase")]
pub enum ComplexityEnum {
Simple,
Secure,
Complex,
}
fn main() {
let args = Args::parse();
if args.update {
self_update().expect("Unable to update the binary. Try from project's GitHub repository");
}
let required_length = 8;
if args.length < required_length {
panic!("Password length must be at least {}.", required_length);
}
let newline = if cfg!(target_os = "windows") {
"\r\n"
} else {
"\n"
};
let mut passwords = String::new();
let mut passwords_generated = 0;
while passwords_generated < args.quantity {
let password = generate_password(args.length, args.special, &args.complexity);
passwords.push_str(&password);
passwords.push_str(newline);
passwords_generated += 1;
}
let passwords_string: String = passwords.trim().parse().unwrap();
if !args.hide {
println!("{}", passwords_string);
}
if !args.export.is_empty() {
if Path::new(&args.export).exists() {
eprintln!("File already exists: {}", &args.export);
std::process::exit(1);
}
let mut file = File::create(&args.export).expect("Failed to create file");
match file.write_all(passwords_string.as_bytes()) {
Ok(_) => println!("Password(s) exported to {}", args.export),
Err(e) => eprintln!("Failed to export passwords: {}", e),
}
}
if args.copy {
if let Ok(mut ctx) = ClipboardContext::new() {
match ctx.set_contents(passwords_string.to_owned()) {
Ok(_) => println!("Password(s) copied to clipboard."),
Err(e) => eprintln!("Failed to copy to clipboard: {}", e),
}
} else {
eprintln!("Clipboard is not available on this system.");
}
}
}
pub fn generate_password(
length: usize,
use_special_chars: bool,
complexity: &ComplexityEnum,
) -> String {
let (lowercase, uppercase, numbers, special_chars) = (
"abcdefghijklmnopqrstuvwxyz",
"ABCDEFGHIJKLMNOPQRSTUVWXYZ",
"0123456789",
"!@#$%^&*()_+-=[]{}|;:,.<>?",
);
let charset = match complexity {
ComplexityEnum::Simple => lowercase,
ComplexityEnum::Secure => &format!("{}{}{}", lowercase, uppercase, numbers),
ComplexityEnum::Complex => {
&format!("{}{}{}{}", lowercase, uppercase, numbers, special_chars)
}
};
let charset: Vec<char> = if use_special_chars {
format!("{}{}", charset, special_chars).chars().collect()
} else {
charset.chars().collect()
};
let mut password: Vec<char> = Vec::new();
if matches!(complexity, ComplexityEnum::Secure | ComplexityEnum::Complex) {
password.push(
lowercase
.chars()
.nth(fastrand::usize(..lowercase.len()))
.unwrap(),
);
password.push(
uppercase
.chars()
.nth(fastrand::usize(..uppercase.len()))
.unwrap(),
);
password.push(
numbers
.chars()
.nth(fastrand::usize(..numbers.len()))
.unwrap(),
);
}
if use_special_chars {
password.push(
special_chars
.chars()
.nth(fastrand::usize(..special_chars.len()))
.unwrap(),
);
}
password.extend(
iter::repeat_with(|| charset[fastrand::usize(..charset.len())])
.take(length - password.len()),
);
fastrand::shuffle(&mut password);
password.iter().collect()
}
pub fn self_update() -> Result<(), Box<dyn std::error::Error>> {
let os_type = env::consts::OS;
let repo_owner = "ideatopia";
let repo_name = "password-generator";
let asset_name = match os_type {
"windows" => "pwdgen-windows.exe",
"linux" => "pwdgen-ubuntu",
"macos" => "pwdgen-macos",
_ => return Err("Unsupported operating system".into()),
};
let bin_name = match os_type {
"windows" => "pwdgen.exe",
_ => "pwdgen",
};
println!("Initiating self-update for {} system...", os_type);
let status = Update::configure()
.repo_owner(repo_owner)
.repo_name(repo_name)
.bin_name(bin_name)
.target(asset_name)
.show_download_progress(true)
.current_version(env!("CARGO_PKG_VERSION"))
.build()?
.update()?;
if status.updated() {
println!("Update successful!");
println!("New version: {}", status.version());
let args: Vec<String> = env::args().collect();
let _ = Command::new(&args[0]).arg("-h").spawn();
std::process::exit(0);
} else {
println!("No update available. Current version is up to date.");
}
Ok(())
}