pub mod charset;
pub mod error;
pub mod phoneme;
pub mod rng;
pub mod secure;
pub use error::Error;
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CompatibilityMode {
#[default]
Default,
Strict,
}
pub const DEFAULT_LENGTH: usize = 8;
pub const DEFAULT_COUNT_PIPED: usize = 1;
pub const DEFAULT_TTY_ROWS: usize = 20;
#[non_exhaustive]
pub struct Pwgen {
length: usize,
count: usize,
secure: bool,
capitalize: bool,
numerals: bool,
symbols: bool,
ambiguous_filter: bool,
no_vowels: bool,
remove_chars: Vec<u8>,
rng: Box<dyn rng::RngSource + Send>,
charset: Vec<u8>,
#[allow(dead_code)]
compat: CompatibilityMode,
}
impl std::fmt::Debug for Pwgen {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Pwgen")
.field("length", &self.length)
.field("count", &self.count)
.field("secure", &self.secure)
.field("capitalize", &self.capitalize)
.field("numerals", &self.numerals)
.field("symbols", &self.symbols)
.field("ambiguous_filter", &self.ambiguous_filter)
.field("no_vowels", &self.no_vowels)
.field("compat", &self.compat)
.finish()
}
}
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct PwgenBuilder {
length: usize,
count: usize,
secure: bool,
capitalize: bool,
numerals: bool,
symbols: bool,
ambiguous_filter: bool,
no_vowels: bool,
remove_chars: Vec<u8>,
reproducible_seed: Option<Vec<u8>>,
compat: CompatibilityMode,
}
impl Default for PwgenBuilder {
fn default() -> Self {
Self::new()
}
}
impl PwgenBuilder {
#[must_use]
pub fn new() -> Self {
Self {
length: DEFAULT_LENGTH,
count: DEFAULT_COUNT_PIPED,
secure: false,
capitalize: true,
numerals: true,
symbols: false,
ambiguous_filter: false,
no_vowels: false,
remove_chars: Vec::new(),
reproducible_seed: None,
compat: CompatibilityMode::Default,
}
}
#[must_use]
pub fn length(mut self, length: usize) -> Self {
self.length = length;
self
}
#[must_use]
pub fn count(mut self, count: usize) -> Self {
self.count = count;
self
}
#[must_use]
pub fn secure(mut self, secure: bool) -> Self {
self.secure = secure;
self
}
#[must_use]
pub fn capitalize(mut self, capitalize: bool) -> Self {
self.capitalize = capitalize;
self
}
#[must_use]
pub fn numerals(mut self, numerals: bool) -> Self {
self.numerals = numerals;
self
}
#[must_use]
pub fn symbols(mut self, symbols: bool) -> Self {
self.symbols = symbols;
self
}
#[must_use]
pub fn ambiguous_filter(mut self, ambiguous_filter: bool) -> Self {
self.ambiguous_filter = ambiguous_filter;
self
}
#[must_use]
pub fn no_vowels(mut self, no_vowels: bool) -> Self {
self.no_vowels = no_vowels;
self
}
#[must_use]
pub fn remove_chars(mut self, chars: impl Into<String>) -> Self {
self.remove_chars = chars.into().into_bytes();
self
}
#[must_use]
pub fn reproducible_seed(mut self, seed: impl Into<Vec<u8>>) -> Self {
self.reproducible_seed = Some(seed.into());
self
}
#[must_use]
pub fn compat(mut self, compat: CompatibilityMode) -> Self {
self.compat = compat;
self
}
pub fn build(mut self) -> Result<Pwgen, Error> {
if self.no_vowels || !self.remove_chars.is_empty() || self.length < 5 {
self.secure = true;
}
let charset = charset::build(
charset::CharSetFlags {
capitalize: self.capitalize,
numerals: self.numerals,
symbols: self.symbols,
ambiguous_filter: self.ambiguous_filter,
no_vowels: self.no_vowels,
},
&self.remove_chars,
);
if charset.is_empty() && self.length > 0 {
return Err(Error::InvalidBuilderConfiguration(
"empty character set after applying filters",
));
}
let rng: Box<dyn rng::RngSource + Send> = if let Some(seed_bytes) = self.reproducible_seed {
use sha2::Digest;
let digest = sha2::Sha256::digest(&seed_bytes);
let mut seed = [0u8; 32];
seed.copy_from_slice(&digest);
Box::new(rng::SeededSource::from_seed(seed))
} else {
Box::new(rng::OsRngSource::new())
};
Ok(Pwgen {
length: self.length,
count: self.count,
secure: self.secure,
capitalize: self.capitalize,
numerals: self.numerals,
symbols: self.symbols,
ambiguous_filter: self.ambiguous_filter,
no_vowels: self.no_vowels,
remove_chars: self.remove_chars,
rng,
charset,
compat: self.compat,
})
}
}
impl Pwgen {
pub fn generate_one(&mut self) -> String {
if self.secure {
secure::generate(&mut *self.rng, &self.charset, self.length)
} else {
phoneme::generate(&mut *self.rng, self.length, self.capitalize, self.numerals)
}
}
pub fn generate_n(&mut self, n: usize) -> Vec<String> {
let mut out = Vec::with_capacity(n);
for _ in 0..n {
out.push(self.generate_one());
}
out
}
pub fn iter(&mut self) -> impl Iterator<Item = String> + '_ {
std::iter::from_fn(move || Some(self.generate_one()))
}
pub fn count(&self) -> usize {
self.count
}
#[allow(dead_code)]
pub(crate) fn debug_charset(&self) -> &[u8] {
&self.charset
}
#[allow(dead_code)]
pub(crate) fn debug_filters(&self) -> (bool, bool, bool, bool, bool, &[u8]) {
(
self.capitalize,
self.numerals,
self.symbols,
self.ambiguous_filter,
self.no_vowels,
&self.remove_chars,
)
}
}
#[cfg(feature = "cli")]
pub mod cli;
#[cfg(feature = "cli")]
pub mod mode;
#[cfg(feature = "cli")]
pub mod output;
#[cfg(feature = "cli")]
pub mod seed;
#[cfg(feature = "cli")]
pub mod strict;
#[cfg(feature = "cli")]
pub fn run() -> std::process::ExitCode {
use clap::Parser;
use std::ffi::OsString;
use std::process::ExitCode;
let raw_argv: Vec<OsString> = std::env::args_os().collect();
let pre_strict = strict::pre_scan_strict_flag(&raw_argv);
let env_strict = std::env::var_os("RUSTY_PWGEN_STRICT");
let argv0 = raw_argv.first().cloned();
let resolved_mode = mode::resolve(pre_strict, env_strict.as_deref(), argv0.as_deref());
if resolved_mode == CompatibilityMode::Strict {
return strict::run(&raw_argv);
}
let cli_args = match cli::Cli::try_parse() {
Ok(args) => args,
Err(e) => {
e.print().ok();
return match e.kind() {
clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion => {
ExitCode::SUCCESS
}
_ => ExitCode::from(2),
};
}
};
if let Some(cli::Subcommand::Completions { shell }) = cli_args.command {
use clap::CommandFactory;
let mut cmd = cli::Cli::command();
let name = cmd.get_name().to_string();
clap_complete::generate(shell, &mut cmd, name, &mut std::io::stdout());
return ExitCode::SUCCESS;
}
let seed_bytes = match cli_args.sha1.as_deref() {
Some(spec) => match seed::resolve_seed_input(spec) {
Ok(b) => Some(b),
Err(Error::SeedSourceUnavailable(path)) => {
eprintln!("rusty-pwgen: seed file '{path}' not found");
return ExitCode::from(1);
}
Err(e) => {
eprintln!("rusty-pwgen: {e}");
return ExitCode::from(1);
}
},
None => None,
};
if !cli_args.secure
&& cli_args.length < 5
&& !cli_args.no_vowels
&& cli_args.remove_chars.is_none()
{
eprintln!(
"rusty-pwgen: pronounceable mode requires length >= 5; using secure mode for length {}",
cli_args.length
);
}
let resolved_count = cli_args.resolved_count(output::is_tty());
let mut builder = PwgenBuilder::new()
.length(cli_args.length)
.count(resolved_count)
.secure(cli_args.secure)
.capitalize(cli_args.capitalize_effective())
.numerals(cli_args.numerals_effective())
.symbols(cli_args.symbols)
.ambiguous_filter(cli_args.ambiguous_filter)
.no_vowels(cli_args.no_vowels);
if let Some(rc) = &cli_args.remove_chars {
builder = builder.remove_chars(rc.clone());
}
if let Some(bytes) = seed_bytes {
builder = builder.reproducible_seed(bytes);
}
let mut pwgen = match builder.build() {
Ok(p) => p,
Err(e) => {
eprintln!("rusty-pwgen: {e}");
return ExitCode::from(1);
}
};
output::emit_passwords(
&mut pwgen,
resolved_count,
cli_args.one_column,
cli_args.columnar,
);
ExitCode::SUCCESS
}