1pub mod charset;
28pub mod error;
29pub mod phoneme;
30pub mod rng;
31pub mod secure;
32
33pub use error::Error;
34
35#[non_exhaustive]
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
46pub enum CompatibilityMode {
47 #[default]
49 Default,
50 Strict,
52}
53
54pub const DEFAULT_LENGTH: usize = 8;
56
57pub const DEFAULT_COUNT_PIPED: usize = 1;
59
60pub const DEFAULT_TTY_ROWS: usize = 20;
62
63#[non_exhaustive]
65pub struct Pwgen {
66 length: usize,
67 count: usize,
68 secure: bool,
69 capitalize: bool,
70 numerals: bool,
71 symbols: bool,
72 ambiguous_filter: bool,
73 no_vowels: bool,
74 remove_chars: Vec<u8>,
75 rng: Box<dyn rng::RngSource + Send>,
76 charset: Vec<u8>,
78 #[allow(dead_code)]
79 compat: CompatibilityMode,
80}
81
82impl std::fmt::Debug for Pwgen {
83 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84 f.debug_struct("Pwgen")
85 .field("length", &self.length)
86 .field("count", &self.count)
87 .field("secure", &self.secure)
88 .field("capitalize", &self.capitalize)
89 .field("numerals", &self.numerals)
90 .field("symbols", &self.symbols)
91 .field("ambiguous_filter", &self.ambiguous_filter)
92 .field("no_vowels", &self.no_vowels)
93 .field("compat", &self.compat)
94 .finish()
95 }
96}
97
98#[non_exhaustive]
113#[derive(Debug, Clone)]
114pub struct PwgenBuilder {
115 length: usize,
116 count: usize,
117 secure: bool,
118 capitalize: bool,
119 numerals: bool,
120 symbols: bool,
121 ambiguous_filter: bool,
122 no_vowels: bool,
123 remove_chars: Vec<u8>,
124 reproducible_seed: Option<Vec<u8>>,
125 compat: CompatibilityMode,
126}
127
128impl Default for PwgenBuilder {
129 fn default() -> Self {
130 Self::new()
131 }
132}
133
134impl PwgenBuilder {
135 #[must_use]
138 pub fn new() -> Self {
139 Self {
140 length: DEFAULT_LENGTH,
141 count: DEFAULT_COUNT_PIPED,
142 secure: false,
143 capitalize: true,
144 numerals: true,
145 symbols: false,
146 ambiguous_filter: false,
147 no_vowels: false,
148 remove_chars: Vec::new(),
149 reproducible_seed: None,
150 compat: CompatibilityMode::Default,
151 }
152 }
153
154 #[must_use]
155 pub fn length(mut self, length: usize) -> Self {
156 self.length = length;
157 self
158 }
159
160 #[must_use]
161 pub fn count(mut self, count: usize) -> Self {
162 self.count = count;
163 self
164 }
165
166 #[must_use]
167 pub fn secure(mut self, secure: bool) -> Self {
168 self.secure = secure;
169 self
170 }
171
172 #[must_use]
173 pub fn capitalize(mut self, capitalize: bool) -> Self {
174 self.capitalize = capitalize;
175 self
176 }
177
178 #[must_use]
179 pub fn numerals(mut self, numerals: bool) -> Self {
180 self.numerals = numerals;
181 self
182 }
183
184 #[must_use]
185 pub fn symbols(mut self, symbols: bool) -> Self {
186 self.symbols = symbols;
187 self
188 }
189
190 #[must_use]
191 pub fn ambiguous_filter(mut self, ambiguous_filter: bool) -> Self {
192 self.ambiguous_filter = ambiguous_filter;
193 self
194 }
195
196 #[must_use]
197 pub fn no_vowels(mut self, no_vowels: bool) -> Self {
198 self.no_vowels = no_vowels;
199 self
200 }
201
202 #[must_use]
203 pub fn remove_chars(mut self, chars: impl Into<String>) -> Self {
204 self.remove_chars = chars.into().into_bytes();
205 self
206 }
207
208 #[must_use]
209 pub fn reproducible_seed(mut self, seed: impl Into<Vec<u8>>) -> Self {
210 self.reproducible_seed = Some(seed.into());
211 self
212 }
213
214 #[must_use]
215 pub fn compat(mut self, compat: CompatibilityMode) -> Self {
216 self.compat = compat;
217 self
218 }
219
220 pub fn build(mut self) -> Result<Pwgen, Error> {
227 if self.no_vowels || !self.remove_chars.is_empty() || self.length < 5 {
229 self.secure = true;
230 }
231
232 let charset = charset::build(
233 charset::CharSetFlags {
234 capitalize: self.capitalize,
235 numerals: self.numerals,
236 symbols: self.symbols,
237 ambiguous_filter: self.ambiguous_filter,
238 no_vowels: self.no_vowels,
239 },
240 &self.remove_chars,
241 );
242
243 if charset.is_empty() && self.length > 0 {
244 return Err(Error::InvalidBuilderConfiguration(
245 "empty character set after applying filters",
246 ));
247 }
248
249 let rng: Box<dyn rng::RngSource + Send> = if let Some(seed_bytes) = self.reproducible_seed {
250 use sha2::Digest;
252 let digest = sha2::Sha256::digest(&seed_bytes);
253 let mut seed = [0u8; 32];
254 seed.copy_from_slice(&digest);
255 Box::new(rng::SeededSource::from_seed(seed))
256 } else {
257 Box::new(rng::OsRngSource::new())
258 };
259
260 Ok(Pwgen {
261 length: self.length,
262 count: self.count,
263 secure: self.secure,
264 capitalize: self.capitalize,
265 numerals: self.numerals,
266 symbols: self.symbols,
267 ambiguous_filter: self.ambiguous_filter,
268 no_vowels: self.no_vowels,
269 remove_chars: self.remove_chars,
270 rng,
271 charset,
272 compat: self.compat,
273 })
274 }
275}
276
277impl Pwgen {
278 pub fn generate_one(&mut self) -> String {
280 if self.secure {
281 secure::generate(&mut *self.rng, &self.charset, self.length)
282 } else {
283 phoneme::generate(&mut *self.rng, self.length, self.capitalize, self.numerals)
284 }
285 }
286
287 pub fn generate_n(&mut self, n: usize) -> Vec<String> {
289 let mut out = Vec::with_capacity(n);
290 for _ in 0..n {
291 out.push(self.generate_one());
292 }
293 out
294 }
295
296 pub fn iter(&mut self) -> impl Iterator<Item = String> + '_ {
299 std::iter::from_fn(move || Some(self.generate_one()))
300 }
301
302 pub fn count(&self) -> usize {
304 self.count
305 }
306
307 #[allow(dead_code)]
309 pub(crate) fn debug_charset(&self) -> &[u8] {
310 &self.charset
311 }
312
313 #[allow(dead_code)]
315 pub(crate) fn debug_filters(&self) -> (bool, bool, bool, bool, bool, &[u8]) {
316 (
317 self.capitalize,
318 self.numerals,
319 self.symbols,
320 self.ambiguous_filter,
321 self.no_vowels,
322 &self.remove_chars,
323 )
324 }
325}
326
327#[cfg(feature = "cli")]
329pub mod cli;
330#[cfg(feature = "cli")]
331pub mod mode;
332#[cfg(feature = "cli")]
333pub mod output;
334#[cfg(feature = "cli")]
335pub mod seed;
336#[cfg(feature = "cli")]
337pub mod strict;
338
339#[cfg(feature = "cli")]
341pub fn run() -> std::process::ExitCode {
342 use clap::Parser;
343 use std::ffi::OsString;
344 use std::process::ExitCode;
345
346 let raw_argv: Vec<OsString> = std::env::args_os().collect();
347 let pre_strict = strict::pre_scan_strict_flag(&raw_argv);
348 let env_strict = std::env::var_os("RUSTY_PWGEN_STRICT");
349 let argv0 = raw_argv.first().cloned();
350 let resolved_mode = mode::resolve(pre_strict, env_strict.as_deref(), argv0.as_deref());
351
352 if resolved_mode == CompatibilityMode::Strict {
353 return strict::run(&raw_argv);
354 }
355
356 let cli_args = match cli::Cli::try_parse() {
357 Ok(args) => args,
358 Err(e) => {
359 e.print().ok();
360 return match e.kind() {
361 clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion => {
362 ExitCode::SUCCESS
363 }
364 _ => ExitCode::from(2),
365 };
366 }
367 };
368
369 if let Some(cli::Subcommand::Completions { shell }) = cli_args.command {
371 use clap::CommandFactory;
372 let mut cmd = cli::Cli::command();
373 let name = cmd.get_name().to_string();
374 clap_complete::generate(shell, &mut cmd, name, &mut std::io::stdout());
375 return ExitCode::SUCCESS;
376 }
377
378 let seed_bytes = match cli_args.sha1.as_deref() {
380 Some(spec) => match seed::resolve_seed_input(spec) {
381 Ok(b) => Some(b),
382 Err(Error::SeedSourceUnavailable(path)) => {
383 eprintln!("rusty-pwgen: seed file '{path}' not found");
384 return ExitCode::from(1);
385 }
386 Err(e) => {
387 eprintln!("rusty-pwgen: {e}");
388 return ExitCode::from(1);
389 }
390 },
391 None => None,
392 };
393
394 if !cli_args.secure
396 && cli_args.length < 5
397 && !cli_args.no_vowels
398 && cli_args.remove_chars.is_none()
399 {
400 eprintln!(
401 "rusty-pwgen: pronounceable mode requires length >= 5; using secure mode for length {}",
402 cli_args.length
403 );
404 }
405
406 let resolved_count = cli_args.resolved_count(output::is_tty());
408
409 let mut builder = PwgenBuilder::new()
410 .length(cli_args.length)
411 .count(resolved_count)
412 .secure(cli_args.secure)
413 .capitalize(cli_args.capitalize_effective())
414 .numerals(cli_args.numerals_effective())
415 .symbols(cli_args.symbols)
416 .ambiguous_filter(cli_args.ambiguous_filter)
417 .no_vowels(cli_args.no_vowels);
418 if let Some(rc) = &cli_args.remove_chars {
419 builder = builder.remove_chars(rc.clone());
420 }
421 if let Some(bytes) = seed_bytes {
422 builder = builder.reproducible_seed(bytes);
423 }
424
425 let mut pwgen = match builder.build() {
426 Ok(p) => p,
427 Err(e) => {
428 eprintln!("rusty-pwgen: {e}");
429 return ExitCode::from(1);
430 }
431 };
432
433 output::emit_passwords(
434 &mut pwgen,
435 resolved_count,
436 cli_args.one_column,
437 cli_args.columnar,
438 );
439 ExitCode::SUCCESS
440}