use clap::{value_parser, Arg, ArgAction, Command};
use rand::{seq::SliceRandom, Rng};
use std::env;
use std::fs::File;
use std::io::prelude::*;
use std::io::BufReader;
use std::path::Path;
use std::process;
use terminal_size::{terminal_size, Width};
const MAX: u8 = 255;
const RANGE_MAX: u8 = 42;
const RANGE_MIN: u8 = 8;
const PASS_WORDS: u8 = 8;
const LOWER: &str = "abcdefghijklmnopqrstuvwxyz";
const UPPER: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const DIGIT: &str = "0123456789";
const OTHER: &str = "!$%&#/()=?+-_,.;:<>[]{}|@*";
const EXTRA: &str = "-_";
const DEF_LENGTH: u8 = 0;
const DEF_NUMBER: u8 = 10;
const DEF_WORDLIST: &str = "/usr/share/dict/words";
struct CommonPrintData<'a> {
length: u8,
alpha: &'a Vec<char>,
alnum: &'a Vec<char>,
printlen: u8,
col_width: u8,
col_num: u16,
}
fn main() {
let lower = LOWER.chars().collect::<Vec<_>>();
let upper = UPPER.chars().collect::<Vec<_>>();
let digit = DIGIT.chars().collect::<Vec<_>>();
let other = OTHER.chars().collect::<Vec<_>>();
let extra = EXTRA.chars().collect::<Vec<_>>();
let alpha = [&lower[..], &upper[..]].concat();
let alnum = [&alpha[..], &digit[..]].concat();
let every = [&alnum[..], &other[..]].concat();
let mut normal = [&alnum[..], &extra[..]].concat();
let mut special = every;
let mut length = DEF_LENGTH;
let mut number = DEF_NUMBER;
let mut wordlist = DEF_WORDLIST.to_string();
//
// env
// Deal with env before cli-args, because cli-args take precedence.
//
let env_length = env::var("MAKEPASS_LENGTH").unwrap_or_default();
let env_number = env::var("MAKEPASS_NUMBER").unwrap_or_default();
let env_wordlist = env::var("MAKEPASS_WORDLIST").unwrap_or_default();
let env_normal = env::var("MAKEPASS_NORMAL").unwrap_or_default();
let env_special = env::var("MAKEPASS_SPECIAL").unwrap_or_default();
if !env_length.is_empty() {
length = env_length.parse().unwrap_or_else(|_| {
eprintln!(
"Error: MAKEPASS_LENGTH is not a valid number. Valid numbers are between 0 and 255"
);
process::exit(1);
});
}
if !env_number.is_empty() {
match env_number.parse::<u8>() {
Ok(n) if n >= 1 => number = n,
_ => {
eprintln!("Error: MAKEPASS_NUMBER is not a valid number. Valid numbers are between 1 and 255");
process::exit(1);
}
}
}
if !env_wordlist.is_empty() {
wordlist = env_wordlist;
}
if !env_normal.is_empty() {
normal = env_number.chars().collect::<Vec<_>>();
}
if !env_special.is_empty() {
special = env_special.chars().collect::<Vec<_>>();
}
//
// Args
//
let opts = Command::new("makepass")
.arg(
Arg::new("length_flag")
.short('l')
.value_name("length")
.help(format!("Length of passwords (0..={MAX}). 0 = random."))
.value_parser(value_parser!(u8).range(0..)),
)
.arg(
Arg::new("number")
.short('n')
.help(format!("Number of passwords (1..={MAX}). [default: 10]"))
.value_parser(value_parser!(u8).range(1..)),
)
.arg(
Arg::new("printlen")
.short('p')
.help("Print length")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("length")
.help(format!(
"Length of passwords (0..={MAX}). 0 = random length."
))
.value_parser(value_parser!(u8).range(0..)),
)
.get_matches();
match opts.get_one::<u8>("length_flag") {
Some(n) => length = *n,
_ => (),
}
match opts.get_one::<u8>("number") {
Some(n) => number = *n,
_ => (),
}
match opts.get_one::<u8>("length") {
Some(n) => length = *n,
_ => (),
}
let printbool = if opts.get_flag("printlen") {
true
} else {
false
};
//
// Other logic
//
let printlen: u8 = if printbool && length < 100 {
3
} else if printbool {
4
} else {
0
};
let size = terminal_size();
let term_width: u16 = match size {
Some((Width(w), _)) => w,
None => 1,
};
let col_width: u8 = match length {
0 => RANGE_MAX + 2,
_ => length + 2,
};
let col_num: u16 = match term_width / (col_width + printlen) as u16 {
0 => 1,
n => n,
};
let data = CommonPrintData {
length: length,
alpha: &alpha,
alnum: &alnum,
printlen: printlen,
col_width: col_width,
col_num: col_num,
};
//
// Do the dirty!
//
print_columns("Normal passwords", number, &normal, &data);
println!("");
print_columns(
"Passwords with special characters",
number / 3 * 2 + 1,
&special,
&data,
);
if Path::new(&wordlist).exists() {
println!("");
println!("Passphrases:");
let words = read_file(&wordlist);
for _ in 0..number / 2 {
println!("{}", passphrase(&words));
}
}
}
fn print_columns(title: &str, num: u8, chars: &Vec<char>, data: &CommonPrintData) {
let mut strings: Vec<String> = Vec::new();
for _ in 0..num {
strings.push(randstring(data.length, chars, data.alpha, data.alnum));
}
println!("{}:", title);
let mut i: u16 = 0;
for s in &strings {
i = i + 1;
if data.printlen > 0 {
print!("{1:00$} ", (data.printlen - 1) as usize, s.len());
}
print!("{1:<0$}", data.col_width as usize, s);
if (i % data.col_num == 0) || (i == num as u16 && i % data.col_num > 0) {
println!("");
}
}
}
fn randstring(len: u8, chars: &Vec<char>, alpha: &Vec<char>, alnum: &Vec<char>) -> String {
let mut rng = rand::thread_rng();
let length = if len == 0 {
rng.gen_range(RANGE_MIN..RANGE_MAX)
} else {
len
};
let mut s = String::with_capacity(length as usize);
for i in 0..length {
if i == 0 {
s.push(*alpha.choose(&mut rng).unwrap());
} else if i == length - 1 {
s.push(*alnum.choose(&mut rng).unwrap());
} else {
s.push(*chars.choose(&mut rng).unwrap());
}
}
s
}
fn read_file(filename: &str) -> Vec<String> {
let file = File::open(filename).unwrap();
let reader = BufReader::new(file);
reader.lines().map(|l| l.unwrap()).collect()
}
fn passphrase(wordlist: &Vec<String>) -> String {
let mut rng = rand::thread_rng();
(0..PASS_WORDS)
.map(|_| wordlist.choose(&mut rng).unwrap())
.cloned()
.collect::<Vec<_>>()
.join("-")
}