Skip to main content

passay_rs/rule/
repeat_character.rs

1use crate::rule::rule_result::RuleResult;
2use crate::rule::{PasswordData, Rule};
3use std::collections::{HashMap, HashSet};
4
5pub const ERROR_CODE: &str = "ILLEGAL_MATCH";
6const DEFAULT_SEQUENCE_LENGTH: usize = 5;
7const MINIMUM_SEQUENCE_LENGTH: usize = 3;
8
9/// Rule for determining if a password contains a duplicate character sequence.
10/// The default sequence length is 5 characters.
11/// Sequences are of the form: 'bbbbb' or '#####'
12///
13/// # Example
14///
15/// ```
16///  use passay_rs::rule::PasswordData;
17///  use passay_rs::rule::repeat_character::RepeatCharacterRule;
18///  use passay_rs::rule::Rule;
19///  use fancy_regex::Regex;
20///
21///  let rule =RepeatCharacterRule::default();
22///  let password = PasswordData::with_password("p4&&&&&#n65".to_string());
23///  let result = rule.validate(&password);
24///  assert!(!result.valid());
25/// ```
26pub struct RepeatCharacterRule {
27    sequence_length: usize,
28    report_all: bool,
29}
30
31impl RepeatCharacterRule {
32    pub fn new(sequence_length: usize, report_all: bool) -> Result<Self, String> {
33        if sequence_length < MINIMUM_SEQUENCE_LENGTH {
34            return Err(format!(
35                "sequence length must be >= {MINIMUM_SEQUENCE_LENGTH}"
36            ));
37        }
38        Ok(Self {
39            sequence_length,
40            report_all,
41        })
42    }
43    pub fn with_sequence_len(sequence_len: usize) -> Result<Self, String> {
44        Self::new(sequence_len, true)
45    }
46
47    fn create_rule_result_detail_parameters(&self, match_str: &str) -> HashMap<String, String> {
48        let mut map = HashMap::with_capacity(2);
49        map.insert("match".to_string(), match_str.to_string());
50        map.insert(
51            "sequence_length".to_string(),
52            self.sequence_length.to_string(),
53        );
54        map
55    }
56}
57
58impl Rule for RepeatCharacterRule {
59    fn validate(&self, password_data: &PasswordData) -> RuleResult {
60        let mut result = RuleResult::default();
61        let mut matches = HashSet::new();
62
63        let mut previous_ch = None;
64        let mut count = 1;
65        let mut matched = false;
66        let mut sequence_start_byte = 0; // Track the byte offset of the current sequence
67
68        for (i, ch) in password_data.password.char_indices() {
69            if previous_ch.is_none() {
70                previous_ch = Some(ch);
71                sequence_start_byte = i;
72            } else if previous_ch == Some(ch) {
73                count += 1;
74                if count >= self.sequence_length {
75                    matched = true;
76                };
77            } else {
78                if matched {
79                    let matched_text = &password_data.password[sequence_start_byte..i];
80                    if !matches.contains(matched_text) {
81                        result.add_error(
82                            ERROR_CODE,
83                            Some(self.create_rule_result_detail_parameters(matched_text)),
84                        );
85
86                        if !self.report_all {
87                            return result;
88                        }
89                        matches.insert(matched_text);
90                    }
91                    matched = false;
92                }
93                previous_ch = Some(ch);
94                count = 1;
95                sequence_start_byte = i;
96            }
97        }
98
99        if matched {
100            let matched_text = &password_data.password[sequence_start_byte..];
101
102            if !matches.contains(matched_text) {
103                result.add_error(
104                    ERROR_CODE,
105                    Some(self.create_rule_result_detail_parameters(matched_text)),
106                );
107            }
108        }
109
110        result
111    }
112}
113
114impl Default for RepeatCharacterRule {
115    fn default() -> Self {
116        RepeatCharacterRule::new(DEFAULT_SEQUENCE_LENGTH, true).unwrap()
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use crate::rule::PasswordData;
123    use crate::rule::repeat_character::{ERROR_CODE, RepeatCharacterRule};
124    use crate::test::{RulePasswordTestItem, check_messages, check_passwords};
125
126    #[test]
127    fn test_passwords() {
128        let test_cases: Vec<RulePasswordTestItem> = vec![
129            // test valid password
130            RulePasswordTestItem(
131                Box::new(RepeatCharacterRule::default()),
132                PasswordData::with_password("p4zRcv8#n65".to_string()),
133                vec![],
134            ),
135            // test repeating character
136            RulePasswordTestItem(
137                Box::new(RepeatCharacterRule::default()),
138                PasswordData::with_password("p4&&&&&#n65".to_string()),
139                vec![ERROR_CODE],
140            ),
141            // test longer repeating character
142            RulePasswordTestItem(
143                Box::new(RepeatCharacterRule::default()),
144                PasswordData::with_password("p4vvvvvvv#n65".to_string()),
145                vec![ERROR_CODE],
146            ),
147            // test valid password for long regex
148            RulePasswordTestItem(
149                Box::new(RepeatCharacterRule::with_sequence_len(7).unwrap()),
150                PasswordData::with_password("p4zRcv8#n65".to_string()),
151                vec![],
152            ),
153            // test long regex with short repeat
154            RulePasswordTestItem(
155                Box::new(RepeatCharacterRule::with_sequence_len(7).unwrap()),
156                PasswordData::with_password("p4&&&&&#n65".to_string()),
157                vec![],
158            ),
159            // test long regex with long repeat
160            RulePasswordTestItem(
161                Box::new(RepeatCharacterRule::with_sequence_len(7).unwrap()),
162                PasswordData::with_password("p4vvvvvvv#n65".to_string()),
163                vec![ERROR_CODE],
164            ),
165            // test multiple matches
166            RulePasswordTestItem(
167                Box::new(RepeatCharacterRule::default()),
168                PasswordData::with_password("p4&&&&&#n65FFFFF".to_string()),
169                vec![ERROR_CODE, ERROR_CODE],
170            ),
171            // test single match
172            RulePasswordTestItem(
173                Box::new(RepeatCharacterRule::new(5, false).unwrap()),
174                PasswordData::with_password("p4&&&&&#n65FFFFF".to_string()),
175                vec![ERROR_CODE],
176            ),
177            // test duplicate matches
178            RulePasswordTestItem(
179                Box::new(RepeatCharacterRule::default()),
180                PasswordData::with_password("p4&&&&&#n65FFFFFQr1&&&&&".to_string()),
181                vec![ERROR_CODE, ERROR_CODE],
182            ),
183            // test utf8 characters
184            RulePasswordTestItem(
185                Box::new(RepeatCharacterRule::default()),
186                PasswordData::with_password("ميييييجتبیيييييي".to_string()),
187                vec![ERROR_CODE, ERROR_CODE],
188            ),
189        ];
190
191        check_passwords(test_cases);
192    }
193
194    #[test]
195    fn test_messages() {
196        let test_cases: Vec<RulePasswordTestItem> = vec![
197            RulePasswordTestItem(
198                Box::new(RepeatCharacterRule::default()),
199                PasswordData::with_password("p4&&&&&#n65".to_string()),
200                vec!["ILLEGAL_MATCH,&&&&&"],
201            ),
202            RulePasswordTestItem(
203                Box::new(RepeatCharacterRule::default()),
204                PasswordData::with_password("p4&&&&&#n65FFFFF".to_string()),
205                vec!["ILLEGAL_MATCH,&&&&&", "ILLEGAL_MATCH,FFFFF"],
206            ),
207            RulePasswordTestItem(
208                Box::new(RepeatCharacterRule::new(5, false).unwrap()),
209                PasswordData::with_password("p4&&&&&#n65FFFFF".to_string()),
210                vec!["ILLEGAL_MATCH,&&&&&"],
211            ),
212            RulePasswordTestItem(
213                Box::new(RepeatCharacterRule::default()),
214                PasswordData::with_password("p4&&&&&#n65FFFFFQr1&&&&&".to_string()),
215                vec!["ILLEGAL_MATCH,&&&&&", "ILLEGAL_MATCH,FFFFF"],
216            ),
217            RulePasswordTestItem(
218                Box::new(RepeatCharacterRule::default()),
219                PasswordData::with_password("مجتبيييييي".to_string()),
220                vec!["ILLEGAL_MATCH,يييييي"],
221            ),
222        ];
223        check_messages(test_cases);
224    }
225}