rpg_util/
lib.rs

1//! # RPG - Rust Password Generator
2//!
3//! A fast, secure, and customizable password generator library.
4//!
5//! ## Features
6//!
7//! - Customizable character sets
8//! - Character exclusion with range support
9//! - Minimum character type requirements
10//! - Pattern-based generation
11//! - Uniform character distribution
12//!
13//! ## Example
14//!
15//! ```rust
16//! use rpg_util::{GenerationParams, PasswordArgs, build_char_set, generate_passwords};
17//! use rand::Rng;
18//!
19//! let args = PasswordArgs {
20//!     capitals_off: false,
21//!     numerals_off: false,
22//!     symbols_off: false,
23//!     exclude_chars: vec![],
24//!     include_chars: None,
25//!     min_capitals: None,
26//!     min_numerals: None,
27//!     min_symbols: None,
28//!     pattern: None,
29//!     length: 16,
30//!     password_count: 1,
31//! };
32//!
33//! let char_set = build_char_set(&args).unwrap();
34//! let mut rng = rand::rng();
35//! let gen_params = rpg_util::GenerationParams {
36//!     length: 16,
37//!     count: 1,
38//!     min_capitals: None,
39//!     min_numerals: None,
40//!     min_symbols: None,
41//!     pattern: None,
42//! };
43//! let passwords = rpg_util::generate_passwords(&char_set, &gen_params, &mut rng);
44//! ```
45
46use rand::Rng;
47use std::collections::HashSet;
48use std::fmt;
49
50/// Calculates password entropy in bits
51pub fn calculate_entropy(char_set_size: usize, length: u32) -> f64 {
52    (char_set_size as f64).log2() * length as f64
53}
54
55/// Custom error type for password generation
56#[derive(Debug, Clone)]
57pub enum PasswordError {
58    InvalidLength,
59    InvalidLengthTooLong,
60    InvalidCount,
61    EmptyCharacterSet,
62    AllTypesDisabled,
63}
64
65impl fmt::Display for PasswordError {
66    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67        match self {
68            PasswordError::InvalidLength => {
69                write!(f, "Error: Password length must be greater than 0.")
70            }
71            PasswordError::InvalidLengthTooLong => {
72                write!(
73                    f,
74                    "Error: Password length exceeds maximum of 10,000 characters."
75                )
76            }
77            PasswordError::InvalidCount => {
78                write!(f, "Error: Password count must be greater than 0.")
79            }
80            PasswordError::EmptyCharacterSet => {
81                write!(
82                    f,
83                    "Error: All characters have been excluded or disabled. Cannot generate passwords.\n\
84                    Hint: Try removing some character exclusions or enabling character types."
85                )
86            }
87            PasswordError::AllTypesDisabled => {
88                write!(
89                    f,
90                    "Error: All character types are disabled and/or all remaining characters are excluded.\n\
91                    Hint: At least one character type must be enabled. Try removing --capitals-off, --numerals-off, or --symbols-off."
92                )
93            }
94        }
95    }
96}
97
98impl std::error::Error for PasswordError {}
99
100// ASCII character range constants
101const ASCII_LOWERCASE_START: u8 = b'a';
102const ASCII_LOWERCASE_END: u8 = b'z';
103const ASCII_UPPERCASE_START: u8 = b'A';
104const ASCII_UPPERCASE_END: u8 = b'Z';
105const ASCII_NUMERAL_START: u8 = b'0';
106const ASCII_NUMERAL_END: u8 = b'9';
107const ASCII_SYMBOL_RANGE_1_START: u8 = 33; // !
108const ASCII_SYMBOL_RANGE_1_END: u8 = 47; // /
109const ASCII_SYMBOL_RANGE_2_START: u8 = 58; // :
110const ASCII_SYMBOL_RANGE_2_END: u8 = 64; // @
111const ASCII_SYMBOL_RANGE_3_START: u8 = 91; // [
112const ASCII_SYMBOL_RANGE_3_END: u8 = 96; // `
113const ASCII_SYMBOL_RANGE_4_START: u8 = 123; // {
114const ASCII_SYMBOL_RANGE_4_END: u8 = 126; // ~
115
116/// Pattern character types
117#[derive(Debug, Clone, Copy)]
118pub enum PatternChar {
119    Lowercase,
120    Uppercase,
121    Numeric,
122    Symbol,
123}
124
125/// Parameters for password generation
126#[derive(Debug, Clone)]
127pub struct GenerationParams {
128    pub length: u32,
129    pub count: u32,
130    pub min_capitals: Option<u32>,
131    pub min_numerals: Option<u32>,
132    pub min_symbols: Option<u32>,
133    pub pattern: Option<Vec<PatternChar>>,
134}
135
136/// Arguments structure for password generation
137pub struct PasswordArgs {
138    pub capitals_off: bool,
139    pub numerals_off: bool,
140    pub symbols_off: bool,
141    pub exclude_chars: Vec<char>,
142    pub include_chars: Option<Vec<char>>,
143    pub min_capitals: Option<u32>,
144    pub min_numerals: Option<u32>,
145    pub min_symbols: Option<u32>,
146    pub pattern: Option<Vec<PatternChar>>,
147    pub length: u32,
148    pub password_count: u32,
149}
150
151/// Parses character exclusion strings, expanding ranges like "a-z" or "0-9"
152/// Returns a vector of individual characters to exclude
153///
154/// # Examples
155/// - "a-z" expands to all lowercase letters
156/// - "0-9" expands to all digits
157/// - "a-c" expands to 'a', 'b', 'c'
158/// - "abc" is treated as individual characters 'a', 'b', 'c'
159/// - "a-z,0-9,b" combines ranges and individual characters
160pub fn parse_exclude_chars(exclude_strings: Vec<String>) -> Result<Vec<char>, String> {
161    let mut exclude_chars = Vec::new();
162
163    for s in exclude_strings {
164        // Check if it's a range (contains a dash with characters on both sides)
165        // Range format: "X-Y" where X and Y are single characters
166        if s.len() == 3 {
167            let chars: Vec<char> = s.chars().collect();
168            if chars[1] == '-' {
169                let start = chars[0] as u8;
170                let end = chars[2] as u8;
171
172                // Validate range (start must be <= end, and both must be ASCII printable)
173                if start <= end && start >= 32 && end < 127 {
174                    for byte in start..=end {
175                        exclude_chars.push(byte as char);
176                    }
177                    continue;
178                } else if start > end {
179                    return Err(format!(
180                        "Invalid range '{}': start character '{}' is greater than end character '{}'",
181                        s, chars[0], chars[2]
182                    ));
183                }
184            }
185        }
186
187        // If not a range, treat as individual character(s)
188        for c in s.chars() {
189            if !exclude_chars.contains(&c) {
190                exclude_chars.push(c);
191            }
192        }
193    }
194
195    Ok(exclude_chars)
196}
197
198/// Builds the character set based on command-line arguments
199/// Returns a vector of valid characters that can be used for password generation
200pub fn build_char_set(args: &PasswordArgs) -> Result<Vec<u8>, PasswordError> {
201    let mut chars = Vec::new();
202
203    // If include_chars is specified, use only those characters
204    if let Some(ref include_chars) = args.include_chars {
205        for &c in include_chars {
206            chars.push(c as u8);
207        }
208    } else {
209        // Pre-allocate with estimated capacity (max ~94 printable ASCII chars)
210        let estimated_capacity = if args.symbols_off {
211            62 // 26 lowercase + 26 uppercase + 10 numerals
212        } else {
213            94 // All printable ASCII
214        };
215        chars.reserve(estimated_capacity);
216
217        // Add lowercase letters (always included)
218        chars.extend(ASCII_LOWERCASE_START..=ASCII_LOWERCASE_END);
219
220        // Add uppercase letters if not disabled
221        if !args.capitals_off {
222            chars.extend(ASCII_UPPERCASE_START..=ASCII_UPPERCASE_END);
223        }
224
225        // Add numerals if not disabled
226        if !args.numerals_off {
227            chars.extend(ASCII_NUMERAL_START..=ASCII_NUMERAL_END);
228        }
229
230        // Add symbols if not disabled (complete ASCII printable symbol ranges)
231        if !args.symbols_off {
232            chars.extend(ASCII_SYMBOL_RANGE_1_START..=ASCII_SYMBOL_RANGE_1_END);
233            chars.extend(ASCII_SYMBOL_RANGE_2_START..=ASCII_SYMBOL_RANGE_2_END);
234            chars.extend(ASCII_SYMBOL_RANGE_3_START..=ASCII_SYMBOL_RANGE_3_END);
235            chars.extend(ASCII_SYMBOL_RANGE_4_START..=ASCII_SYMBOL_RANGE_4_END);
236        }
237    }
238
239    // Convert exclude_chars Vec to HashSet for O(1) lookup
240    let exclude_set: HashSet<char> = args.exclude_chars.iter().cloned().collect();
241
242    // Filter out excluded characters
243    chars.retain(|&b| !exclude_set.contains(&(b as char)));
244
245    // Validate that we have at least one character available
246    if chars.is_empty() {
247        return Err(PasswordError::EmptyCharacterSet);
248    }
249
250    Ok(chars)
251}
252
253/// Maximum allowed password length to prevent memory issues
254const MAX_PASSWORD_LENGTH: u32 = 10_000;
255
256/// Validates command-line arguments
257pub fn validate_args(args: &PasswordArgs) -> Result<(), PasswordError> {
258    if args.length == 0 {
259        return Err(PasswordError::InvalidLength);
260    }
261
262    if args.length > MAX_PASSWORD_LENGTH {
263        return Err(PasswordError::InvalidLengthTooLong);
264    }
265
266    if args.password_count == 0 {
267        return Err(PasswordError::InvalidCount);
268    }
269
270    // Check if all character types are disabled
271    if args.capitals_off && args.numerals_off && args.symbols_off {
272        // Only lowercase letters remain, which is valid
273        // But we should check if they're all excluded
274        let test_set = build_char_set(args)?;
275        if test_set.is_empty() {
276            return Err(PasswordError::AllTypesDisabled);
277        }
278    }
279
280    Ok(())
281}
282
283/// Calculates the number of columns for table output
284pub fn column_count(password_count: u32) -> usize {
285    // Use a more reasonable default: prefer 3-4 columns for readability
286    // but adapt based on count
287    match password_count {
288        1..=3 => 1,
289        4..=8 => 2,
290        9..=15 => 3,
291        16..=24 => 4,
292        _ => {
293            // For larger counts, use a divisor that makes sense
294            if password_count.is_multiple_of(5) {
295                5
296            } else if password_count.is_multiple_of(4) {
297                4
298            } else if password_count.is_multiple_of(3) {
299                3
300            } else if password_count.is_multiple_of(2) {
301                2
302            } else {
303                3 // Default to 3 columns for readability
304            }
305        }
306    }
307}
308
309/// Parses a pattern string like "LLLNNNSSS" into PatternChar vector
310pub fn parse_pattern(pattern: &str) -> Result<Vec<PatternChar>, String> {
311    let mut result = Vec::new();
312    for c in pattern.chars() {
313        match c {
314            'L' | 'l' => result.push(PatternChar::Lowercase),
315            'U' | 'u' => result.push(PatternChar::Uppercase),
316            'N' | 'n' => result.push(PatternChar::Numeric),
317            'S' | 's' => result.push(PatternChar::Symbol),
318            _ => {
319                return Err(format!(
320                    "Invalid pattern character: '{}'. Use L (lowercase), U (uppercase), N (numeric), S (symbol)",
321                    c
322                ));
323            }
324        }
325    }
326    Ok(result)
327}
328
329/// Generates a password from a pattern
330fn generate_password_from_pattern<R: Rng>(
331    char_set: &[u8],
332    pattern: &[PatternChar],
333    rng: &mut R,
334) -> String {
335    let mut pass = String::with_capacity(pattern.len());
336
337    let lowercase: Vec<u8> = (ASCII_LOWERCASE_START..=ASCII_LOWERCASE_END)
338        .filter(|&b| char_set.contains(&b))
339        .collect();
340    let uppercase: Vec<u8> = (ASCII_UPPERCASE_START..=ASCII_UPPERCASE_END)
341        .filter(|&b| char_set.contains(&b))
342        .collect();
343    let numeric: Vec<u8> = (ASCII_NUMERAL_START..=ASCII_NUMERAL_END)
344        .filter(|&b| char_set.contains(&b))
345        .collect();
346    let symbols: Vec<u8> = char_set
347        .iter()
348        .filter(|&&b| {
349            !(ASCII_LOWERCASE_START..=ASCII_LOWERCASE_END).contains(&b)
350                && !(ASCII_UPPERCASE_START..=ASCII_UPPERCASE_END).contains(&b)
351                && !(ASCII_NUMERAL_START..=ASCII_NUMERAL_END).contains(&b)
352        })
353        .copied()
354        .collect();
355
356    for &pat_char in pattern {
357        let char_byte = match pat_char {
358            PatternChar::Lowercase => {
359                if lowercase.is_empty() {
360                    char_set[rng.random_range(0..char_set.len())]
361                } else {
362                    lowercase[rng.random_range(0..lowercase.len())]
363                }
364            }
365            PatternChar::Uppercase => {
366                if uppercase.is_empty() {
367                    char_set[rng.random_range(0..char_set.len())]
368                } else {
369                    uppercase[rng.random_range(0..uppercase.len())]
370                }
371            }
372            PatternChar::Numeric => {
373                if numeric.is_empty() {
374                    char_set[rng.random_range(0..char_set.len())]
375                } else {
376                    numeric[rng.random_range(0..numeric.len())]
377                }
378            }
379            PatternChar::Symbol => {
380                if symbols.is_empty() {
381                    char_set[rng.random_range(0..char_set.len())]
382                } else {
383                    symbols[rng.random_range(0..symbols.len())]
384                }
385            }
386        };
387        pass.push(char_byte as char);
388    }
389
390    pass
391}
392
393/// Generates a single password ensuring minimum character type requirements
394fn generate_password_with_minimums<R: Rng>(
395    char_set: &[u8],
396    length: u32,
397    min_capitals: Option<u32>,
398    min_numerals: Option<u32>,
399    min_symbols: Option<u32>,
400    rng: &mut R,
401) -> String {
402    let mut pass_vec: Vec<char> = Vec::with_capacity(length as usize);
403
404    // First, ensure minimum requirements are met
405
406    // Collect character sets for each type
407    let capitals: Vec<u8> = (ASCII_UPPERCASE_START..=ASCII_UPPERCASE_END)
408        .filter(|&b| char_set.contains(&b))
409        .collect();
410    let numerals: Vec<u8> = (ASCII_NUMERAL_START..=ASCII_NUMERAL_END)
411        .filter(|&b| char_set.contains(&b))
412        .collect();
413    let symbols: Vec<u8> = char_set
414        .iter()
415        .filter(|&&b| {
416            !(ASCII_LOWERCASE_START..=ASCII_LOWERCASE_END).contains(&b)
417                && !(ASCII_UPPERCASE_START..=ASCII_UPPERCASE_END).contains(&b)
418                && !(ASCII_NUMERAL_START..=ASCII_NUMERAL_END).contains(&b)
419        })
420        .copied()
421        .collect();
422
423    // Add required capitals
424    if let Some(min) = min_capitals {
425        for _ in 0..min {
426            if !capitals.is_empty() {
427                let idx = rng.random_range(0..capitals.len());
428                pass_vec.push(capitals[idx] as char);
429            }
430        }
431    }
432
433    // Add required numerals
434    if let Some(min) = min_numerals {
435        for _ in 0..min {
436            if !numerals.is_empty() {
437                let idx = rng.random_range(0..numerals.len());
438                pass_vec.push(numerals[idx] as char);
439            }
440        }
441    }
442
443    // Add required symbols
444    if let Some(min) = min_symbols {
445        for _ in 0..min {
446            if !symbols.is_empty() {
447                let idx = rng.random_range(0..symbols.len());
448                pass_vec.push(symbols[idx] as char);
449            }
450        }
451    }
452
453    // Fill the rest randomly
454    while pass_vec.len() < length as usize {
455        let c_byte = char_set[rng.random_range(0..char_set.len())];
456        pass_vec.push(c_byte as char);
457    }
458
459    // Shuffle to randomize positions
460    use rand::seq::SliceRandom;
461    pass_vec.shuffle(rng);
462
463    pass_vec.into_iter().collect()
464}
465
466/// Generates passwords using the provided character set and RNG
467pub fn generate_passwords<R: Rng>(
468    char_set: &[u8],
469    params: &GenerationParams,
470    rng: &mut R,
471) -> Vec<String> {
472    let mut passwords = Vec::with_capacity(params.count as usize);
473
474    for _ in 0..params.count {
475        let pass = if let Some(ref pat) = params.pattern {
476            generate_password_from_pattern(char_set, pat, rng)
477        } else {
478            generate_password_with_minimums(
479                char_set,
480                params.length,
481                params.min_capitals,
482                params.min_numerals,
483                params.min_symbols,
484                rng,
485            )
486        };
487        passwords.push(pass);
488    }
489
490    passwords
491}
492
493/// Prints passwords in column format
494pub fn print_columns(passwords: Vec<String>, column_count: usize, show_header: bool) {
495    if show_header {
496        println!(
497            "Printing {} passwords in {} columns",
498            passwords.len(),
499            column_count
500        );
501    }
502
503    if column_count == 1 {
504        // Simple one-per-line output
505        for pass in passwords {
506            println!("{}", pass);
507        }
508        return;
509    }
510
511    // Calculate column width for alignment
512    let max_width = passwords.iter().map(|p| p.len()).max().unwrap_or(0).max(1);
513
514    let mut col = 0;
515    for pass in passwords {
516        print!("{:<width$}", pass, width = max_width);
517        col += 1;
518        if col == column_count {
519            col = 0;
520            println!();
521        } else {
522            print!(" ");
523        }
524    }
525    // Add trailing newline if last row is incomplete
526    if col != 0 {
527        println!();
528    }
529}
530
531#[cfg(test)]
532mod tests {
533    use super::*;
534
535    fn create_test_args(
536        capitals_off: bool,
537        numerals_off: bool,
538        symbols_off: bool,
539        exclude_chars: Vec<char>,
540    ) -> PasswordArgs {
541        PasswordArgs {
542            capitals_off,
543            numerals_off,
544            symbols_off,
545            exclude_chars,
546            include_chars: None,
547            min_capitals: None,
548            min_numerals: None,
549            min_symbols: None,
550            pattern: None,
551            length: 16,
552            password_count: 1,
553        }
554    }
555
556    #[test]
557    fn test_build_char_set_default() {
558        let args = create_test_args(false, false, false, vec![]);
559        let char_set = build_char_set(&args).unwrap();
560        // Should include lowercase, uppercase, numerals, and symbols
561        assert!(!char_set.is_empty());
562        assert!(char_set.len() > 60); // At least 26 + 26 + 10 + some symbols
563    }
564
565    #[test]
566    fn test_build_char_set_no_capitals() {
567        let args = create_test_args(true, false, false, vec![]);
568        let char_set = build_char_set(&args).unwrap();
569        // Should not include uppercase letters
570        assert!(!char_set.contains(&b'A'));
571        assert!(!char_set.contains(&b'Z'));
572        // Should still include lowercase
573        assert!(char_set.contains(&b'a'));
574    }
575
576    #[test]
577    fn test_build_char_set_no_numerals() {
578        let args = create_test_args(false, true, false, vec![]);
579        let char_set = build_char_set(&args).unwrap();
580        // Should not include numerals
581        assert!(!char_set.contains(&b'0'));
582        assert!(!char_set.contains(&b'9'));
583    }
584
585    #[test]
586    fn test_build_char_set_no_symbols() {
587        let args = create_test_args(false, false, true, vec![]);
588        let char_set = build_char_set(&args).unwrap();
589        // Should not include symbols
590        assert!(!char_set.contains(&b'!'));
591        assert!(!char_set.contains(&b'@'));
592    }
593
594    #[test]
595    fn test_build_char_set_with_exclusions() {
596        let args = create_test_args(false, false, false, vec!['a', 'b', 'c']);
597        let char_set = build_char_set(&args).unwrap();
598        // Should not include excluded characters
599        assert!(!char_set.contains(&b'a'));
600        assert!(!char_set.contains(&b'b'));
601        assert!(!char_set.contains(&b'c'));
602        // Should still include other lowercase
603        assert!(char_set.contains(&b'd'));
604    }
605
606    #[test]
607    fn test_build_char_set_all_excluded() {
608        // Exclude all lowercase letters when only lowercase is available
609        let mut exclude_all = Vec::new();
610        for c in b'a'..=b'z' {
611            exclude_all.push(c as char);
612        }
613        let args = create_test_args(true, true, true, exclude_all);
614        let result = build_char_set(&args);
615        assert!(result.is_err());
616        assert!(matches!(
617            result.unwrap_err(),
618            PasswordError::EmptyCharacterSet
619        ));
620    }
621
622    #[test]
623    fn test_validate_args_valid() {
624        let args = create_test_args(false, false, false, vec![]);
625        assert!(validate_args(&args).is_ok());
626    }
627
628    #[test]
629    fn test_validate_args_invalid_length() {
630        let mut args = create_test_args(false, false, false, vec![]);
631        args.length = 0;
632        let result = validate_args(&args);
633        assert!(result.is_err());
634        assert!(matches!(result.unwrap_err(), PasswordError::InvalidLength));
635    }
636
637    #[test]
638    fn test_validate_args_invalid_count() {
639        let mut args = create_test_args(false, false, false, vec![]);
640        args.password_count = 0;
641        let result = validate_args(&args);
642        assert!(result.is_err());
643        assert!(matches!(result.unwrap_err(), PasswordError::InvalidCount));
644    }
645
646    #[test]
647    fn test_column_count() {
648        assert_eq!(column_count(1), 1);
649        assert_eq!(column_count(2), 1);
650        assert_eq!(column_count(3), 1);
651        assert_eq!(column_count(4), 2);
652        assert_eq!(column_count(5), 2);
653        assert_eq!(column_count(6), 2);
654        assert_eq!(column_count(9), 3);
655        assert_eq!(column_count(10), 3);
656        assert_eq!(column_count(16), 4);
657        assert_eq!(column_count(20), 4);
658        assert_eq!(column_count(25), 5);
659    }
660
661    #[test]
662    fn test_column_count_large() {
663        // Test that large numbers default to reasonable values
664        let cols = column_count(100);
665        assert!(cols >= 2 && cols <= 5);
666    }
667
668    #[test]
669    fn test_parse_exclude_chars_range() {
670        let result = parse_exclude_chars(vec!["a-z".to_string()]).unwrap();
671        assert_eq!(result.len(), 26);
672        assert!(result.contains(&'a'));
673        assert!(result.contains(&'z'));
674        assert!(result.contains(&'m'));
675    }
676
677    #[test]
678    fn test_parse_exclude_chars_numeric_range() {
679        let result = parse_exclude_chars(vec!["0-9".to_string()]).unwrap();
680        assert_eq!(result.len(), 10);
681        assert!(result.contains(&'0'));
682        assert!(result.contains(&'9'));
683        assert!(result.contains(&'5'));
684    }
685
686    #[test]
687    fn test_parse_exclude_chars_small_range() {
688        let result = parse_exclude_chars(vec!["a-c".to_string()]).unwrap();
689        assert_eq!(result.len(), 3);
690        assert!(result.contains(&'a'));
691        assert!(result.contains(&'b'));
692        assert!(result.contains(&'c'));
693    }
694
695    #[test]
696    fn test_parse_exclude_chars_individual() {
697        let result = parse_exclude_chars(vec!["abc".to_string()]).unwrap();
698        assert_eq!(result.len(), 3);
699        assert!(result.contains(&'a'));
700        assert!(result.contains(&'b'));
701        assert!(result.contains(&'c'));
702    }
703
704    #[test]
705    fn test_parse_exclude_chars_mixed() {
706        let result =
707            parse_exclude_chars(vec!["a-c".to_string(), "x".to_string(), "0-2".to_string()])
708                .unwrap();
709        assert!(result.contains(&'a'));
710        assert!(result.contains(&'b'));
711        assert!(result.contains(&'c'));
712        assert!(result.contains(&'x'));
713        assert!(result.contains(&'0'));
714        assert!(result.contains(&'1'));
715        assert!(result.contains(&'2'));
716    }
717
718    #[test]
719    fn test_parse_exclude_chars_invalid_range() {
720        let result = parse_exclude_chars(vec!["z-a".to_string()]);
721        assert!(result.is_err());
722        assert!(result.unwrap_err().contains("Invalid range"));
723    }
724
725    #[test]
726    fn test_calculate_entropy() {
727        // Test with different character set sizes and lengths
728        let entropy1 = calculate_entropy(26, 8); // lowercase only, 8 chars
729        assert!(entropy1 > 0.0);
730
731        let entropy2 = calculate_entropy(62, 16); // alphanumeric, 16 chars
732        assert!(entropy2 > entropy1);
733
734        let entropy3 = calculate_entropy(94, 20); // all printable ASCII, 20 chars
735        assert!(entropy3 > entropy2);
736
737        // Verify entropy increases with length
738        let entropy4 = calculate_entropy(62, 32);
739        assert!(entropy4 > entropy2);
740    }
741
742    #[test]
743    fn test_password_error_display() {
744        let err1 = PasswordError::InvalidLength;
745        assert!(
746            err1.to_string()
747                .contains("Password length must be greater than 0")
748        );
749
750        let err2 = PasswordError::InvalidLengthTooLong;
751        assert!(err2.to_string().contains("exceeds maximum of 10,000"));
752
753        let err3 = PasswordError::InvalidCount;
754        assert!(
755            err3.to_string()
756                .contains("Password count must be greater than 0")
757        );
758
759        let err4 = PasswordError::EmptyCharacterSet;
760        assert!(
761            err4.to_string()
762                .contains("All characters have been excluded")
763        );
764        assert!(err4.to_string().contains("Hint"));
765
766        let err5 = PasswordError::AllTypesDisabled;
767        assert!(
768            err5.to_string()
769                .contains("All character types are disabled")
770        );
771        assert!(err5.to_string().contains("Hint"));
772    }
773
774    #[test]
775    fn test_build_char_set_with_include_chars() {
776        let mut args = create_test_args(false, false, false, vec![]);
777        args.include_chars = Some(vec!['a', 'b', 'c', '1', '2', '!']);
778        let char_set = build_char_set(&args).unwrap();
779
780        assert_eq!(char_set.len(), 6);
781        assert!(char_set.contains(&b'a'));
782        assert!(char_set.contains(&b'b'));
783        assert!(char_set.contains(&b'c'));
784        assert!(char_set.contains(&b'1'));
785        assert!(char_set.contains(&b'2'));
786        assert!(char_set.contains(&b'!'));
787        // Should not include other characters
788        assert!(!char_set.contains(&b'd'));
789        assert!(!char_set.contains(&b'A'));
790    }
791
792    #[test]
793    fn test_build_char_set_with_include_chars_and_exclusions() {
794        let mut args = create_test_args(false, false, false, vec!['a']);
795        args.include_chars = Some(vec!['a', 'b', 'c']);
796        let char_set = build_char_set(&args).unwrap();
797
798        // 'a' should be excluded even though it's in include_chars
799        assert!(!char_set.contains(&b'a'));
800        assert!(char_set.contains(&b'b'));
801        assert!(char_set.contains(&b'c'));
802    }
803
804    #[test]
805    fn test_validate_args_too_long() {
806        let mut args = create_test_args(false, false, false, vec![]);
807        args.length = 10_001;
808        let result = validate_args(&args);
809        assert!(result.is_err());
810        assert!(matches!(
811            result.unwrap_err(),
812            PasswordError::InvalidLengthTooLong
813        ));
814    }
815
816    #[test]
817    fn test_validate_args_all_types_disabled_with_exclusions() {
818        // All types disabled and all lowercase excluded
819        // This should result in EmptyCharacterSet from build_char_set, which gets propagated
820        let mut exclude_all = Vec::new();
821        for c in b'a'..=b'z' {
822            exclude_all.push(c as char);
823        }
824        let args = create_test_args(true, true, true, exclude_all);
825        let result = validate_args(&args);
826        assert!(result.is_err());
827        // When all types are disabled and all chars excluded, build_char_set returns EmptyCharacterSet
828        // which gets propagated through the ? operator
829        let err = result.unwrap_err();
830        assert!(
831            matches!(err, PasswordError::EmptyCharacterSet)
832                || matches!(err, PasswordError::AllTypesDisabled)
833        );
834    }
835
836    #[test]
837    fn test_column_count_multiples() {
838        // Test multiples of 5
839        assert_eq!(column_count(25), 5);
840        assert_eq!(column_count(30), 5);
841        assert_eq!(column_count(35), 5);
842
843        // Test multiples of 4 (but not 5)
844        assert_eq!(column_count(28), 4);
845        assert_eq!(column_count(32), 4);
846
847        // Test multiples of 3 (but not 4 or 5)
848        assert_eq!(column_count(27), 3);
849        assert_eq!(column_count(33), 3);
850
851        // Test multiples of 2 (but not 3, 4, or 5)
852        assert_eq!(column_count(26), 2);
853        assert_eq!(column_count(34), 2);
854
855        // Test prime numbers (should default to 3)
856        assert_eq!(column_count(29), 3);
857        assert_eq!(column_count(31), 3);
858    }
859
860    #[test]
861    fn test_parse_pattern() {
862        // Test valid patterns
863        let pattern1 = parse_pattern("LLL").unwrap();
864        assert_eq!(pattern1.len(), 3);
865        assert!(matches!(pattern1[0], PatternChar::Lowercase));
866        assert!(matches!(pattern1[1], PatternChar::Lowercase));
867        assert!(matches!(pattern1[2], PatternChar::Lowercase));
868
869        let pattern2 = parse_pattern("UUNNSS").unwrap();
870        assert_eq!(pattern2.len(), 6);
871        assert!(matches!(pattern2[0], PatternChar::Uppercase));
872        assert!(matches!(pattern2[1], PatternChar::Uppercase));
873        assert!(matches!(pattern2[2], PatternChar::Numeric));
874        assert!(matches!(pattern2[3], PatternChar::Numeric));
875        assert!(matches!(pattern2[4], PatternChar::Symbol));
876        assert!(matches!(pattern2[5], PatternChar::Symbol));
877
878        // Test case insensitivity
879        let pattern3 = parse_pattern("lluunnss").unwrap();
880        assert_eq!(pattern3.len(), 8);
881        assert!(matches!(pattern3[0], PatternChar::Lowercase));
882        assert!(matches!(pattern3[2], PatternChar::Uppercase));
883        assert!(matches!(pattern3[4], PatternChar::Numeric));
884        assert!(matches!(pattern3[6], PatternChar::Symbol));
885
886        // Test invalid pattern
887        let result = parse_pattern("LLX");
888        assert!(result.is_err());
889        assert!(result.unwrap_err().contains("Invalid pattern character"));
890
891        // Test empty pattern
892        let pattern4 = parse_pattern("").unwrap();
893        assert_eq!(pattern4.len(), 0);
894    }
895
896    #[test]
897    fn test_generate_password_from_pattern() {
898        use rand::{SeedableRng, rngs::StdRng};
899
900        let char_set = vec![
901            b'a', b'b', b'c', b'A', b'B', b'C', b'0', b'1', b'2', b'!', b'@', b'#',
902        ];
903        let pattern = vec![
904            PatternChar::Lowercase,
905            PatternChar::Uppercase,
906            PatternChar::Numeric,
907            PatternChar::Symbol,
908        ];
909
910        let mut rng = StdRng::seed_from_u64(42);
911        let password = generate_password_from_pattern(&char_set, &pattern, &mut rng);
912
913        assert_eq!(password.len(), 4);
914        // Verify each character type (we can't predict exact chars due to randomness,
915        // but we can verify the pattern was followed by checking character classes)
916        let chars: Vec<char> = password.chars().collect();
917        assert!(chars[0].is_ascii_lowercase());
918        assert!(chars[1].is_ascii_uppercase());
919        assert!(chars[2].is_ascii_digit());
920        assert!(!chars[3].is_alphanumeric());
921    }
922
923    #[test]
924    fn test_generate_password_from_pattern_empty_sets() {
925        use rand::{SeedableRng, rngs::StdRng};
926
927        // Character set with no uppercase, no numeric, no symbols
928        let char_set = vec![b'a', b'b', b'c'];
929        let pattern = vec![
930            PatternChar::Lowercase,
931            PatternChar::Uppercase, // Will fallback to char_set
932            PatternChar::Numeric,   // Will fallback to char_set
933            PatternChar::Symbol,    // Will fallback to char_set
934        ];
935
936        let mut rng = StdRng::seed_from_u64(123);
937        let password = generate_password_from_pattern(&char_set, &pattern, &mut rng);
938
939        assert_eq!(password.len(), 4);
940        // All should be lowercase since that's all that's available
941        for c in password.chars() {
942            assert!(c.is_ascii_lowercase());
943        }
944    }
945
946    #[test]
947    fn test_generate_password_from_pattern_empty_lowercase() {
948        use rand::{SeedableRng, rngs::StdRng};
949
950        // Character set with no lowercase (only uppercase, numeric, symbols)
951        let char_set = vec![b'A', b'B', b'0', b'1', b'!', b'@'];
952        let pattern = vec![PatternChar::Lowercase]; // Will fallback to char_set
953
954        let mut rng = StdRng::seed_from_u64(456);
955        let password = generate_password_from_pattern(&char_set, &pattern, &mut rng);
956
957        assert_eq!(password.len(), 1);
958        // Should fallback to any character from char_set
959        assert!(char_set.contains(&(password.chars().next().unwrap() as u8)));
960    }
961
962    #[test]
963    fn test_generate_password_from_pattern_empty_uppercase() {
964        use rand::{SeedableRng, rngs::StdRng};
965
966        // Character set with no uppercase
967        let char_set = vec![b'a', b'b', b'0', b'1', b'!', b'@'];
968        let pattern = vec![PatternChar::Uppercase]; // Will fallback to char_set
969
970        let mut rng = StdRng::seed_from_u64(789);
971        let password = generate_password_from_pattern(&char_set, &pattern, &mut rng);
972
973        assert_eq!(password.len(), 1);
974        assert!(char_set.contains(&(password.chars().next().unwrap() as u8)));
975    }
976
977    #[test]
978    fn test_generate_password_from_pattern_empty_numeric() {
979        use rand::{SeedableRng, rngs::StdRng};
980
981        // Character set with no numeric
982        let char_set = vec![b'a', b'b', b'A', b'B', b'!', b'@'];
983        let pattern = vec![PatternChar::Numeric]; // Will fallback to char_set
984
985        let mut rng = StdRng::seed_from_u64(1011);
986        let password = generate_password_from_pattern(&char_set, &pattern, &mut rng);
987
988        assert_eq!(password.len(), 1);
989        assert!(char_set.contains(&(password.chars().next().unwrap() as u8)));
990    }
991
992    #[test]
993    fn test_generate_password_from_pattern_empty_symbols() {
994        use rand::{SeedableRng, rngs::StdRng};
995
996        // Character set with no symbols
997        let char_set = vec![b'a', b'b', b'A', b'B', b'0', b'1'];
998        let pattern = vec![PatternChar::Symbol]; // Will fallback to char_set
999
1000        let mut rng = StdRng::seed_from_u64(1213);
1001        let password = generate_password_from_pattern(&char_set, &pattern, &mut rng);
1002
1003        assert_eq!(password.len(), 1);
1004        assert!(char_set.contains(&(password.chars().next().unwrap() as u8)));
1005    }
1006
1007    #[test]
1008    fn test_generate_password_with_minimums() {
1009        use rand::{SeedableRng, rngs::StdRng};
1010
1011        let char_set = vec![
1012            b'a', b'b', b'c', // lowercase
1013            b'A', b'B', b'C', // uppercase
1014            b'0', b'1', b'2', // numeric
1015            b'!', b'@', b'#', // symbols
1016        ];
1017
1018        let mut rng = StdRng::seed_from_u64(456);
1019        let password =
1020            generate_password_with_minimums(&char_set, 10, Some(2), Some(2), Some(2), &mut rng);
1021
1022        assert_eq!(password.len(), 10);
1023
1024        // Count character types
1025        let mut capitals = 0;
1026        let mut numerals = 0;
1027        let mut symbols = 0;
1028
1029        for c in password.chars() {
1030            if c.is_ascii_uppercase() {
1031                capitals += 1;
1032            } else if c.is_ascii_digit() {
1033                numerals += 1;
1034            } else if !c.is_alphanumeric() {
1035                symbols += 1;
1036            }
1037        }
1038
1039        assert!(capitals >= 2);
1040        assert!(numerals >= 2);
1041        assert!(symbols >= 2);
1042    }
1043
1044    #[test]
1045    fn test_generate_password_with_minimums_empty_sets() {
1046        use rand::{SeedableRng, rngs::StdRng};
1047
1048        // Only lowercase available
1049        let char_set = vec![b'a', b'b', b'c'];
1050
1051        let mut rng = StdRng::seed_from_u64(789);
1052        let password =
1053            generate_password_with_minimums(&char_set, 5, Some(2), Some(2), Some(2), &mut rng);
1054
1055        assert_eq!(password.len(), 5);
1056        // All should be lowercase since that's all available
1057        for c in password.chars() {
1058            assert!(c.is_ascii_lowercase());
1059        }
1060    }
1061
1062    #[test]
1063    fn test_generate_password_with_minimums_no_minimums() {
1064        use rand::{SeedableRng, rngs::StdRng};
1065
1066        let char_set = vec![b'a', b'b', b'c', b'A', b'B', b'0', b'1', b'!', b'@'];
1067
1068        let mut rng = StdRng::seed_from_u64(101);
1069        let password = generate_password_with_minimums(&char_set, 8, None, None, None, &mut rng);
1070
1071        assert_eq!(password.len(), 8);
1072    }
1073
1074    #[test]
1075    fn test_generate_passwords() {
1076        use rand::{SeedableRng, rngs::StdRng};
1077
1078        let char_set = vec![b'a', b'b', b'c', b'1', b'2', b'3'];
1079        let params = GenerationParams {
1080            length: 5,
1081            count: 3,
1082            min_capitals: None,
1083            min_numerals: None,
1084            min_symbols: None,
1085            pattern: None,
1086        };
1087
1088        let mut rng = StdRng::seed_from_u64(202);
1089        let passwords = generate_passwords(&char_set, &params, &mut rng);
1090
1091        assert_eq!(passwords.len(), 3);
1092        for pass in &passwords {
1093            assert_eq!(pass.len(), 5);
1094        }
1095    }
1096
1097    #[test]
1098    fn test_generate_passwords_with_pattern() {
1099        use rand::{SeedableRng, rngs::StdRng};
1100
1101        let char_set = vec![b'a', b'b', b'A', b'B', b'0', b'1', b'!', b'@'];
1102        let pattern = vec![
1103            PatternChar::Lowercase,
1104            PatternChar::Uppercase,
1105            PatternChar::Numeric,
1106            PatternChar::Symbol,
1107        ];
1108        let params = GenerationParams {
1109            length: 4,
1110            count: 2,
1111            min_capitals: None,
1112            min_numerals: None,
1113            min_symbols: None,
1114            pattern: Some(pattern),
1115        };
1116
1117        let mut rng = StdRng::seed_from_u64(303);
1118        let passwords = generate_passwords(&char_set, &params, &mut rng);
1119
1120        assert_eq!(passwords.len(), 2);
1121        for pass in &passwords {
1122            assert_eq!(pass.len(), 4);
1123        }
1124    }
1125
1126    #[test]
1127    fn test_generate_passwords_with_minimums() {
1128        use rand::{SeedableRng, rngs::StdRng};
1129
1130        let char_set = vec![
1131            b'a', b'b', b'c', b'A', b'B', b'C', b'0', b'1', b'2', b'!', b'@', b'#',
1132        ];
1133        let params = GenerationParams {
1134            length: 8,
1135            count: 2,
1136            min_capitals: Some(1),
1137            min_numerals: Some(1),
1138            min_symbols: Some(1),
1139            pattern: None,
1140        };
1141
1142        let mut rng = StdRng::seed_from_u64(404);
1143        let passwords = generate_passwords(&char_set, &params, &mut rng);
1144
1145        assert_eq!(passwords.len(), 2);
1146        for pass in &passwords {
1147            assert_eq!(pass.len(), 8);
1148            // Verify minimums are met
1149            let mut has_capital = false;
1150            let mut has_numeral = false;
1151            let mut has_symbol = false;
1152            for c in pass.chars() {
1153                if c.is_ascii_uppercase() {
1154                    has_capital = true;
1155                } else if c.is_ascii_digit() {
1156                    has_numeral = true;
1157                } else if !c.is_alphanumeric() {
1158                    has_symbol = true;
1159                }
1160            }
1161            assert!(has_capital);
1162            assert!(has_numeral);
1163            assert!(has_symbol);
1164        }
1165    }
1166
1167    #[test]
1168    fn test_print_columns_single_column() {
1169        let passwords = vec![
1170            "pass1".to_string(),
1171            "pass2".to_string(),
1172            "pass3".to_string(),
1173        ];
1174        // We can't easily test print! without capturing stdout, so we'll test the logic
1175        // by verifying the function doesn't panic
1176        print_columns(passwords.clone(), 1, false);
1177        print_columns(passwords, 1, true);
1178    }
1179
1180    #[test]
1181    fn test_print_columns_multiple_columns() {
1182        let passwords: Vec<String> = vec![
1183            "short".to_string(),
1184            "verylongpassword".to_string(),
1185            "medium".to_string(),
1186            "x".to_string(),
1187        ];
1188        // Test that it doesn't panic
1189        print_columns(passwords.clone(), 2, false);
1190        print_columns(passwords.clone(), 2, true);
1191        print_columns(passwords.clone(), 3, false);
1192        print_columns(passwords, 4, false);
1193    }
1194
1195    #[test]
1196    fn test_print_columns_incomplete_row() {
1197        let passwords = vec![
1198            "a".to_string(),
1199            "b".to_string(),
1200            "c".to_string(),
1201            "d".to_string(),
1202            "e".to_string(), // 5 passwords, 3 columns = incomplete last row
1203        ];
1204        // Should not panic with incomplete row
1205        print_columns(passwords, 3, false);
1206    }
1207
1208    #[test]
1209    fn test_print_columns_empty() {
1210        let passwords: Vec<String> = vec![];
1211        print_columns(passwords.clone(), 1, false);
1212        print_columns(passwords, 3, true);
1213    }
1214
1215    #[test]
1216    fn test_print_columns_single_password() {
1217        let passwords = vec!["password123".to_string()];
1218        print_columns(passwords.clone(), 1, false);
1219        print_columns(passwords, 3, false);
1220    }
1221
1222    #[test]
1223    fn test_parse_exclude_chars_non_printable_start() {
1224        // Test range with start character < 32 (non-printable)
1225        // This should skip the range logic and treat as individual chars
1226        let result = parse_exclude_chars(vec!["\x1f-9".to_string()]);
1227        // Should succeed but treat as individual characters, not a range
1228        assert!(result.is_ok());
1229        let chars = result.unwrap();
1230        // Should contain the characters from the string, not a range expansion
1231        assert!(chars.len() >= 2); // At least \x1f, -, and 9
1232    }
1233
1234    #[test]
1235    fn test_parse_exclude_chars_non_printable_end() {
1236        // Test range with end character >= 127 (non-printable)
1237        // This should skip the range logic
1238        let result = parse_exclude_chars(vec!["a-\x7f".to_string()]);
1239        // Should succeed but treat as individual characters
1240        assert!(result.is_ok());
1241        // Should not expand as a range
1242        let chars = result.unwrap();
1243        // The range logic should be skipped due to end >= 127
1244        // So it should treat as individual characters
1245        assert!(chars.contains(&'a'));
1246    }
1247
1248    #[test]
1249    fn test_parse_exclude_chars_duplicate_handling() {
1250        // Test that duplicates are properly handled
1251        let result = parse_exclude_chars(vec!["a".to_string(), "a".to_string(), "b".to_string()]);
1252        assert!(result.is_ok());
1253        let chars = result.unwrap();
1254        // Should only contain one 'a' and one 'b'
1255        assert_eq!(chars.iter().filter(|&&c| c == 'a').count(), 1);
1256        assert_eq!(chars.iter().filter(|&&c| c == 'b').count(), 1);
1257    }
1258
1259    #[test]
1260    fn test_parse_exclude_chars_duplicate_in_range_and_individual() {
1261        // Test that characters in ranges are not duplicated when also specified individually
1262        let result = parse_exclude_chars(vec!["a-c".to_string(), "b".to_string()]);
1263        assert!(result.is_ok());
1264        let chars = result.unwrap();
1265        // Should contain a, b, c each once
1266        assert_eq!(chars.iter().filter(|&&c| c == 'a').count(), 1);
1267        assert_eq!(chars.iter().filter(|&&c| c == 'b').count(), 1);
1268        assert_eq!(chars.iter().filter(|&&c| c == 'c').count(), 1);
1269    }
1270
1271    #[test]
1272    fn test_parse_exclude_chars_boundary_conditions() {
1273        // Test range exactly at printable ASCII boundaries
1274        // Space (32) to ~ (126) should work
1275        let result = parse_exclude_chars(vec![" -~".to_string()]);
1276        assert!(result.is_ok());
1277        let chars = result.unwrap();
1278        // Should expand to all printable ASCII
1279        assert!(chars.contains(&' '));
1280        assert!(chars.contains(&'~'));
1281    }
1282
1283    #[test]
1284    fn test_build_char_set_all_types_disabled_lowercase_available() {
1285        // Test with all character types disabled, but lowercase still available
1286        let args = create_test_args(true, true, true, vec![]);
1287        let char_set = build_char_set(&args).unwrap();
1288        // Should only contain lowercase letters
1289        assert!(!char_set.is_empty());
1290        assert!(char_set.contains(&b'a'));
1291        assert!(char_set.contains(&b'z'));
1292        assert!(!char_set.contains(&b'A'));
1293        assert!(!char_set.contains(&b'0'));
1294        assert!(!char_set.contains(&b'!'));
1295    }
1296
1297    #[test]
1298    fn test_build_char_set_include_chars_empty_after_exclude() {
1299        // Test include_chars where exclude_chars removes all characters
1300        let mut args = create_test_args(false, false, false, vec!['a', 'b', 'c']);
1301        args.include_chars = Some(vec!['a', 'b', 'c']);
1302        let result = build_char_set(&args);
1303        assert!(result.is_err());
1304        assert!(matches!(
1305            result.unwrap_err(),
1306            PasswordError::EmptyCharacterSet
1307        ));
1308    }
1309
1310    #[test]
1311    fn test_build_char_set_include_chars_with_exclusions_partial() {
1312        // Test include_chars with exclude_chars that removes some but not all
1313        let mut args = create_test_args(false, false, false, vec!['a']);
1314        args.include_chars = Some(vec!['a', 'b', 'c', 'd', 'e']);
1315        let char_set = build_char_set(&args).unwrap();
1316        assert_eq!(char_set.len(), 4); // b, c, d, e
1317        assert!(!char_set.contains(&b'a'));
1318        assert!(char_set.contains(&b'b'));
1319        assert!(char_set.contains(&b'c'));
1320        assert!(char_set.contains(&b'd'));
1321        assert!(char_set.contains(&b'e'));
1322    }
1323
1324    #[test]
1325    fn test_password_error_display_all_variants() {
1326        // Ensure all error variants are tested for Display implementation
1327        let err = PasswordError::InvalidLength;
1328        let msg = err.to_string();
1329        assert!(msg.contains("Password length must be greater than 0"));
1330
1331        let err = PasswordError::InvalidLengthTooLong;
1332        let msg = err.to_string();
1333        assert!(msg.contains("exceeds maximum of 10,000"));
1334
1335        let err = PasswordError::InvalidCount;
1336        let msg = err.to_string();
1337        assert!(msg.contains("Password count must be greater than 0"));
1338
1339        let err = PasswordError::EmptyCharacterSet;
1340        let msg = err.to_string();
1341        assert!(msg.contains("All characters have been excluded"));
1342        assert!(msg.contains("Hint"));
1343
1344        let err = PasswordError::AllTypesDisabled;
1345        let msg = err.to_string();
1346        assert!(msg.contains("All character types are disabled"));
1347        assert!(msg.contains("Hint"));
1348    }
1349
1350    #[test]
1351    fn test_password_error_source() {
1352        // Test that PasswordError implements std::error::Error
1353        let err = PasswordError::InvalidLength;
1354        // Should be able to use as Error trait object
1355        let _err_ref: &dyn std::error::Error = &err;
1356    }
1357
1358    #[test]
1359    fn test_generate_password_with_minimums_exceeding_length() {
1360        use rand::{SeedableRng, rngs::StdRng};
1361
1362        let char_set = vec![b'a', b'b', b'A', b'B', b'0', b'1', b'!', b'@'];
1363        // Request 5 minimums but length is only 4
1364        // Minimums take precedence, so password will be length 5
1365        let mut rng = StdRng::seed_from_u64(1001);
1366        let password = generate_password_with_minimums(&char_set, 4, Some(5), None, None, &mut rng);
1367
1368        // Should generate a password with at least 5 capitals (minimum takes precedence)
1369        assert!(password.len() >= 5);
1370        let capitals = password.chars().filter(|c| c.is_ascii_uppercase()).count();
1371        assert!(capitals >= 5); // Minimum requirement is met
1372    }
1373
1374    #[test]
1375    fn test_generate_password_with_minimums_sum_exceeds_length() {
1376        use rand::{SeedableRng, rngs::StdRng};
1377
1378        let char_set = vec![
1379            b'a', b'b', b'c', b'A', b'B', b'C', b'0', b'1', b'2', b'!', b'@', b'#',
1380        ];
1381        // Request min_capitals=3, min_numerals=3, min_symbols=3, but length=6
1382        // Minimums take precedence, so password will be at least length 9
1383        let mut rng = StdRng::seed_from_u64(1002);
1384        let password =
1385            generate_password_with_minimums(&char_set, 6, Some(3), Some(3), Some(3), &mut rng);
1386
1387        // Password length should be at least 9 (sum of minimums)
1388        // May be more if minimums are applied then filled up to length
1389        assert!(password.len() >= 9);
1390        let capitals = password.chars().filter(|c| c.is_ascii_uppercase()).count();
1391        let numerals = password.chars().filter(|c| c.is_ascii_digit()).count();
1392        let symbols = password.chars().filter(|c| !c.is_alphanumeric()).count();
1393
1394        // Should meet all minimum requirements
1395        assert!(capitals >= 3);
1396        assert!(numerals >= 3);
1397        assert!(symbols >= 3);
1398    }
1399
1400    #[test]
1401    fn test_generate_password_with_minimums_exact_length() {
1402        use rand::{SeedableRng, rngs::StdRng};
1403
1404        let char_set = vec![b'a', b'b', b'A', b'B', b'0', b'1', b'!', b'@'];
1405        // Request min_capitals=2, min_numerals=2, length=4
1406        let mut rng = StdRng::seed_from_u64(1003);
1407        let password =
1408            generate_password_with_minimums(&char_set, 4, Some(2), Some(2), None, &mut rng);
1409
1410        assert_eq!(password.len(), 4);
1411        let capitals = password.chars().filter(|c| c.is_ascii_uppercase()).count();
1412        let numerals = password.chars().filter(|c| c.is_ascii_digit()).count();
1413
1414        assert!(capitals >= 2);
1415        assert!(numerals >= 2);
1416    }
1417
1418    #[test]
1419    fn test_generate_password_from_pattern_all_same_type() {
1420        use rand::{SeedableRng, rngs::StdRng};
1421
1422        let char_set = vec![b'a', b'b', b'c', b'A', b'B', b'C', b'0', b'1', b'2'];
1423        let pattern = vec![
1424            PatternChar::Lowercase,
1425            PatternChar::Lowercase,
1426            PatternChar::Lowercase,
1427        ];
1428
1429        let mut rng = StdRng::seed_from_u64(2001);
1430        let password = generate_password_from_pattern(&char_set, &pattern, &mut rng);
1431
1432        assert_eq!(password.len(), 3);
1433        for c in password.chars() {
1434            assert!(c.is_ascii_lowercase());
1435        }
1436    }
1437
1438    #[test]
1439    fn test_generate_password_from_pattern_very_long() {
1440        use rand::{SeedableRng, rngs::StdRng};
1441
1442        let char_set = vec![b'a', b'b', b'A', b'B', b'0', b'1', b'!', b'@'];
1443        // Create a pattern of length 100
1444        let pattern: Vec<PatternChar> = (0..100)
1445            .map(|i| match i % 4 {
1446                0 => PatternChar::Lowercase,
1447                1 => PatternChar::Uppercase,
1448                2 => PatternChar::Numeric,
1449                _ => PatternChar::Symbol,
1450            })
1451            .collect();
1452
1453        let mut rng = StdRng::seed_from_u64(2002);
1454        let password = generate_password_from_pattern(&char_set, &pattern, &mut rng);
1455
1456        assert_eq!(password.len(), 100);
1457    }
1458
1459    #[test]
1460    fn test_print_columns_very_long_passwords() {
1461        let passwords = vec!["a".repeat(100), "b".repeat(50), "c".repeat(150)];
1462        // Test width calculation with very long passwords
1463        print_columns(passwords.clone(), 1, false);
1464        print_columns(passwords.clone(), 2, false);
1465        print_columns(passwords, 3, true);
1466    }
1467
1468    #[test]
1469    fn test_print_columns_single_password_multi_column() {
1470        // Test single password with multiple columns (should still print it)
1471        let passwords = vec!["single".to_string()];
1472        print_columns(passwords, 5, false);
1473    }
1474
1475    #[test]
1476    fn test_validate_args_all_types_disabled_lowercase_available() {
1477        // Test validate_args when all types are disabled but lowercase available
1478        let args = create_test_args(true, true, true, vec![]);
1479        let result = validate_args(&args);
1480        assert!(result.is_ok()); // Should be valid since lowercase is still available
1481    }
1482}