Skip to main content

password_strength/analyzer/
trigraph.rs

1use crate::analyzer::Analyzer;
2use std::collections::HashMap;
3use unicode_segmentation::UnicodeSegmentation;
4
5/// `TrigraphAnalyzer` is an analyzer that calculates the strength of a password based on the frequency of trigraphs (sequences of three characters).
6///
7/// It works by counting the occurrences of each trigraph in the password and then calculating the entropy of these frequencies.
8/// Higher entropy indicates a more random and thus stronger password.
9///
10/// # Examples
11///
12/// ```
13/// use password_strength::analyzer::trigraph::TrigraphAnalyzer;
14/// use password_strength::analyzer::Analyzer;
15///
16/// let analyzer = TrigraphAnalyzer::default();
17///
18/// let password = "abcdef";
19/// let score = analyzer.analyze(password);
20/// assert!(score > 0.0);
21/// assert!(score <= 1.0);
22///
23/// let password = "password123";
24/// let score = analyzer.analyze(password);
25/// assert!(score > 0.0);
26/// assert!(score <= 1.0);
27///
28/// let password = "1234567890";
29/// let score = analyzer.analyze(password);
30/// assert!(score > 0.0);
31/// assert!(score <= 1.0);
32///
33/// let password = "!@#$%^&*()";
34/// let score = analyzer.analyze(password);
35/// assert!(score > 0.0);
36/// assert!(score <= 1.0);
37///
38/// let password = "ab";
39/// let score = analyzer.analyze(password);
40/// assert_eq!(score, 0.0);
41/// ```
42#[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}