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
9pub 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; 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 RulePasswordTestItem(
131 Box::new(RepeatCharacterRule::default()),
132 PasswordData::with_password("p4zRcv8#n65".to_string()),
133 vec![],
134 ),
135 RulePasswordTestItem(
137 Box::new(RepeatCharacterRule::default()),
138 PasswordData::with_password("p4&&&&&#n65".to_string()),
139 vec![ERROR_CODE],
140 ),
141 RulePasswordTestItem(
143 Box::new(RepeatCharacterRule::default()),
144 PasswordData::with_password("p4vvvvvvv#n65".to_string()),
145 vec![ERROR_CODE],
146 ),
147 RulePasswordTestItem(
149 Box::new(RepeatCharacterRule::with_sequence_len(7).unwrap()),
150 PasswordData::with_password("p4zRcv8#n65".to_string()),
151 vec![],
152 ),
153 RulePasswordTestItem(
155 Box::new(RepeatCharacterRule::with_sequence_len(7).unwrap()),
156 PasswordData::with_password("p4&&&&&#n65".to_string()),
157 vec![],
158 ),
159 RulePasswordTestItem(
161 Box::new(RepeatCharacterRule::with_sequence_len(7).unwrap()),
162 PasswordData::with_password("p4vvvvvvv#n65".to_string()),
163 vec![ERROR_CODE],
164 ),
165 RulePasswordTestItem(
167 Box::new(RepeatCharacterRule::default()),
168 PasswordData::with_password("p4&&&&&#n65FFFFF".to_string()),
169 vec![ERROR_CODE, ERROR_CODE],
170 ),
171 RulePasswordTestItem(
173 Box::new(RepeatCharacterRule::new(5, false).unwrap()),
174 PasswordData::with_password("p4&&&&&#n65FFFFF".to_string()),
175 vec![ERROR_CODE],
176 ),
177 RulePasswordTestItem(
179 Box::new(RepeatCharacterRule::default()),
180 PasswordData::with_password("p4&&&&&#n65FFFFFQr1&&&&&".to_string()),
181 vec![ERROR_CODE, ERROR_CODE],
182 ),
183 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}