Skip to main content

passay_rs/rule/
dictionary_substring.rs

1use crate::dictionary::Dictionary;
2use crate::rule::rule_result::RuleResult;
3use crate::rule::{DictionaryRuleTrait, PasswordData, Rule};
4use std::collections::HashMap;
5
6pub(crate) const ERROR_CODE: &str = "ILLEGAL_WORD";
7const ERROR_CODE_REVERSED: &str = "ILLEGAL_WORD_REVERSED";
8
9/// Rule for determining if a password contains a dictionary word with optional checking for reversed words.
10///
11/// # Example
12///
13/// ```
14///  use passay_rs::dictionary::word_lists::word_list_dictionary::WordListDictionary;
15///  use passay_rs::dictionary::word_lists::ArrayWordList;
16///  use passay_rs::dictionary::word_lists::sort::SliceSort;
17///  use passay_rs::rule::dictionary_substring::DictionarySubstringRule;
18///  use passay_rs::rule::PasswordData;
19///  use passay_rs::rule::Rule;
20///
21///  let words = ["test", "z", "x", "y", "man", "lance"].map(String::from).to_vec();
22///  // world list should be sorted so we sort it now
23///  let word_list = ArrayWordList::with_sorter(words, false, Some(SliceSort));
24///  let dictionary = WordListDictionary::new(word_list);
25///  let dictionary_rule = DictionarySubstringRule::new(dictionary, true);
26///  let password = PasswordData::with_password("p4tlancely5gew".to_string());
27///  let result = dictionary_rule.validate(&password);
28///  assert!(!result.valid());
29/// ```
30pub struct DictionarySubstringRule<D: Dictionary> {
31    dictionary: D,
32    match_backwards: bool,
33}
34
35impl<D: Dictionary> DictionarySubstringRule<D> {
36    pub fn new(dictionary: D, match_backwards: bool) -> Self {
37        Self {
38            dictionary,
39            match_backwards,
40        }
41    }
42    pub fn from_dictionary(dictionary: D) -> Self {
43        Self {
44            dictionary,
45            match_backwards: false,
46        }
47    }
48    fn do_word_search(&self, text: &str) -> Option<String> {
49        for i in 1..=text.len() {
50            let mut j = 0usize;
51            while j + i <= text.len() {
52                let s = &text[j..j + i];
53                if self.dictionary.search(s) {
54                    return Some(s.to_string());
55                }
56                j += 1;
57            }
58        }
59        None
60    }
61    fn create_rule_result_detail_parameters(&self, matching_word: &str) -> HashMap<String, String> {
62        let mut map = HashMap::with_capacity(1);
63        map.insert("matchingWord".to_string(), matching_word.to_string());
64        map
65    }
66}
67
68impl<D: Dictionary> Rule for DictionarySubstringRule<D> {
69    fn validate(&self, password_data: &PasswordData) -> RuleResult {
70        let mut result = RuleResult::default();
71        let text = password_data.password();
72        let matching_word = self.do_word_search(text);
73        if let Some(m) = matching_word {
74            result.add_error(
75                ERROR_CODE,
76                Some(self.create_rule_result_detail_parameters(&m)),
77            )
78        }
79        if self.match_backwards && text.len() > 1 {
80            let text = text.chars().rev().collect::<String>();
81            let matching_word = self.do_word_search(&text);
82            if let Some(m) = matching_word {
83                result.add_error(
84                    ERROR_CODE_REVERSED,
85                    Some(self.create_rule_result_detail_parameters(&m)),
86                )
87            }
88        }
89        result
90    }
91    fn as_dictionary_rule(&self) -> Option<&dyn DictionaryRuleTrait> {
92        Some(self)
93    }
94}
95impl<D: Dictionary> DictionaryRuleTrait for DictionarySubstringRule<D> {
96    fn dictionary(&self) -> &dyn Dictionary {
97        &self.dictionary
98    }
99}
100#[cfg(test)]
101mod tests {
102    use crate::dictionary::word_lists::sort::SliceSort;
103    use crate::dictionary::word_lists::word_list_dictionary::WordListDictionary;
104    use crate::dictionary::word_lists::{ArrayWordList, create_from_read};
105    use crate::rule::PasswordData;
106    use crate::rule::dictionary_substring::{
107        DictionarySubstringRule, ERROR_CODE, ERROR_CODE_REVERSED,
108    };
109    use crate::test::{RulePasswordTestItem, check_messages, check_passwords};
110
111    #[test]
112    fn test_passwords() {
113        let test_cases: Vec<RulePasswordTestItem> = vec![
114            // valid password
115            RulePasswordTestItem(
116                create_rule(),
117                PasswordData::with_password("p4t3t#7wd5gew".to_string()),
118                vec![],
119            ),
120            // dictionary word
121            RulePasswordTestItem(
122                create_rule(),
123                PasswordData::with_password("p4tlancely5gew".to_string()),
124                vec![ERROR_CODE],
125            ),
126            // backwards dictionary word
127            RulePasswordTestItem(
128                create_rule(),
129                PasswordData::with_password("p4tylecnal5gew".to_string()),
130                vec![],
131            ),
132            // mixed case dictionary word
133            RulePasswordTestItem(
134                create_rule(),
135                PasswordData::with_password("p4tlAnCeLy5gew".to_string()),
136                vec![],
137            ),
138            // backwards mixed case dictionary word
139            RulePasswordTestItem(
140                create_rule(),
141                PasswordData::with_password("p4tyLeCnAl5gew".to_string()),
142                vec![],
143            ),
144            // valid password
145            RulePasswordTestItem(
146                create_backward_rule(),
147                PasswordData::with_password("p4t3t#7wd5gew".to_string()),
148                vec![],
149            ),
150            // dictionary word
151            RulePasswordTestItem(
152                create_backward_rule(),
153                PasswordData::with_password("p4tlancely5gew".to_string()),
154                vec![ERROR_CODE],
155            ),
156            // backwards dictionary word
157            RulePasswordTestItem(
158                create_backward_rule(),
159                PasswordData::with_password("p4tylecnal5gew".to_string()),
160                vec![ERROR_CODE_REVERSED],
161            ),
162            // mixed case dictionary word
163            RulePasswordTestItem(
164                create_backward_rule(),
165                PasswordData::with_password("p4tlAnCeLy5gew".to_string()),
166                vec![],
167            ),
168            // backwards mixed case dictionary word
169            RulePasswordTestItem(
170                create_backward_rule(),
171                PasswordData::with_password("p4tyLeCnAl5gew".to_string()),
172                vec![],
173            ),
174            // valid password
175            RulePasswordTestItem(
176                create_ignore_case_rule(),
177                PasswordData::with_password("p4t3t#7wd5gew".to_string()),
178                vec![],
179            ),
180            // dictionary word
181            RulePasswordTestItem(
182                create_ignore_case_rule(),
183                PasswordData::with_password("p4tlancely5gew".to_string()),
184                vec![ERROR_CODE],
185            ),
186            // backwards dictionary word
187            RulePasswordTestItem(
188                create_ignore_case_rule(),
189                PasswordData::with_password("p4tylecnal5gew".to_string()),
190                vec![],
191            ),
192            // mixed case dictionary word
193            RulePasswordTestItem(
194                create_ignore_case_rule(),
195                PasswordData::with_password("p4tlAnCeLy5gew".to_string()),
196                vec![ERROR_CODE],
197            ),
198            // backwards mixed case dictionary word
199            RulePasswordTestItem(
200                create_ignore_case_rule(),
201                PasswordData::with_password("p4tyLeCnAl5gew".to_string()),
202                vec![],
203            ),
204            // valid password
205            RulePasswordTestItem(
206                create_all_rule(),
207                PasswordData::with_password("p4t3t#7wd5gew".to_string()),
208                vec![],
209            ),
210            // dictionary word
211            RulePasswordTestItem(
212                create_all_rule(),
213                PasswordData::with_password("p4tlancely5gew".to_string()),
214                vec![ERROR_CODE],
215            ),
216            // backwards dictionary word
217            RulePasswordTestItem(
218                create_all_rule(),
219                PasswordData::with_password("p4tylecnal5gew".to_string()),
220                vec![ERROR_CODE_REVERSED],
221            ),
222            // mixed case dictionary word
223            RulePasswordTestItem(
224                create_all_rule(),
225                PasswordData::with_password("p4tlAnCeLy5gew".to_string()),
226                vec![ERROR_CODE],
227            ),
228            // backwards mixed case dictionary word
229            RulePasswordTestItem(
230                create_all_rule(),
231                PasswordData::with_password("p4tyLeCnAl5gew".to_string()),
232                vec![ERROR_CODE_REVERSED],
233            ),
234        ];
235        check_passwords(test_cases);
236    }
237
238    #[test]
239    fn test_messages() {
240        let test_cases: Vec<RulePasswordTestItem> = vec![
241            RulePasswordTestItem(
242                create_rule(),
243                PasswordData::with_password("p4tlancely5gew".to_string()),
244                vec!["ILLEGAL_WORD,lance"],
245            ),
246            RulePasswordTestItem(
247                create_backward_rule(),
248                PasswordData::with_password("p4tylecnal5gew".to_string()),
249                vec!["ILLEGAL_WORD_REVERSED,lance"],
250            ),
251        ];
252        check_messages(test_cases);
253    }
254    fn create_rule() -> Box<DictionarySubstringRule<WordListDictionary<ArrayWordList>>> {
255        let case_sensitive_word_list = create_from_read(read_word_list(), true, Some(SliceSort));
256        let case_sensitive_dict = WordListDictionary::new(case_sensitive_word_list);
257        Box::new(DictionarySubstringRule::from_dictionary(
258            case_sensitive_dict,
259        ))
260    }
261
262    fn create_backward_rule() -> Box<DictionarySubstringRule<WordListDictionary<ArrayWordList>>> {
263        let case_sensitive_word_list = create_from_read(read_word_list(), true, Some(SliceSort));
264        let case_sensitive_dict = WordListDictionary::new(case_sensitive_word_list);
265        Box::new(DictionarySubstringRule::new(case_sensitive_dict, true))
266    }
267
268    fn create_ignore_case_rule() -> Box<DictionarySubstringRule<WordListDictionary<ArrayWordList>>>
269    {
270        let case_insensitive_word_list = create_from_read(read_word_list(), false, Some(SliceSort));
271        let case_insensitive_dict = WordListDictionary::new(case_insensitive_word_list);
272        Box::new(DictionarySubstringRule::from_dictionary(
273            case_insensitive_dict,
274        ))
275    }
276    fn create_all_rule() -> Box<DictionarySubstringRule<WordListDictionary<ArrayWordList>>> {
277        let case_insensitive_word_list = create_from_read(read_word_list(), false, Some(SliceSort));
278        let case_insensitive_dict = WordListDictionary::new(case_insensitive_word_list);
279        Box::new(DictionarySubstringRule::new(case_insensitive_dict, true))
280    }
281    fn read_word_list() -> &'static [u8] {
282        include_bytes!("../../resources/test/web2-gt3")
283    }
284}