coding_agent_search/pages/
password.rs1use console::{Term, style};
16use std::io::Write;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum PasswordStrength {
21 Weak,
22 Fair,
23 Good,
24 Strong,
25}
26
27#[derive(Debug, Clone, Copy)]
28struct PasswordStrengthVisuals {
29 color: &'static str,
30 label: &'static str,
31 bar: &'static str,
32 percent: u8,
33}
34
35impl PasswordStrength {
36 fn visuals(self) -> PasswordStrengthVisuals {
37 match self {
38 Self::Weak => PasswordStrengthVisuals {
39 color: "red",
40 label: "Weak",
41 bar: "[█░░░]",
42 percent: 25,
43 },
44 Self::Fair => PasswordStrengthVisuals {
45 color: "yellow",
46 label: "Fair",
47 bar: "[██░░]",
48 percent: 50,
49 },
50 Self::Good => PasswordStrengthVisuals {
51 color: "blue",
52 label: "Good",
53 bar: "[███░]",
54 percent: 75,
55 },
56 Self::Strong => PasswordStrengthVisuals {
57 color: "green",
58 label: "Strong",
59 bar: "[████]",
60 percent: 100,
61 },
62 }
63 }
64
65 pub fn color(&self) -> &'static str {
67 self.visuals().color
68 }
69
70 pub fn label(&self) -> &'static str {
72 self.visuals().label
73 }
74
75 pub fn bar(&self) -> &'static str {
77 self.visuals().bar
78 }
79
80 pub fn percent(&self) -> u8 {
82 self.visuals().percent
83 }
84}
85
86impl std::fmt::Display for PasswordStrength {
87 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88 write!(f, "{}", self.label())
89 }
90}
91
92#[derive(Debug, Clone)]
94pub struct PasswordValidation {
95 pub strength: PasswordStrength,
97 pub score: u8,
99 pub entropy_bits: f64,
101 pub suggestions: Vec<&'static str>,
103 pub checks: PasswordChecks,
105}
106
107#[derive(Debug, Clone, Copy)]
109pub struct PasswordChecks {
110 pub has_lowercase: bool,
111 pub has_uppercase: bool,
112 pub has_digit: bool,
113 pub has_special: bool,
114 pub length: usize,
115 pub meets_min_length: bool,
116}
117
118pub fn validate_password(password: &str) -> PasswordValidation {
137 let length = password.chars().count();
138 let has_upper = password.chars().any(|c| c.is_ascii_uppercase());
139 let has_lower = password.chars().any(|c| c.is_ascii_lowercase());
140 let has_digit = password.chars().any(|c| c.is_ascii_digit());
141 let has_special = password.chars().any(|c| !c.is_alphanumeric());
142
143 let length_score: u8 = match length {
145 0..=7 => 0,
146 8..=11 => 1,
147 12..=15 => 2,
148 _ => 3,
149 };
150
151 let score =
153 length_score + has_upper as u8 + has_lower as u8 + has_digit as u8 + has_special as u8;
154
155 let mut suggestions = Vec::new();
157 if length < 12 {
158 suggestions.push("Use at least 12 characters");
159 }
160 if !has_upper {
161 suggestions.push("Add uppercase letters");
162 }
163 if !has_lower {
164 suggestions.push("Add lowercase letters");
165 }
166 if !has_digit {
167 suggestions.push("Add numbers");
168 }
169 if !has_special {
170 suggestions.push("Add special characters (!@#$%^&*)");
171 }
172
173 let strength = match score {
175 0..=2 => PasswordStrength::Weak,
176 3..=4 => PasswordStrength::Fair,
177 5..=6 => PasswordStrength::Good,
178 _ => PasswordStrength::Strong,
179 };
180
181 let entropy_bits = estimate_entropy(password);
183
184 PasswordValidation {
185 strength,
186 score,
187 entropy_bits,
188 suggestions,
189 checks: PasswordChecks {
190 has_lowercase: has_lower,
191 has_uppercase: has_upper,
192 has_digit,
193 has_special,
194 length,
195 meets_min_length: length >= 12,
196 },
197 }
198}
199
200fn estimate_entropy(password: &str) -> f64 {
205 if password.is_empty() {
206 return 0.0;
207 }
208
209 let has_lower = password.chars().any(|c| c.is_ascii_lowercase());
210 let has_upper = password.chars().any(|c| c.is_ascii_uppercase());
211 let has_digit = password.chars().any(|c| c.is_ascii_digit());
212 let has_special = password.chars().any(|c| !c.is_alphanumeric());
213
214 let mut pool_size = 0u32;
215 if has_lower {
216 pool_size += 26;
217 }
218 if has_upper {
219 pool_size += 26;
220 }
221 if has_digit {
222 pool_size += 10;
223 }
224 if has_special {
225 pool_size += 32;
226 }
227
228 if pool_size == 0 {
229 pool_size = 26; }
231
232 let bits_per_char = (pool_size as f64).log2();
233 let length = password.chars().count() as f64;
234
235 bits_per_char * length
236}
237
238pub fn display_strength(term: &mut Term, validation: &PasswordValidation) -> std::io::Result<()> {
246 let strength = &validation.strength;
247
248 let colored_bar = match strength {
250 PasswordStrength::Weak => style(strength.bar()).red(),
251 PasswordStrength::Fair => style(strength.bar()).yellow(),
252 PasswordStrength::Good => style(strength.bar()).blue(),
253 PasswordStrength::Strong => style(strength.bar()).green(),
254 };
255
256 let colored_label = match strength {
257 PasswordStrength::Weak => style(strength.label()).red().bold(),
258 PasswordStrength::Fair => style(strength.label()).yellow().bold(),
259 PasswordStrength::Good => style(strength.label()).blue().bold(),
260 PasswordStrength::Strong => style(strength.label()).green().bold(),
261 };
262
263 term.clear_line()?;
265 write!(term, "Strength: {} {}", colored_bar, colored_label)?;
266
267 if !validation.suggestions.is_empty() {
269 writeln!(term)?;
270 for suggestion in &validation.suggestions {
271 writeln!(term, " {} {}", style("•").dim(), style(suggestion).dim())?;
272 }
273 }
274
275 term.flush()?;
276 Ok(())
277}
278
279pub fn format_strength_inline(validation: &PasswordValidation) -> String {
283 let strength = &validation.strength;
284
285 let bar = match strength {
286 PasswordStrength::Weak => style(strength.bar()).red(),
287 PasswordStrength::Fair => style(strength.bar()).yellow(),
288 PasswordStrength::Good => style(strength.bar()).blue(),
289 PasswordStrength::Strong => style(strength.bar()).green(),
290 };
291
292 let label = match strength {
293 PasswordStrength::Weak => style(strength.label()).red().bold(),
294 PasswordStrength::Fair => style(strength.label()).yellow().bold(),
295 PasswordStrength::Good => style(strength.label()).blue().bold(),
296 PasswordStrength::Strong => style(strength.label()).green().bold(),
297 };
298
299 format!("{} {}", bar, label)
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305
306 #[test]
307 fn test_empty_password() {
308 let result = validate_password("");
309 assert_eq!(result.strength, PasswordStrength::Weak);
310 assert!(!result.suggestions.is_empty());
311 }
312
313 #[test]
314 fn test_weak_password() {
315 let result = validate_password("password");
316 assert_eq!(result.strength, PasswordStrength::Weak);
317 assert!(result.suggestions.contains(&"Add uppercase letters"));
318 assert!(result.suggestions.contains(&"Add numbers"));
319 assert!(
320 result
321 .suggestions
322 .contains(&"Add special characters (!@#$%^&*)")
323 );
324 }
325
326 #[test]
327 fn test_fair_password() {
328 let result = validate_password("Password1");
329 assert_eq!(result.strength, PasswordStrength::Fair);
330 }
331
332 #[test]
333 fn test_good_password() {
334 let result = validate_password("Password1!");
335 assert_eq!(result.strength, PasswordStrength::Good);
336 }
337
338 #[test]
339 fn test_strong_password() {
340 let result = validate_password("MySecureP@ssw0rd!");
341 assert_eq!(result.strength, PasswordStrength::Strong);
342 assert!(result.suggestions.is_empty());
343 }
344
345 #[test]
346 fn test_long_lowercase_only() {
347 let result = validate_password("averylongpasswordwithnothingelse");
349 assert!(matches!(
350 result.strength,
351 PasswordStrength::Fair | PasswordStrength::Good
352 ));
353 }
354
355 #[test]
356 fn test_strength_bar_rendering() {
357 let cases = [
358 (PasswordStrength::Weak, "[█░░░]"),
359 (PasswordStrength::Fair, "[██░░]"),
360 (PasswordStrength::Good, "[███░]"),
361 (PasswordStrength::Strong, "[████]"),
362 ];
363
364 for (strength, expected_bar) in cases {
365 assert_eq!(strength.bar(), expected_bar, "{strength:?}");
366 }
367 }
368
369 #[test]
370 fn test_strength_color_and_label() {
371 let cases = [
372 (PasswordStrength::Weak, "red", "Weak"),
373 (PasswordStrength::Fair, "yellow", "Fair"),
374 (PasswordStrength::Good, "blue", "Good"),
375 (PasswordStrength::Strong, "green", "Strong"),
376 ];
377
378 for (strength, expected_color, expected_label) in cases {
379 assert_eq!(strength.color(), expected_color, "{strength:?}");
380 assert_eq!(strength.label(), expected_label, "{strength:?}");
381 assert_eq!(strength.to_string(), expected_label, "{strength:?}");
382 }
383 }
384
385 #[test]
386 fn test_strength_percent() {
387 let cases = [
388 (PasswordStrength::Weak, 25),
389 (PasswordStrength::Fair, 50),
390 (PasswordStrength::Good, 75),
391 (PasswordStrength::Strong, 100),
392 ];
393
394 for (strength, expected_percent) in cases {
395 assert_eq!(strength.percent(), expected_percent, "{strength:?}");
396 }
397 }
398
399 #[test]
400 fn test_checks_populated() {
401 let result = validate_password("Test123!");
402 assert!(result.checks.has_lowercase);
403 assert!(result.checks.has_uppercase);
404 assert!(result.checks.has_digit);
405 assert!(result.checks.has_special);
406 assert_eq!(result.checks.length, 8);
407 assert!(!result.checks.meets_min_length);
408 }
409
410 #[test]
411 fn test_entropy_calculation() {
412 let result = validate_password("MySecureP@ssw0rd");
416 assert!(result.entropy_bits > 80.0);
417 }
418
419 #[test]
420 fn test_unicode_password() {
421 let result = validate_password("Pässwörd123!");
423 assert!(result.checks.has_special); assert!(result.checks.has_uppercase);
425 assert!(result.checks.has_digit);
426 }
427}