1use rand::Rng;
47use std::collections::HashSet;
48use std::fmt;
49
50pub fn calculate_entropy(char_set_size: usize, length: u32) -> f64 {
52 (char_set_size as f64).log2() * length as f64
53}
54
55#[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
100const 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; const ASCII_SYMBOL_RANGE_1_END: u8 = 47; const ASCII_SYMBOL_RANGE_2_START: u8 = 58; const ASCII_SYMBOL_RANGE_2_END: u8 = 64; const ASCII_SYMBOL_RANGE_3_START: u8 = 91; const ASCII_SYMBOL_RANGE_3_END: u8 = 96; const ASCII_SYMBOL_RANGE_4_START: u8 = 123; const ASCII_SYMBOL_RANGE_4_END: u8 = 126; #[derive(Debug, Clone, Copy)]
118pub enum PatternChar {
119 Lowercase,
120 Uppercase,
121 Numeric,
122 Symbol,
123}
124
125#[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
136pub 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
151pub 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 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 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 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
198pub fn build_char_set(args: &PasswordArgs) -> Result<Vec<u8>, PasswordError> {
201 let mut chars = Vec::new();
202
203 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 let estimated_capacity = if args.symbols_off {
211 62 } else {
213 94 };
215 chars.reserve(estimated_capacity);
216
217 chars.extend(ASCII_LOWERCASE_START..=ASCII_LOWERCASE_END);
219
220 if !args.capitals_off {
222 chars.extend(ASCII_UPPERCASE_START..=ASCII_UPPERCASE_END);
223 }
224
225 if !args.numerals_off {
227 chars.extend(ASCII_NUMERAL_START..=ASCII_NUMERAL_END);
228 }
229
230 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 let exclude_set: HashSet<char> = args.exclude_chars.iter().cloned().collect();
241
242 chars.retain(|&b| !exclude_set.contains(&(b as char)));
244
245 if chars.is_empty() {
247 return Err(PasswordError::EmptyCharacterSet);
248 }
249
250 Ok(chars)
251}
252
253const MAX_PASSWORD_LENGTH: u32 = 10_000;
255
256pub 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 if args.capitals_off && args.numerals_off && args.symbols_off {
272 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
283pub fn column_count(password_count: u32) -> usize {
285 match password_count {
288 1..=3 => 1,
289 4..=8 => 2,
290 9..=15 => 3,
291 16..=24 => 4,
292 _ => {
293 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 }
305 }
306 }
307}
308
309pub 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
329fn 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
393fn 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 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 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 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 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 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 use rand::seq::SliceRandom;
461 pass_vec.shuffle(rng);
462
463 pass_vec.into_iter().collect()
464}
465
466pub 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
493pub 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 for pass in passwords {
506 println!("{}", pass);
507 }
508 return;
509 }
510
511 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 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 assert!(!char_set.is_empty());
562 assert!(char_set.len() > 60); }
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 assert!(!char_set.contains(&b'A'));
571 assert!(!char_set.contains(&b'Z'));
572 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 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 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 assert!(!char_set.contains(&b'a'));
600 assert!(!char_set.contains(&b'b'));
601 assert!(!char_set.contains(&b'c'));
602 assert!(char_set.contains(&b'd'));
604 }
605
606 #[test]
607 fn test_build_char_set_all_excluded() {
608 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 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 let entropy1 = calculate_entropy(26, 8); assert!(entropy1 > 0.0);
730
731 let entropy2 = calculate_entropy(62, 16); assert!(entropy2 > entropy1);
733
734 let entropy3 = calculate_entropy(94, 20); assert!(entropy3 > entropy2);
736
737 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 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 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 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 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 assert_eq!(column_count(25), 5);
840 assert_eq!(column_count(30), 5);
841 assert_eq!(column_count(35), 5);
842
843 assert_eq!(column_count(28), 4);
845 assert_eq!(column_count(32), 4);
846
847 assert_eq!(column_count(27), 3);
849 assert_eq!(column_count(33), 3);
850
851 assert_eq!(column_count(26), 2);
853 assert_eq!(column_count(34), 2);
854
855 assert_eq!(column_count(29), 3);
857 assert_eq!(column_count(31), 3);
858 }
859
860 #[test]
861 fn test_parse_pattern() {
862 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 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 let result = parse_pattern("LLX");
888 assert!(result.is_err());
889 assert!(result.unwrap_err().contains("Invalid pattern character"));
890
891 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 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 let char_set = vec![b'a', b'b', b'c'];
929 let pattern = vec![
930 PatternChar::Lowercase,
931 PatternChar::Uppercase, PatternChar::Numeric, PatternChar::Symbol, ];
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 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 let char_set = vec![b'A', b'B', b'0', b'1', b'!', b'@'];
952 let pattern = vec![PatternChar::Lowercase]; 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 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 let char_set = vec![b'a', b'b', b'0', b'1', b'!', b'@'];
968 let pattern = vec![PatternChar::Uppercase]; 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 let char_set = vec![b'a', b'b', b'A', b'B', b'!', b'@'];
983 let pattern = vec![PatternChar::Numeric]; 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 let char_set = vec![b'a', b'b', b'A', b'B', b'0', b'1'];
998 let pattern = vec![PatternChar::Symbol]; 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', b'A', b'B', b'C', b'0', b'1', b'2', b'!', b'@', b'#', ];
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 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 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 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, ¶ms, &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, ¶ms, &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, ¶ms, &mut rng);
1144
1145 assert_eq!(passwords.len(), 2);
1146 for pass in &passwords {
1147 assert_eq!(pass.len(), 8);
1148 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 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 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(), ];
1204 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 let result = parse_exclude_chars(vec!["\x1f-9".to_string()]);
1227 assert!(result.is_ok());
1229 let chars = result.unwrap();
1230 assert!(chars.len() >= 2); }
1233
1234 #[test]
1235 fn test_parse_exclude_chars_non_printable_end() {
1236 let result = parse_exclude_chars(vec!["a-\x7f".to_string()]);
1239 assert!(result.is_ok());
1241 let chars = result.unwrap();
1243 assert!(chars.contains(&'a'));
1246 }
1247
1248 #[test]
1249 fn test_parse_exclude_chars_duplicate_handling() {
1250 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 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 let result = parse_exclude_chars(vec!["a-c".to_string(), "b".to_string()]);
1263 assert!(result.is_ok());
1264 let chars = result.unwrap();
1265 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 let result = parse_exclude_chars(vec![" -~".to_string()]);
1276 assert!(result.is_ok());
1277 let chars = result.unwrap();
1278 assert!(chars.contains(&' '));
1280 assert!(chars.contains(&'~'));
1281 }
1282
1283 #[test]
1284 fn test_build_char_set_all_types_disabled_lowercase_available() {
1285 let args = create_test_args(true, true, true, vec![]);
1287 let char_set = build_char_set(&args).unwrap();
1288 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 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 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); 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 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 let err = PasswordError::InvalidLength;
1354 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 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 assert!(password.len() >= 5);
1370 let capitals = password.chars().filter(|c| c.is_ascii_uppercase()).count();
1371 assert!(capitals >= 5); }
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 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 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 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 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 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 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 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 let args = create_test_args(true, true, true, vec![]);
1479 let result = validate_args(&args);
1480 assert!(result.is_ok()); }
1482}