password_strength/analyzer/
trigraph.rs1use crate::analyzer::Analyzer;
2use std::collections::HashMap;
3use unicode_segmentation::UnicodeSegmentation;
4
5#[derive(Default)]
43pub struct TrigraphAnalyzer;
44
45impl Analyzer for TrigraphAnalyzer {
46 fn analyze(&self, password: &str) -> f32 {
47 let graphemes: Vec<&str> = password.graphemes(true).collect();
48 let num_graphemes = graphemes.len();
49
50 if num_graphemes < 3 {
51 return 0.0;
52 }
53
54 let mut frequencies = HashMap::new();
55 let total_trigraphs = num_graphemes - 2;
56
57 if total_trigraphs == 0 {
58 return 0.0;
59 }
60
61 for trigraph_slice in graphemes.windows(3) {
62 let trigraph_key = trigraph_slice.join("");
63 *frequencies.entry(trigraph_key).or_insert(0) += 1;
64 }
65
66 let mut entropy = 0.0;
67 let total_trigraphs_f64 = total_trigraphs as f64;
68
69 for &count in frequencies.values() {
70 let p = count as f64 / total_trigraphs_f64;
71 if p > 0.0 {
72 entropy -= p * p.log2();
73 }
74 }
75
76 let num_unique_trigraphs = frequencies.len() as f64;
77 let max_entropy = if num_unique_trigraphs > 1.0 {
78 num_unique_trigraphs.log2()
79 } else {
80 0.0
81 };
82
83 let final_score = if max_entropy > 0.0 {
84 entropy / max_entropy
85 } else {
86 0.0
87 };
88
89 final_score.clamp(0.0, 1.0) as f32
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96
97 macro_rules! assert_trigraph_analyzer {
98 ($password:expr, $test_name:ident) => {
99 #[test]
100 fn $test_name() {
101 let analyzer = TrigraphAnalyzer::default();
102 let score = analyzer.analyze($password);
103
104 assert!(score > 0.0);
105 assert!(score <= 1.0);
106 }
107 };
108 }
109
110 assert_trigraph_analyzer!("abcdef", test_trigraph_abcdef);
111 assert_trigraph_analyzer!("password123", test_trigraph_password123);
112 assert_trigraph_analyzer!("1234567890", test_trigraph_1234567890);
113 assert_trigraph_analyzer!("!@#$%^&*()", test_trigraph_symbols);
114
115 #[test]
116 fn test_unicode_trigraph() {
117 let analyzer = TrigraphAnalyzer;
118 let password = "aB1!üöß字例😊_Long";
119 let score = analyzer.analyze(password);
120
121 assert!((0.0..=1.0).contains(&score), "Score should be between 0.0 and 1.0");
122 }
123
124 #[test]
125 fn test_short_unicode() {
126 let analyzer = TrigraphAnalyzer;
127 let password = "üö";
128 let score = analyzer.analyze(password);
129
130 assert_eq!(score, 0.0, "The score should be 0.0 for less than 3 graphemes");
131 }
132
133 #[test]
134 fn test_repeated_unicode_trigraph() {
135 let analyzer = TrigraphAnalyzer;
136 let password = "😊😊😊😊😊";
137 let score = analyzer.analyze(password);
138
139 assert_eq!(score, 0.0, "The score should be 0.0 for highly repetitive trigraphs");
140 }
141}