1#![doc = include_str!("../README.md")]
2#![doc(
3 html_logo_url = "https://raw.githubusercontent.com/2bndy5/mk-pass/main/docs/docs/images/logo-square.png"
4)]
5#![doc(
6 html_favicon_url = "https://github.com/2bndy5/mk-pass/raw/main/docs/docs/images/favicon.ico"
7)]
8use rand::prelude::*;
9mod helpers;
10use helpers::{CharKind, CountTypesUsed};
11pub use helpers::{DECIMAL, LOWERCASE, SPECIAL_CHARACTERS, UPPERCASE};
12mod config;
13pub use config::PasswordRequirements;
14
15#[cfg(feature = "clap")]
16pub use clap;
17
18pub fn generate_password(config: PasswordRequirements) -> String {
23 let config = config.validate();
24 let len = config.length as usize;
25 let mut rng = rand::rng();
26 let mut password = String::with_capacity(len);
27
28 let mut used_types = CountTypesUsed::default();
29 let mut available_types = vec![CharKind::Uppercase, CharKind::Lowercase];
30 if config.specials > 0 {
31 available_types.push(CharKind::Special);
32 }
33 if config.decimal > 0 {
34 available_types.push(CharKind::Decimal);
35 }
36 let max_letters = len as u16 - config.decimal - config.specials;
37 let max_lowercase = max_letters / 2;
38 let max_uppercase = max_letters - max_lowercase;
39 #[cfg(test)]
40 {
41 println!("max_lowers: {max_lowercase}, max_uppers: {max_uppercase}");
42 }
43
44 let mut pass_chars = vec!['\n'; len];
45
46 let start = if config.first_is_letter {
47 let letter_types = [CharKind::Lowercase, CharKind::Uppercase];
48 let sample_kind = letter_types[rng.random_range(0..letter_types.len())];
49 let sample_set = sample_kind.into_sample();
50 pass_chars[0] = sample_set[rng.random_range(0..LOWERCASE.len())];
51 match sample_kind {
52 CharKind::Lowercase => {
53 used_types.lowercase += 1;
54 if used_types.lowercase == max_lowercase {
55 available_types = CharKind::pop_kind(available_types, &sample_kind);
56 }
57 }
58 _ => {
59 used_types.uppercase += 1;
60 if used_types.uppercase == max_uppercase {
61 available_types = CharKind::pop_kind(available_types, &sample_kind);
62 }
63 }
64 }
65 1
66 } else {
67 0
68 };
69 let mut positions = (start..len).collect::<Vec<usize>>();
70
71 for _ in start..len {
72 debug_assert!(!available_types.is_empty());
73 let kind = available_types[rng.random_range(0..available_types.len())];
75 match kind {
76 CharKind::Lowercase => {
77 used_types.lowercase += 1;
78 #[cfg(test)]
79 {
80 println!("used lowers: {}", used_types.lowercase);
81 }
82 if used_types.lowercase == max_lowercase {
83 available_types = CharKind::pop_kind(available_types, &kind);
84 }
85 }
86 CharKind::Uppercase => {
87 used_types.uppercase += 1;
88 #[cfg(test)]
89 {
90 println!("used uppers: {}", used_types.uppercase);
91 }
92 if used_types.uppercase == max_uppercase {
93 available_types = CharKind::pop_kind(available_types, &kind);
94 }
95 }
96 CharKind::Decimal => {
97 used_types.number += 1;
98 if used_types.number == config.decimal {
99 available_types = CharKind::pop_kind(available_types, &kind);
100 }
101 }
102 CharKind::Special => {
103 used_types.special += 1;
104 if used_types.special == config.specials {
105 available_types = CharKind::pop_kind(available_types, &kind);
106 }
107 }
108 }
109
110 let sample = kind.into_sample();
112 let mut rand_index = rng.random_range(0..sample.len());
113 if !config.allow_repeats {
114 while pass_chars.contains(&sample[rand_index]) {
115 rand_index = rng.random_range(0..sample.len());
116 }
117 }
118
119 let rnd_pos = rng.random_range(0..positions.len());
121 let pos = positions.remove(rnd_pos);
122 pass_chars[pos] = sample[rand_index];
123 }
124
125 for ch in pass_chars {
126 password.push(ch);
127 }
128 password
129}
130
131#[cfg(test)]
132mod test {
133 use super::{PasswordRequirements, generate_password};
134 use crate::helpers::{DECIMAL, LOWERCASE, SPECIAL_CHARACTERS, UPPERCASE};
135
136 fn count(output: &str) -> (usize, usize, usize, usize, usize) {
137 let (mut uppers, mut lowers, mut decimal, mut specials) = (0, 0, 0, 0);
138 let mut repeats = vec![];
139 for (i, ch) in output.char_indices() {
140 if LOWERCASE.contains(&ch) {
141 lowers += 1;
142 } else if UPPERCASE.contains(&ch) {
143 uppers += 1;
144 } else if DECIMAL.contains(&ch) {
145 decimal += 1;
146 } else if SPECIAL_CHARACTERS.contains(&ch) {
147 specials += 1;
148 }
149 if output[0..i].contains(ch) && !repeats.contains(&ch) {
150 repeats.push(ch);
151 }
152 }
153 let repeats = repeats.len();
154 println!(
155 "decimal: {decimal}, uppercase: {uppers}, lowercase: {lowers}, special: {specials}, repeats: {repeats}"
156 );
157 (uppers, lowers, decimal, specials, repeats)
158 }
159
160 fn gen_pass(config: PasswordRequirements) {
161 let password = generate_password(config);
162 println!("Generated password: {password}");
163 assert_eq!(password.len(), config.length as usize);
164 let (uppers, lowers, decimal, specials, repeats) = count(&password);
165 assert_eq!(decimal, config.decimal as usize);
166 assert_eq!(specials, config.specials as usize);
167 let letters = (config.length - config.specials - config.decimal) as usize;
168 assert_eq!(letters, uppers + lowers);
169 assert_eq!(repeats > 0, config.allow_repeats);
170 if config.first_is_letter {
171 let first = password.chars().next().unwrap();
172 assert!(LOWERCASE.contains(&first) || UPPERCASE.contains(&first));
173 }
174 }
175
176 #[test]
177 fn special_4() {
178 let config = PasswordRequirements {
179 specials: 4,
180 ..Default::default()
181 };
182 gen_pass(config);
183 }
184
185 #[test]
186 fn no_special() {
187 let config = PasswordRequirements {
188 specials: 0,
189 ..Default::default()
190 };
191 gen_pass(config);
192 }
193
194 #[test]
195 fn no_first_is_letter() {
196 let config = PasswordRequirements {
198 first_is_letter: false,
199 ..Default::default()
200 };
201 gen_pass(config);
202 }
203
204 #[test]
205 fn no_decimal() {
206 let config = PasswordRequirements {
207 decimal: 0,
208 ..Default::default()
209 };
210 gen_pass(config);
211 }
212
213 #[test]
214 fn allow_repeats() {
215 let config = PasswordRequirements {
216 allow_repeats: true,
217 decimal: 18,
218 length: 20,
219 specials: 0,
220 ..Default::default()
221 };
222 gen_pass(config);
223 }
224
225 fn till_first_is(lower: bool) {
234 let config = PasswordRequirements {
235 specials: 8,
236 decimal: 0,
237 length: 10,
238 ..Default::default()
239 };
240 let sample_set: &[char] = if lower { &LOWERCASE } else { &UPPERCASE };
241
242 let mut password = generate_password(config);
243 while !sample_set.contains(&password.chars().next().unwrap()) {
244 password = generate_password(config);
245 }
246 }
247
248 #[test]
249 fn gen_first_lower() {
250 till_first_is(true);
251 }
252
253 #[test]
254 fn gen_first_upper() {
255 till_first_is(false);
256 }
257}