mirror of
https://github.com/elwrcl/pasx.git
synced 2026-10-31 10:58:04 +03:00
254 lines
7.0 KiB
Rust
254 lines
7.0 KiB
Rust
use crate::generator::charset::CharsetOptions;
|
|
use crate::generator::passphrase::PassphraseOptions;
|
|
use clap::{Args, Parser, Subcommand, ValueEnum};
|
|
|
|
#[derive(Parser, Debug)]
|
|
#[command(name = "pasx", version, about = "Terminal password generator")]
|
|
pub struct Cli {
|
|
#[command(subcommand)]
|
|
pub command: Option<Command>,
|
|
|
|
#[command(flatten)]
|
|
pub opts: GenArgs,
|
|
}
|
|
|
|
#[derive(Subcommand, Debug)]
|
|
pub enum Command {
|
|
/// Generate one password, print it, and exit
|
|
Gen(GenCmdArgs),
|
|
/// Print a shell completion script
|
|
Completions {
|
|
#[arg(value_enum)]
|
|
shell: ShellKind,
|
|
},
|
|
}
|
|
|
|
#[derive(Args, Debug)]
|
|
pub struct GenCmdArgs {
|
|
#[command(flatten)]
|
|
pub opts: GenArgs,
|
|
|
|
/// Also copy the password to the clipboard
|
|
#[arg(long)]
|
|
pub copy: bool,
|
|
}
|
|
|
|
#[derive(Args, Debug, Clone)]
|
|
pub struct GenArgs {
|
|
/// Generation mode
|
|
#[arg(short = 'm', long, value_enum, default_value_t = Mode::Charset)]
|
|
pub mode: Mode,
|
|
|
|
/// Password length in characters
|
|
#[arg(short = 'l', long, default_value_t = 18)]
|
|
pub length: usize,
|
|
|
|
/// Exclude uppercase letters
|
|
#[arg(long)]
|
|
pub no_upper: bool,
|
|
|
|
/// Exclude lowercase letters
|
|
#[arg(long)]
|
|
pub no_lower: bool,
|
|
|
|
/// Exclude digits
|
|
#[arg(long)]
|
|
pub no_digits: bool,
|
|
|
|
/// Exclude symbols
|
|
#[arg(long)]
|
|
pub no_symbols: bool,
|
|
|
|
/// Exclude look-alike characters (0 O o l 1 I)
|
|
#[arg(long)]
|
|
pub exclude_ambiguous: bool,
|
|
|
|
/// Number of words in passphrase mode
|
|
#[arg(short = 'w', long, default_value_t = 5)]
|
|
pub words: usize,
|
|
|
|
/// Separator between words in passphrase mode
|
|
#[arg(long, default_value_t = '-')]
|
|
pub separator: char,
|
|
|
|
/// Capitalize each word in passphrase mode
|
|
#[arg(long)]
|
|
pub capitalize: bool,
|
|
|
|
/// Append a random digit in passphrase mode
|
|
#[arg(long)]
|
|
pub add_number: bool,
|
|
}
|
|
|
|
#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum Mode {
|
|
Charset,
|
|
Passphrase,
|
|
}
|
|
|
|
#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum ShellKind {
|
|
Nu,
|
|
Bash,
|
|
Zsh,
|
|
Fish,
|
|
}
|
|
|
|
impl GenArgs {
|
|
pub fn charset_options(&self) -> CharsetOptions {
|
|
CharsetOptions {
|
|
length: self.length,
|
|
upper: !self.no_upper,
|
|
lower: !self.no_lower,
|
|
digits: !self.no_digits,
|
|
symbols: !self.no_symbols,
|
|
exclude_ambiguous: self.exclude_ambiguous,
|
|
}
|
|
}
|
|
|
|
pub fn passphrase_options(&self) -> PassphraseOptions {
|
|
PassphraseOptions {
|
|
words: self.words,
|
|
separator: self.separator,
|
|
capitalize: self.capitalize,
|
|
add_number: self.add_number,
|
|
}
|
|
}
|
|
}
|
|
|
|
use clap::CommandFactory;
|
|
|
|
pub fn write_completions(shell: ShellKind, out: &mut impl std::io::Write) -> std::io::Result<()> {
|
|
let mut cmd = Cli::command();
|
|
let name = cmd.get_name().to_string();
|
|
|
|
// clap_complete panics internally if the writer it is handed fails, so it
|
|
// renders into memory and the fallible write happens here.
|
|
let mut script = Vec::new();
|
|
match shell {
|
|
ShellKind::Nu => {
|
|
clap_complete::generate(clap_complete_nushell::Nushell, &mut cmd, name, &mut script)
|
|
}
|
|
ShellKind::Bash => {
|
|
clap_complete::generate(clap_complete::Shell::Bash, &mut cmd, name, &mut script)
|
|
}
|
|
ShellKind::Zsh => {
|
|
clap_complete::generate(clap_complete::Shell::Zsh, &mut cmd, name, &mut script)
|
|
}
|
|
ShellKind::Fish => {
|
|
clap_complete::generate(clap_complete::Shell::Fish, &mut cmd, name, &mut script)
|
|
}
|
|
}
|
|
out.write_all(&script)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use clap::{CommandFactory, Parser};
|
|
|
|
#[test]
|
|
fn argument_definition_is_valid() {
|
|
Cli::command().debug_assert();
|
|
}
|
|
|
|
#[test]
|
|
fn bare_invocation_has_no_subcommand_and_full_defaults() {
|
|
let cli = Cli::parse_from(["pasx"]);
|
|
assert!(cli.command.is_none());
|
|
assert_eq!(cli.opts.mode, Mode::Charset);
|
|
assert_eq!(cli.opts.charset_options(), CharsetOptions::default());
|
|
assert_eq!(cli.opts.passphrase_options(), PassphraseOptions::default());
|
|
}
|
|
|
|
#[test]
|
|
fn negative_flags_switch_classes_off() {
|
|
let cli = Cli::parse_from(["pasx", "--length", "24", "--no-symbols", "--no-upper"]);
|
|
let opts = cli.opts.charset_options();
|
|
assert_eq!(opts.length, 24);
|
|
assert!(!opts.symbols);
|
|
assert!(!opts.upper);
|
|
assert!(opts.lower);
|
|
assert!(opts.digits);
|
|
}
|
|
|
|
#[test]
|
|
fn passphrase_flags_reach_the_options() {
|
|
let cli = Cli::parse_from([
|
|
"pasx",
|
|
"--mode",
|
|
"passphrase",
|
|
"--words",
|
|
"7",
|
|
"--separator",
|
|
".",
|
|
"--capitalize",
|
|
]);
|
|
assert_eq!(cli.opts.mode, Mode::Passphrase);
|
|
let opts = cli.opts.passphrase_options();
|
|
assert_eq!(opts.words, 7);
|
|
assert_eq!(opts.separator, '.');
|
|
assert!(opts.capitalize);
|
|
assert!(!opts.add_number);
|
|
}
|
|
|
|
#[test]
|
|
fn gen_subcommand_takes_the_same_flags_plus_copy() {
|
|
let cli = Cli::parse_from(["pasx", "gen", "-l", "32", "--copy"]);
|
|
match cli.command {
|
|
Some(Command::Gen(args)) => {
|
|
assert_eq!(args.opts.charset_options().length, 32);
|
|
assert!(args.copy);
|
|
}
|
|
other => panic!("expected gen subcommand, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn completions_subcommand_parses_every_shell() {
|
|
for (arg, expected) in [
|
|
("nu", ShellKind::Nu),
|
|
("bash", ShellKind::Bash),
|
|
("zsh", ShellKind::Zsh),
|
|
("fish", ShellKind::Fish),
|
|
] {
|
|
let cli = Cli::parse_from(["pasx", "completions", arg]);
|
|
match cli.command {
|
|
Some(Command::Completions { shell }) => assert_eq!(shell, expected),
|
|
other => panic!("expected completions subcommand, got {other:?}"),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn exclude_ambiguous_reaches_the_options() {
|
|
let cli = Cli::parse_from(["pasx", "--exclude-ambiguous"]);
|
|
assert!(cli.opts.charset_options().exclude_ambiguous);
|
|
}
|
|
|
|
#[test]
|
|
fn nushell_completions_mention_the_binary_and_its_flags() {
|
|
let mut buf = Vec::new();
|
|
write_completions(ShellKind::Nu, &mut buf).unwrap();
|
|
let script = String::from_utf8(buf).unwrap();
|
|
assert!(script.contains("pasx"), "{script}");
|
|
assert!(script.contains("--exclude-ambiguous"), "{script}");
|
|
assert!(script.contains("pasx gen"), "{script}");
|
|
}
|
|
|
|
#[test]
|
|
fn every_shell_produces_a_non_empty_script() {
|
|
let shells = [
|
|
ShellKind::Nu,
|
|
ShellKind::Bash,
|
|
ShellKind::Zsh,
|
|
ShellKind::Fish,
|
|
];
|
|
for shell in shells {
|
|
let mut buf = Vec::new();
|
|
write_completions(shell, &mut buf).unwrap();
|
|
assert!(!buf.is_empty(), "{shell:?} produced nothing");
|
|
}
|
|
}
|
|
}
|