Skip to main content

passay_rs/rule/
repeat_characters.rs

1use crate::rule::rule_result::RuleResult;
2use crate::rule::{PasswordData, Rule};
3use std::collections::HashMap;
4
5const ERROR_CODE: &str = "ILLEGAL_REPEATED_CHARS";
6const DEFAULT_SEQUENCE_LENGTH: usize = 5;
7const DEFAULT_SEQUENCE_COUNT: usize = 1;
8
9/// Rule for determining if a password contains multiple sequences of repeating characters.
10/// For example, the password "11a22b333xyz" will fail validation of this rule with
11/// a sequence length of 2 and sequence count of 3, since it contains 3 sequences (or more)
12/// of 2 repeating characters (or more).
13///
14/// # Example
15///
16/// ```
17///  use passay_rs::rule::PasswordData;
18///  use passay_rs::rule::repeat_characters::RepeatCharactersRule;
19///  use passay_rs::rule::Rule;
20///  use fancy_regex::Regex;
21///
22///  let rule = RepeatCharactersRule::new(5, 2).unwrap();
23///  let password = PasswordData::with_password("p4&&&&&#n65FFFFF".to_string());
24///  let result = rule.validate(&password);
25///  assert!(!result.valid());
26/// ```
27pub struct RepeatCharactersRule {
28    sequence_length: usize,
29    sequence_count: usize,
30}
31
32impl RepeatCharactersRule {
33    pub fn new(sequence_length: usize, sequence_count: usize) -> Result<Self, String> {
34        if sequence_count < 1 {
35            return Err("sequence count must be > 0".into());
36        }
37        if sequence_length < 2 {
38            return Err("sequence length must be > 2".into());
39        }
40
41        Ok(Self {
42            sequence_length,
43            sequence_count,
44        })
45    }
46    pub fn with_sequence_length(sequence_length: usize) -> Result<Self, String> {
47        Self::new(sequence_length, DEFAULT_SEQUENCE_COUNT)
48    }
49
50    fn create_rule_result_detail_parameters(&self, matches: &[String]) -> HashMap<String, String> {
51        let mut map = HashMap::with_capacity(4);
52        map.insert(
53            "sequenceLength".to_string(),
54            self.sequence_length.to_string(),
55        );
56        map.insert("sequenceCount".to_string(), self.sequence_count.to_string());
57        map.insert("matchesCount".to_string(), matches.len().to_string());
58        map.insert("matches".to_string(), matches.join(","));
59        map
60    }
61}
62
63impl Default for RepeatCharactersRule {
64    fn default() -> Self {
65        Self::new(DEFAULT_SEQUENCE_LENGTH, DEFAULT_SEQUENCE_COUNT).unwrap()
66    }
67}
68
69impl Rule for RepeatCharactersRule {
70    fn validate(&self, password_data: &PasswordData) -> RuleResult {
71        let mut result = RuleResult::default();
72        let mut matches = vec![];
73        let password = format!("{}{}", password_data.password(), '\u{ffff}');
74        let mut count = 0;
75        let mut repeat = 1;
76        let mut prev: Option<char> = None;
77        let chars: Vec<char> = password.chars().collect();
78        let max = chars.len() - 1;
79
80        for i in 0..=max {
81            let c = chars[i];
82            if prev.is_some() && c == prev.unwrap() {
83                repeat += 1;
84            } else {
85                if repeat >= self.sequence_length {
86                    let m: String = chars[i - repeat..i].iter().collect();
87                    matches.push(m);
88                    count += 1;
89                }
90                repeat = 1;
91            }
92            prev = Some(c);
93        }
94        if count >= self.sequence_count {
95            result.add_error(
96                ERROR_CODE,
97                Some(self.create_rule_result_detail_parameters(&matches)),
98            );
99        }
100        result
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use crate::rule::PasswordData;
107    use crate::rule::repeat_characters::{ERROR_CODE, RepeatCharactersRule};
108    use crate::test::{RulePasswordTestItem, check_messages, check_passwords};
109
110    #[test]
111    fn test_passwords() {
112        let test_cases: Vec<RulePasswordTestItem> = vec![
113            // test valid password
114            RulePasswordTestItem(
115                Box::new(RepeatCharactersRule::default()),
116                PasswordData::with_password("p4zRcv8#n65".to_string()),
117                vec![],
118            ),
119            // test repeating character
120            RulePasswordTestItem(
121                Box::new(RepeatCharactersRule::default()),
122                PasswordData::with_password("p4&&&&&#n65".to_string()),
123                vec![ERROR_CODE],
124            ),
125            // test longer repeating character
126            RulePasswordTestItem(
127                Box::new(RepeatCharactersRule::default()),
128                PasswordData::with_password("p4vvvvvvv#n65".to_string()),
129                vec![ERROR_CODE],
130            ),
131            // test valid password for long sequence
132            RulePasswordTestItem(
133                Box::new(RepeatCharactersRule::with_sequence_length(7).unwrap()),
134                PasswordData::with_password("p4zRcv8#n65".to_string()),
135                vec![],
136            ),
137            // test long sequence with short repeat
138            RulePasswordTestItem(
139                Box::new(RepeatCharactersRule::with_sequence_length(7).unwrap()),
140                PasswordData::with_password("p4&&&&&#n65".to_string()),
141                vec![],
142            ),
143            // test long sequence with long repeat
144            RulePasswordTestItem(
145                Box::new(RepeatCharactersRule::with_sequence_length(7).unwrap()),
146                PasswordData::with_password("p4vvvvvvv#n65".to_string()),
147                vec![ERROR_CODE],
148            ),
149            // test multiple matches
150            RulePasswordTestItem(
151                Box::new(RepeatCharactersRule::default()),
152                PasswordData::with_password("p4&&&&&#n65FFFFF".to_string()),
153                vec![ERROR_CODE],
154            ),
155            // test multiple matches with allowed count
156            RulePasswordTestItem(
157                Box::new(RepeatCharactersRule::new(5, 3).unwrap()),
158                PasswordData::with_password("p4&&&&&#n65FFFFF".to_string()),
159                vec![],
160            ),
161            // test single match when max is two
162            RulePasswordTestItem(
163                Box::new(RepeatCharactersRule::new(5, 2).unwrap()),
164                PasswordData::with_password("p4&&&&&#n65FFFF".to_string()),
165                vec![],
166            ),
167            // test two matches when max is two
168            RulePasswordTestItem(
169                Box::new(RepeatCharactersRule::new(5, 2).unwrap()),
170                PasswordData::with_password("p4&&&&&#n65FFFFF".to_string()),
171                vec![ERROR_CODE],
172            ),
173            // test two matches when max is more than two
174            RulePasswordTestItem(
175                Box::new(RepeatCharactersRule::new(5, 3).unwrap()),
176                PasswordData::with_password("p4&&&&&#n65FFFFF".to_string()),
177                vec![],
178            ),
179        ];
180
181        check_passwords(test_cases);
182    }
183
184    #[test]
185    fn test_messages() {
186        let test_cases: Vec<RulePasswordTestItem> = vec![RulePasswordTestItem(
187            Box::new(RepeatCharactersRule::new(2, 2).unwrap()),
188            PasswordData::with_password("paaxvbbdkccx".to_string()),
189            vec!["ILLEGAL_REPEATED_CHARS,3,2,2,aa,bb,cc"],
190        )];
191        check_messages(test_cases);
192    }
193}