mirror of
https://github.com/elwrcl/pasx.git
synced 2026-10-31 19:08:04 +03:00
init
This commit is contained in:
22 files changed
+11922
No files matched your search
+253
@@ -0,0 +1,253 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user