use crate::{Pwgen, PwgenBuilder};
use std::ffi::OsString;
use std::io::Write;
use std::process::ExitCode;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UnknownFlag {
Short(char),
Long(String),
}
pub fn format_unknown_flag(flag: &UnknownFlag) -> String {
match flag {
UnknownFlag::Short(c) => format!("rusty-pwgen: invalid option -- '{c}'"),
UnknownFlag::Long(name) => format!("rusty-pwgen: unknown option -- '{name}'"),
}
}
pub fn pre_scan_strict_flag(argv: &[OsString]) -> Option<bool> {
let mut chosen: Option<bool> = None;
for arg in argv.iter().skip(1) {
let s = arg.to_string_lossy();
if s == "--strict" {
chosen = Some(true);
} else if s == "--no-strict" {
chosen = Some(false);
} else if s == "--" {
break;
}
}
chosen
}
#[derive(Debug, Default)]
struct StrictArgs {
length: Option<usize>,
count: Option<usize>,
secure: bool,
capitalize: Option<bool>,
numerals: Option<bool>,
symbols: bool,
ambiguous_filter: bool,
no_vowels: bool,
remove_chars: Option<String>,
sha1: Option<String>,
columnar: bool,
one_column: bool,
}
fn parse(argv: &[OsString]) -> Result<StrictArgs, UnknownFlag> {
let mut out = StrictArgs::default();
let mut iter = argv.iter().skip(1).peekable();
while let Some(arg) = iter.next() {
let s = arg.to_string_lossy();
if s == "--strict" || s == "--no-strict" {
continue;
}
if s == "--" {
break;
}
if let Some(rest) = s.strip_prefix("--") {
let name = rest.split('=').next().unwrap_or(rest).to_string();
return Err(UnknownFlag::Long(name));
}
if let Some(rest) = s.strip_prefix('-') {
if rest.is_empty() {
return Err(UnknownFlag::Short('-'));
}
let mut chars = rest.chars().peekable();
while let Some(c) = chars.next() {
match c {
'c' => out.capitalize = Some(true),
'A' => out.capitalize = Some(false),
'n' => out.numerals = Some(true),
'0' => out.numerals = Some(false),
'y' => out.symbols = true,
's' => out.secure = true,
'B' => out.ambiguous_filter = true,
'v' => out.no_vowels = true,
'1' => out.one_column = true,
'C' => out.columnar = true,
'N' => {
let value = if let Some(rest_chars) =
chars.clone().collect::<String>().chars().next()
{
let inline: String = chars.collect();
if inline.is_empty() {
iter.next()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default()
} else {
let _ = rest_chars;
inline
}
} else {
iter.next()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default()
};
out.count = value.parse().ok();
break;
}
'r' => {
let value = iter
.next()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default();
out.remove_chars = Some(value);
break;
}
'H' => {
let value = iter
.next()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default();
out.sha1 = Some(value);
break;
}
other => return Err(UnknownFlag::Short(other)),
}
}
continue;
}
if out.length.is_none() {
out.length = s.parse().ok();
} else if out.count.is_none() {
out.count = s.parse().ok();
}
}
Ok(out)
}
pub fn run(argv: &[OsString]) -> ExitCode {
let parsed = match parse(argv) {
Ok(p) => p,
Err(unk) => {
let _ = writeln!(std::io::stderr().lock(), "{}", format_unknown_flag(&unk));
return ExitCode::from(2);
}
};
let length = parsed.length.unwrap_or(crate::DEFAULT_LENGTH);
let count_explicit = parsed.count;
let seed_bytes = match parsed.sha1.as_deref() {
Some(spec) => match crate::seed::resolve_seed_input(spec) {
Ok(b) => Some(b),
Err(crate::Error::SeedSourceUnavailable(path)) => {
let _ = writeln!(
std::io::stderr().lock(),
"rusty-pwgen: seed file '{path}' not found"
);
return ExitCode::from(1);
}
Err(e) => {
let _ = writeln!(std::io::stderr().lock(), "rusty-pwgen: {e}");
return ExitCode::from(1);
}
},
None => None,
};
let is_tty = crate::output::is_tty();
let resolved_count = count_explicit.unwrap_or_else(|| {
if is_tty {
let cols_per_row = crate::output::columns() / (length + 1).max(1);
cols_per_row.max(1) * crate::DEFAULT_TTY_ROWS
} else {
crate::DEFAULT_COUNT_PIPED
}
});
let mut builder = PwgenBuilder::new()
.length(length)
.count(resolved_count)
.secure(parsed.secure)
.capitalize(parsed.capitalize.unwrap_or(true))
.numerals(parsed.numerals.unwrap_or(true))
.symbols(parsed.symbols)
.ambiguous_filter(parsed.ambiguous_filter)
.no_vowels(parsed.no_vowels)
.compat(crate::CompatibilityMode::Strict);
if let Some(rc) = parsed.remove_chars {
builder = builder.remove_chars(rc);
}
if let Some(bytes) = seed_bytes {
builder = builder.reproducible_seed(bytes);
}
let mut pwgen: Pwgen = match builder.build() {
Ok(p) => p,
Err(e) => {
let _ = writeln!(std::io::stderr().lock(), "rusty-pwgen: {e}");
return ExitCode::from(1);
}
};
crate::output::emit_passwords(
&mut pwgen,
resolved_count,
parsed.one_column,
parsed.columnar,
);
ExitCode::SUCCESS
}
#[cfg(test)]
mod tests {
use super::*;
fn argv(parts: &[&str]) -> Vec<OsString> {
parts.iter().map(|s| OsString::from(*s)).collect()
}
#[test]
fn format_unknown_short() {
assert_eq!(
format_unknown_flag(&UnknownFlag::Short('x')),
"rusty-pwgen: invalid option -- 'x'"
);
}
#[test]
fn format_unknown_long() {
assert_eq!(
format_unknown_flag(&UnknownFlag::Long(String::from("help"))),
"rusty-pwgen: unknown option -- 'help'"
);
}
#[test]
fn parse_default_flags() {
let r = parse(&argv(&["pwgen"])).unwrap();
assert_eq!(r.length, None);
assert_eq!(r.count, None);
assert!(!r.secure);
}
#[test]
fn parse_secure_with_length() {
let r = parse(&argv(&["pwgen", "-s", "16"])).unwrap();
assert!(r.secure);
assert_eq!(r.length, Some(16));
}
#[test]
fn parse_rejects_help_long_flag() {
let err = parse(&argv(&["pwgen", "--help"])).unwrap_err();
assert_eq!(err, UnknownFlag::Long(String::from("help")));
}
#[test]
fn parse_rejects_version_long_flag() {
let err = parse(&argv(&["pwgen", "--version"])).unwrap_err();
assert_eq!(err, UnknownFlag::Long(String::from("version")));
}
#[test]
fn parse_rejects_unknown_short() {
let err = parse(&argv(&["pwgen", "-x"])).unwrap_err();
assert_eq!(err, UnknownFlag::Short('x'));
}
#[test]
fn parse_last_wins_caps_conflict() {
let r = parse(&argv(&["pwgen", "-c", "-A"])).unwrap();
assert_eq!(r.capitalize, Some(false));
let r = parse(&argv(&["pwgen", "-A", "-c"])).unwrap();
assert_eq!(r.capitalize, Some(true));
}
#[test]
fn parse_grouped_short_flags() {
let r = parse(&argv(&["pwgen", "-cny"])).unwrap();
assert_eq!(r.capitalize, Some(true));
assert_eq!(r.numerals, Some(true));
assert!(r.symbols);
}
#[test]
fn parse_positional_length_count() {
let r = parse(&argv(&["pwgen", "12", "5"])).unwrap();
assert_eq!(r.length, Some(12));
assert_eq!(r.count, Some(5));
}
#[test]
fn parse_n_flag_with_value() {
let r = parse(&argv(&["pwgen", "-N", "5"])).unwrap();
assert_eq!(r.count, Some(5));
}
#[test]
fn parse_first_unknown_short_wins() {
let err = parse(&argv(&["pwgen", "-cAz"])).unwrap_err();
assert_eq!(err, UnknownFlag::Short('z'));
}
}