1#![forbid(unsafe_code)]
87
88pub mod error;
89
90use crate::error::{PasswordRulesError, PasswordRulesErrorContext};
91use nom::error::FromExternalError;
92use nom::{
93 self,
94 branch::alt,
95 bytes::complete::{is_not, tag_no_case as nom_tag},
96 character::complete::{char, digit1, multispace0},
97 combinator::{complete, cut, map, map_res, opt, peek, recognize, value},
98 error::ParseError,
99 sequence::{delimited, tuple},
100 IResult,
101};
102use once_cell::sync::Lazy;
103use std::collections::{BTreeMap, BTreeSet};
104use std::{cmp::max, cmp::min, ops::RangeInclusive};
105
106const ASCII_RANGE: RangeInclusive<char> = ' '..='~';
109const UPPER_RANGE: RangeInclusive<char> = 'A'..='Z';
110const LOWER_RANGE: RangeInclusive<char> = 'a'..='z';
111const DIGIT_RANGE: RangeInclusive<char> = '0'..='9';
112const SPECIAL_CHARS: &str = " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
113
114#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
116pub enum CharacterClass {
117 Upper,
119 Lower,
121 Digit,
123 Special,
125 AsciiPrintable,
127 Unicode,
129 Custom(Vec<char>),
132}
133
134impl CharacterClass {
135 pub fn chars(&self) -> Vec<char> {
137 use CharacterClass::*;
138
139 match self {
140 Upper => UPPER_RANGE.collect(),
141 Lower => LOWER_RANGE.collect(),
142 Digit => DIGIT_RANGE.collect(),
143 Special => SPECIAL_CHARS.chars().collect(),
144 AsciiPrintable | Unicode => ASCII_RANGE.collect(),
148 Custom(custom) => custom.clone(),
149 }
150 }
151}
152
153#[derive(Debug, Clone, Default, PartialEq, Eq)]
155pub struct PasswordRules {
156 pub max_consecutive: Option<u32>,
158 pub min_length: Option<u32>,
160 pub max_length: Option<u32>,
162 pub allowed: Vec<CharacterClass>,
164 pub required: Vec<Vec<CharacterClass>>,
166}
167
168#[derive(Debug, Clone)]
171enum PasswordRule {
172 Allow(Vec<CharacterClass>),
173 Require(Vec<CharacterClass>),
174 MinLength(Option<u32>),
175 MaxLength(Option<u32>),
176 MaxConsecutive(Option<u32>),
177}
178
179impl PasswordRules {
180 pub fn is_subset(&self, other: &PasswordRules) -> bool {
182 if let Some(max_consecutive) = other.max_consecutive {
183 if self.max_consecutive.map(|x| x <= max_consecutive) != Some(true) {
184 return false;
185 }
186 }
187
188 if let Some(min_length) = other.min_length {
189 if self.min_length.map(|x| x >= min_length) != Some(true) {
190 return false;
191 }
192 }
193
194 if let Some(max_length) = other.max_length {
195 if self.max_length.map(|x| x <= max_length) != Some(true) {
196 return false;
197 }
198 }
199
200 if !satisfies_allowed(self, other) {
201 return false;
202 }
203
204 satisfies_required(self.required.clone(), other.required.clone())
205 }
206}
207
208fn satisfies_allowed(a: &PasswordRules, b: &PasswordRules) -> bool {
209 let b_allowed = b
210 .required
211 .iter()
212 .flatten()
213 .chain(b.allowed.iter())
214 .flat_map(|class| class.chars().into_iter())
215 .collect::<BTreeSet<char>>();
216
217 a.required
218 .iter()
219 .flatten()
220 .chain(a.allowed.iter())
221 .map(CharacterSet::from)
222 .all(|set| set.is_subset(&b_allowed))
223}
224
225fn satisfies_required(mut a: Vec<Vec<CharacterClass>>, mut b: Vec<Vec<CharacterClass>>) -> bool {
226 fn presort_by_length(sets: &mut [Vec<CharacterClass>]) {
228 for set in sets.iter_mut() {
229 set.sort_by_key(|class| CharacterSet::from(class).len());
230 }
231
232 sets.sort_by_key(|set| {
233 (
234 set.len(),
235 set.last().map(|class| CharacterSet::from(class).len()),
236 )
237 });
238 }
239
240 presort_by_length(&mut a);
241 presort_by_length(&mut b);
242
243 a.iter().for_each(|ra| {
246 if let Some(i) = b.iter().enumerate().find_map(|(i, rb)| {
247 ra.iter()
248 .all(|x_class| {
249 rb.iter().any(|y_class| {
250 let x_set = CharacterSet::from(x_class);
251 let y_set = CharacterSet::from(y_class);
252 x_set.is_subset(&y_set)
253 })
254 })
255 .then_some(i)
256 }) {
257 b.remove(i);
258 }
259 });
260
261 b.is_empty()
262}
263
264enum CharacterSet<'a> {
265 Static(&'a BTreeSet<char>),
266 Dynamic(BTreeSet<char>),
267}
268
269impl std::ops::Deref for CharacterSet<'_> {
270 type Target = BTreeSet<char>;
271
272 fn deref(&self) -> &Self::Target {
273 match self {
274 CharacterSet::Static(a) => a,
275 CharacterSet::Dynamic(a) => a,
276 }
277 }
278}
279
280impl<'a> From<&'a CharacterClass> for CharacterSet<'a> {
281 fn from(class: &'a CharacterClass) -> Self {
282 static STATIC_CHARACTER_SETS: Lazy<BTreeMap<CharacterClass, BTreeSet<char>>> =
283 Lazy::new(|| {
284 [
285 CharacterClass::Upper,
286 CharacterClass::Lower,
287 CharacterClass::Digit,
288 CharacterClass::Special,
289 CharacterClass::AsciiPrintable,
290 CharacterClass::Unicode,
291 ]
292 .iter()
293 .map(|c| (c.clone(), c.chars().into_iter().collect::<BTreeSet<_>>()))
294 .collect()
295 });
296
297 match class {
298 CharacterClass::Custom(_) => CharacterSet::Dynamic(class.chars().into_iter().collect()),
299 _ => CharacterSet::Static(STATIC_CHARACTER_SETS.get(class).unwrap()),
300 }
301 }
302}
303
304fn space_surround<'a, P, O, E>(parser: P) -> impl FnMut(&'a str) -> IResult<&'a str, O, E>
310where
311 P: FnMut(&'a str) -> IResult<&'a str, O, E>,
312 E: ParseError<&'a str>,
313{
314 delimited(multispace0, parser, multispace0)
315}
316
317fn fold_separated_terminated<P, S, R, F, T, I, O, O2, O3, E>(
331 mut parser: P,
332 mut sep: S,
333 terminator: R,
334 allow_trailing_separator: bool,
335 init: T,
336 mut fold: F,
337) -> impl FnMut(I) -> IResult<I, T, E>
338where
339 P: FnMut(I) -> IResult<I, O, E>,
340 S: FnMut(I) -> IResult<I, O2, E>,
341 R: FnMut(I) -> IResult<I, O3, E>,
342 I: Clone,
343 T: Clone,
344 F: FnMut(T, O) -> T,
345 E: ParseError<I>,
346{
347 let mut terminator_lookahead = peek(terminator);
348
349 move |mut input| {
350 let mut accum = init.clone();
351
352 let (fold_err, tail) = match parser(input.clone()) {
354 Err(nom::Err::Error(err)) => (err, input),
355 Err(err) => return Err(err),
356 Ok((tail, output)) => {
357 accum = fold(accum, output);
358 input = tail;
359
360 loop {
362 match sep(input.clone()) {
364 Err(nom::Err::Error(err)) => break (err, input),
365 Err(err) => return Err(err),
366 Ok((tail, _)) => input = tail,
367 }
368
369 match parser(input.clone()) {
372 Err(err) if !allow_trailing_separator => return Err(err),
373 Err(nom::Err::Error(err)) => break (err, input),
374 Err(err) => return Err(err),
375 Ok((tail, output)) => {
376 accum = fold(accum, output);
377 input = tail;
378 }
379 }
380 }
381 }
382 };
383
384 match terminator_lookahead(tail.clone()) {
386 Ok(..) => Ok((tail, accum)),
387 Err(err) => Err(err.map(move |err| fold_err.or(err))),
388 }
389 }
390}
391
392fn opt_terminated<P, R, I, O, O2, E>(
400 mut parser: P,
401 terminator: R,
402) -> impl FnMut(I) -> IResult<I, Option<O>, E>
403where
404 P: FnMut(I) -> IResult<I, O, E>,
405 R: FnMut(I) -> IResult<I, O2, E>,
406 E: ParseError<I>,
407 I: Clone,
408{
409 let mut terminator_lookahead = peek(terminator);
410
411 move |input| match parser(input.clone()) {
412 Ok((tail, value)) => Ok((tail, Some(value))),
413 Err(nom::Err::Error(opt_err)) => match terminator_lookahead(input.clone()) {
414 Ok(..) => Ok((input, None)),
415 Err(err) => Err(err.map(move |err| opt_err.or(err))),
416 },
417 Err(err) => Err(err),
418 }
419}
420
421fn eof<'a, E>(input: &'a str) -> IResult<&'a str, (), E>
423where
424 E: ParseError<&'a str>,
425{
426 if input.is_empty() {
427 Ok((input, ()))
428 } else {
429 Err(nom::Err::Error(E::from_error_kind(
430 input,
431 nom::error::ErrorKind::Eof,
432 )))
433 }
434}
435
436fn tag_no_case<'a, E>(tag: &'static str) -> impl FnMut(&'a str) -> IResult<&'a str, &'a str, E>
439where
440 E: error::WithTagError<&'a str>,
441{
442 let nom_tag = nom_tag(tag);
443 move |input| match nom_tag(input) {
444 Ok(result) => Ok(result),
445 Err(err) => Err(err.map(|()| E::from_tag(input, tag))),
446 }
447}
448
449fn parse_custom_character_class<'a>(
455 input: &'a str,
456) -> IResult<&'a str, Vec<char>, PasswordRulesErrorContext<'a>> {
457 let opt_dash = opt(char('-'));
459
460 let inner = opt(is_not("-]"));
463
464 let opt_bracket = |input: &'a str| {
467 if input.starts_with("]]") {
468 Ok((&input[1..], Some(']')))
469 } else {
470 Ok((input, None))
471 }
472 };
473
474 let body = recognize(tuple((opt_dash, inner, opt_bracket)));
476
477 let body = map(body, |s| {
480 s.chars()
481 .filter(|&c| c.is_ascii_graphic() || c == ' ')
482 .collect()
483 });
484
485 delimited(char('['), body, cut(char(']')))(input)
486}
487
488fn parse_character_class(input: &str) -> IResult<&str, CharacterClass, PasswordRulesErrorContext> {
490 alt((
491 value(CharacterClass::Upper, tag_no_case("upper")),
492 value(CharacterClass::Lower, tag_no_case("lower")),
493 value(CharacterClass::Digit, tag_no_case("digit")),
494 value(CharacterClass::Special, tag_no_case("special")),
495 value(
496 CharacterClass::AsciiPrintable,
497 tag_no_case("ascii-printable"),
498 ),
499 value(CharacterClass::Unicode, tag_no_case("unicode")),
500 map(parse_custom_character_class, CharacterClass::Custom),
501 ))(input)
502}
503
504fn parse_character_classes(
506 input: &str,
507) -> IResult<&str, Vec<CharacterClass>, PasswordRulesErrorContext> {
508 let comma = space_surround(char(','));
509
510 let terminator = space_surround(alt((value((), char(';')), eof)));
513 fold_separated_terminated(
514 parse_character_class,
515 comma,
516 terminator,
517 false,
518 Vec::new(),
519 |mut classes, class| {
520 classes.push(class);
521 classes
522 },
523 )(input)
524}
525
526fn parse_number<'a, E>(input: &'a str) -> IResult<&'a str, u32, E>
528where
529 E: ParseError<&'a str> + FromExternalError<&'a str, std::num::ParseIntError>,
530{
531 map_res(digit1, str::parse)(input)
532}
533
534fn parse_generic_rule<'a, P, O>(
539 name: &'static str,
540 parser: P,
541) -> impl FnMut(&'a str) -> IResult<&'a str, O, PasswordRulesErrorContext<'a>>
542where
543 P: Fn(&'a str) -> IResult<&'a str, O, PasswordRulesErrorContext<'a>>,
544{
545 let colon = space_surround(char(':'));
546 let pattern = tuple((tag_no_case(name), cut(colon), cut(parser)));
547 map(pattern, |(_, _, out)| out)
548}
549
550fn parse_optional_rule_number<'a, E>(input: &'a str) -> IResult<&'a str, Option<u32>, E>
554where
555 E: ParseError<&'a str> + FromExternalError<&'a str, std::num::ParseIntError>,
556{
557 opt_terminated(
558 parse_number,
559 space_surround(alt((value((), char(';')), eof))),
560 )(input)
561}
562
563fn parse_rule(input: &str) -> IResult<&str, PasswordRule, PasswordRulesErrorContext> {
567 alt((
568 map(
569 parse_generic_rule("required", parse_character_classes),
570 PasswordRule::Require,
571 ),
572 map(
573 parse_generic_rule("allowed", parse_character_classes),
574 PasswordRule::Allow,
575 ),
576 map(
577 parse_generic_rule("max-consecutive", parse_optional_rule_number),
578 PasswordRule::MaxConsecutive,
579 ),
580 map(
581 parse_generic_rule("minlength", parse_optional_rule_number),
582 PasswordRule::MinLength,
583 ),
584 map(
585 parse_generic_rule("maxlength", parse_optional_rule_number),
586 PasswordRule::MaxLength,
587 ),
588 ))(input)
589}
590
591fn apply<T>(source: &mut Option<T>, new: T, mut cmp: impl FnMut(T, T) -> T) {
595 *source = match source.take() {
596 None => Some(new),
597 Some(old) => Some(cmp(old, new)),
598 }
599}
600
601fn parse_rule_list(input: &str) -> IResult<&str, PasswordRules, PasswordRulesErrorContext> {
606 fold_separated_terminated(
607 parse_rule,
608 space_surround(char(';')),
609 space_surround(eof),
610 true,
611 PasswordRules::default(),
612 |mut rules, rule| {
613 match rule {
614 PasswordRule::Allow(classes) => rules.allowed.extend(classes),
615 PasswordRule::Require(classes) => {
616 let classes = canonicalize(classes);
617 if !classes.is_empty() {
618 rules.required.push(canonicalize(classes));
619 }
620 }
621 PasswordRule::MinLength(Some(length)) => apply(&mut rules.min_length, length, max),
622 PasswordRule::MaxLength(Some(length)) => apply(&mut rules.max_length, length, min),
623 PasswordRule::MaxConsecutive(Some(length)) => {
624 apply(&mut rules.max_consecutive, length, min)
625 }
626 _ => {}
627 };
628 rules
629 },
630 )(input)
631}
632
633pub fn parse_password_rules(
643 s: &str,
644 supply_default: bool,
645) -> Result<PasswordRules, PasswordRulesError> {
646 let s = s.trim();
647
648 if s.is_empty() {
649 return Err(PasswordRulesError::empty());
650 }
651
652 let mut parse_rules = complete(parse_rule_list);
653
654 let mut rules = match parse_rules(s) {
655 Ok((_, rules)) => rules,
656 Err(nom::Err::Incomplete(..)) => unreachable!(),
657 Err(nom::Err::Error(err)) | Err(nom::Err::Failure(err)) => {
658 return Err(err.extract_context(s))
659 }
660 };
661
662 rules.allowed = canonicalize(rules.allowed);
663
664 if supply_default && rules.allowed.is_empty() && rules.required.is_empty() {
666 rules.allowed.push(CharacterClass::AsciiPrintable);
667 }
668
669 Ok(rules)
670}
671
672struct AsciiTable {
674 table: [bool; 128],
675}
676
677#[derive(Debug, Clone)]
678enum CheckResult<I> {
679 Match,
680 Mismatch(I),
681}
682
683impl AsciiTable {
684 fn new() -> Self {
685 Self {
686 table: [false; 128],
687 }
688 }
689
690 fn set(&mut self, b: char) {
692 self.table[b as usize] = true;
693 }
694
695 fn set_range(&mut self, range: impl IntoIterator<Item = char>) {
697 range.into_iter().for_each(|c| self.set(c))
698 }
699
700 fn check(&self, b: char) -> bool {
702 self.table.get(b as usize).copied().unwrap_or(false)
703 }
704
705 fn check_range(&self, range: impl IntoIterator<Item = char>) -> bool {
707 range.into_iter().all(move |b| self.check(b))
708 }
709
710 fn check_or_extract<'s>(
713 &'s self,
714 range: impl IntoIterator<Item = char> + Clone + 's,
715 ) -> CheckResult<impl Iterator<Item = char> + 's> {
716 if self.check_range(range.clone()) {
717 CheckResult::Match
718 } else {
719 CheckResult::Mismatch(range.into_iter().filter(move |&b| self.check(b)))
720 }
721 }
722}
723
724fn canonicalize(mut classes: Vec<CharacterClass>) -> Vec<CharacterClass> {
726 let mut table = AsciiTable::new();
728
729 if classes.contains(&CharacterClass::Unicode) {
732 return vec![CharacterClass::Unicode];
733 }
734
735 if classes.contains(&CharacterClass::AsciiPrintable) {
736 return vec![CharacterClass::AsciiPrintable];
737 }
738
739 for class in classes.drain(..) {
741 match class {
742 CharacterClass::Upper => table.set_range(UPPER_RANGE),
743 CharacterClass::Lower => table.set_range(LOWER_RANGE),
744 CharacterClass::Digit => table.set_range(DIGIT_RANGE),
745 CharacterClass::Special => table.set_range(SPECIAL_CHARS.chars()),
746 CharacterClass::Custom(chars) => table.set_range(chars.into_iter()),
747 _ => unreachable!(), }
749 }
750
751 let mut custom_characters = vec![];
753
754 if let CheckResult::Match = table.check_or_extract(ASCII_RANGE) {
755 return vec![CharacterClass::AsciiPrintable];
756 }
757
758 match table.check_or_extract(UPPER_RANGE) {
759 CheckResult::Match => classes.push(CharacterClass::Upper),
760 CheckResult::Mismatch(chars) => custom_characters.extend(chars),
761 }
762
763 match table.check_or_extract(LOWER_RANGE) {
764 CheckResult::Match => classes.push(CharacterClass::Lower),
765 CheckResult::Mismatch(chars) => custom_characters.extend(chars),
766 }
767
768 match table.check_or_extract(DIGIT_RANGE) {
769 CheckResult::Match => classes.push(CharacterClass::Digit),
770 CheckResult::Mismatch(chars) => custom_characters.extend(chars),
771 }
772
773 match table.check_or_extract(SPECIAL_CHARS.chars()) {
774 CheckResult::Match => classes.push(CharacterClass::Special),
775 CheckResult::Mismatch(chars) => custom_characters.extend(chars),
776 }
777
778 if !custom_characters.is_empty() {
779 classes.push(CharacterClass::Custom(custom_characters));
780 }
781
782 classes
783}
784
785#[cfg(test)]
786mod test {
787 use super::*;
788
789 mod satisfies_required {
790 use super::*;
791
792 #[test]
793 fn test_satisfies_required() {
794 assert!(satisfies_required(
795 vec![vec![CharacterClass::Digit]],
796 vec![vec![
797 CharacterClass::Digit,
798 CharacterClass::Upper,
799 CharacterClass::Lower,
800 ]],
801 ),);
802
803 assert!(!satisfies_required(
804 vec![vec![
805 CharacterClass::Digit,
806 CharacterClass::Upper,
807 CharacterClass::Lower,
808 ]],
809 vec![vec![CharacterClass::Digit]],
810 ),);
811
812 assert!(satisfies_required(
813 vec![vec![CharacterClass::Digit], vec![CharacterClass::Digit],],
814 vec![
815 vec![CharacterClass::Digit],
816 vec![CharacterClass::Digit, CharacterClass::Upper],
817 ],
818 ),);
819
820 assert!(!satisfies_required(
821 vec![
822 vec![CharacterClass::Custom(vec!['[', '#', '!', '*'])],
823 vec![CharacterClass::Digit],
824 ],
825 vec![
826 vec![CharacterClass::Custom(vec!['#', '!'])],
827 vec![CharacterClass::Digit, CharacterClass::Upper],
828 ],
829 ),);
830
831 assert!(satisfies_required(
832 vec![
833 vec![CharacterClass::Digit, CharacterClass::Upper],
834 vec![CharacterClass::Digit],
835 ],
836 vec![vec![CharacterClass::Digit],],
837 ),);
838
839 assert!(!satisfies_required(
840 vec![vec![CharacterClass::Digit],],
841 vec![
842 vec![CharacterClass::Digit, CharacterClass::Upper],
843 vec![CharacterClass::Digit],
844 ],
845 ),);
846
847 assert!(satisfies_required(
848 vec![
849 vec![CharacterClass::Custom(vec!['[', '#', '!', '*', '^', '%'])],
850 vec![CharacterClass::Custom(vec!['[', '#', '!', '*'])],
851 ],
852 vec![vec![CharacterClass::Custom(vec!['[', '#', '!', '*'])],],
853 ),);
854
855 assert!(satisfies_required(
856 vec![vec![CharacterClass::Upper], vec![CharacterClass::Digit],],
857 vec![
858 vec![
859 CharacterClass::Digit,
860 CharacterClass::Lower,
861 CharacterClass::Upper
862 ],
863 vec![CharacterClass::Digit, CharacterClass::Lower],
864 ],
865 ),);
866 }
867 }
868
869 mod canonicalize {
870 use super::*;
871
872 fn test_canonicalizer(input: &str, expected: Vec<CharacterClass>) {
873 let input = input.chars().collect();
874 let classes = vec![CharacterClass::Custom(input)];
875 let res = canonicalize(classes);
876
877 assert_eq!(res, expected)
878 }
879
880 #[test]
881 fn few_characters() {
882 test_canonicalizer("abc", vec![CharacterClass::Custom(vec!['a', 'b', 'c'])])
883 }
884
885 #[test]
886 fn all_lower() {
887 test_canonicalizer("abcdefghijklmnopqrstuvwxyz", vec![CharacterClass::Lower])
888 }
889
890 #[test]
891 fn all_alpha() {
892 test_canonicalizer(
893 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
894 vec![CharacterClass::Upper, CharacterClass::Lower],
895 )
896 }
897
898 #[test]
899 fn all_special() {
900 test_canonicalizer(
901 r##"- !"#$%&'()*+,./:;<=>?@[\^_`{|}~]"##,
902 vec![CharacterClass::Special],
903 )
904 }
905
906 #[test]
907 fn digits_and_some_lowers() {
908 test_canonicalizer(
909 "67abc1def0ghijk2ln8op9qr4stuv5wxy3z",
910 vec![
911 CharacterClass::Digit,
912 CharacterClass::Custom("abcdefghijklnopqrstuvwxyz".chars().collect()),
913 ],
914 )
915 }
916
917 #[test]
918 fn alphanumeric_and_some_specials() {
919 test_canonicalizer(
920 "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ()*+.",
921 vec![
922 CharacterClass::Upper,
923 CharacterClass::Lower,
924 CharacterClass::Digit,
925 CharacterClass::Custom(vec!['(', ')', '*', '+', '.']),
926 ],
927 )
928 }
929
930 #[test]
931 fn everything() {
932 test_canonicalizer(
933 r##"-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!"#$%&'()*+,./:;<=>?@[\^_`{|}~ ]"##,
934 vec![CharacterClass::AsciiPrintable],
935 )
936 }
937 }
938
939 mod parser {
940 use super::*;
941
942 macro_rules! tests {
964 ($($test:ident $($doc:literal)? : $input:literal => $expected:expr;)*) => {$(
965 $(#[doc = $doc])?
966 #[test]
967 fn $test() {
968 assert_eq!(
969 parse_password_rules($input, true).ok(),
970 $expected,
971
972 "{doc}\ninput: {input:?}",
973 doc = None $(.or(Some($doc)))? .unwrap_or(""),
974 input = $input
975 );
976 }
977 )*};
978 }
979
980 macro_rules! password_rules {
996 (
997 $(max-consecutive: $consecutive:expr;)?
998 $(maxlength: $maxlength:expr;)?
999 $(minlength: $minlength:expr;)?
1000 $(allowed: $($allowed_class:tt),* $(,)? ;)?
1001 $(required: $($required_class:tt),* $(,)? ;)*
1002 ) => {
1003 Some(PasswordRules{
1004 max_consecutive: None $(.or(Some($consecutive)))?,
1005 max_length: None $(.or(Some($maxlength)))?,
1006 min_length: None $(.or(Some($minlength)))?,
1007 allowed: vec![$($(
1008 character_class!($allowed_class),
1009 )*)?],
1010 required: vec![$(
1011 vec![$(
1012 character_class!($required_class),
1013 )*],
1014 )*],
1015 })
1016 }
1017 }
1018
1019 macro_rules! character_class {
1021 (upper) => {CharacterClass::Upper};
1022 (lower) => {CharacterClass::Lower};
1023 (digit) => {CharacterClass::Digit};
1024 (special) => {CharacterClass::Special};
1025 (ascii) => {CharacterClass::AsciiPrintable};
1026 (unicode) => {CharacterClass::Unicode};
1027 ([$($c:expr)*]) => {CharacterClass::Custom(vec![$($c,)*])};
1028 }
1029
1030 tests! {
1031 empty_string: "" => None;
1032
1033 missing_property: "allowed:;" => password_rules!(allowed: ascii;);
1034
1035 multiple_classes: "allowed: digit, special;" => password_rules!(
1036 allowed: digit, special;
1037 );
1038
1039 missing_integers: "max-consecutive:;minlength:;maxlength:;" => password_rules!(
1040 allowed: ascii;
1041 );
1042
1043 empty_custom_class: "allowed:[];" => password_rules!(allowed: ascii;);
1044
1045 multiple_length_constraints:
1046 "maxlength:50;\
1047 max-consecutive:40;\
1048 minlength:10;\
1049 max-consecutive:30;\
1050 minlength:12;\
1051 maxlength:20;"
1052 =>
1053 password_rules!(
1054 max-consecutive: 30;
1055 maxlength: 20;
1056 minlength: 12;
1057 allowed: ascii;
1058 );
1059
1060 custom_class_bracket_hyphen: "allowed: [-]]; required: [[]]; allowed:[-];" => password_rules!(
1061 allowed: ['-' ']'];
1062 required: ['[' ']'];
1063 );
1064
1065 invalid_hyphen: "allowed: [a-];" => None;
1066
1067 invalid_bracket: "allowed: []a];" => None;
1068
1069 complex_input:
1070 "allowed:special;\
1071 max-consecutive:3;\
1072 required: upper, digit, ['*/];\
1073 allowed: [abc], digit,special;\
1074 minlength:20;"
1075 =>
1076 password_rules!(
1077 max-consecutive: 3;
1078 minlength: 20;
1079 allowed: digit, special, ['a' 'b' 'c'];
1080 required: upper, digit, ['\'' '*' '/'];
1081 );
1082
1083 skip_unicode_characters: "allowed: [供应A商B责任C进展];" => password_rules!(
1084 allowed: ['A' 'B' 'C'];
1085 );
1086
1087 unicode_overpowers_everything:
1088 "allowed: \
1089 [abcdefghijklmnopqrstuvwxyz], \
1090 upper, digit, ascii-printable, \
1091 special, unicode;"
1092 =>
1093 password_rules!(allowed: unicode;);
1094
1095 ascii_overpowers_everything_else:
1096 "allowed: lower; \
1097 allowed: [ABCDEFGHIJKLMNOPQRSTUVWXYZ]; \
1098 allowed: special; \
1099 allowed: [0123456789]; \
1100 allowed: ascii-printable;"
1101 =>
1102 password_rules!(allowed: ascii;);
1103
1104
1105 allow_missing_trailing_semicolon: "allowed: lower; required: upper" => password_rules!(
1106 allowed: lower;
1107 required: upper;
1108 );
1109
1110 multiple_required_sets:
1111 "required: upper, lower; required: digit; allowed: digit; allowed: upper;"
1112 =>
1113 password_rules!(
1114 allowed: upper, digit;
1115 required: upper, lower;
1116 required: digit;
1117 );
1118 }
1119
1120 mod apple_suite {
1127 use super::*;
1128
1129 tests! {
1130 empty_string: "" => None;
1131
1132 req_upper1: " required: upper" => password_rules!(required: upper;);
1133 req_upper2: " required: upper;" => password_rules!(required: upper;);
1134 req_upper3: " required: upper " => password_rules!(required: upper;);
1135 req_upper4: "required:upper" => password_rules!(required: upper;);
1136 req_upper6: "required: upper" => password_rules!(required: upper;);
1137
1138 req_upper_case "Test that character class names are case insensitive":
1139 "required: uPPeR" => password_rules!(required: upper;);
1140
1141 all_upper1: "allowed:upper" => password_rules!(allowed: upper;);
1142 all_upper2: "allowed: upper" => password_rules!(allowed: upper;);
1143
1144 required_canonical "Test that a custom character set that overlaps a class is omitted":
1145 "required: upper, [AZ];" => password_rules!(required: upper;);
1146
1147 allowed_reduction "Test that multiple allowed rules are collapsed together":
1148 "required: upper; allowed: upper; allowed: lower" => password_rules!(
1149 allowed: upper, lower;
1150 required: upper;
1151 );
1152
1153 max_consecutive1: "max-consecutive: 5" => password_rules!(
1154 max-consecutive: 5;
1155 allowed: ascii;
1156 );
1157 max_consecutive2: "max-consecutive:5" => password_rules!(
1158 max-consecutive: 5;
1159 allowed: ascii;
1160 );
1161 max_consecutive3: " max-consecutive:5" => password_rules!(
1162 max-consecutive: 5;
1163 allowed: ascii;
1164 );
1165
1166
1167 max_consecutive_min1 "Test that the lowest number wins for multiple max-consecutive":
1168 "max-consecutive: 5; max-consecutive: 3" => password_rules!(
1169 max-consecutive: 3;
1170 allowed: ascii;
1171 );
1172 max_consecutive_min2 "Test that the lowest number wins for multiple max-consecutive":
1173 "max-consecutive: 3; max-consecutive: 5" => password_rules!(
1174 max-consecutive: 3;
1175 allowed: ascii;
1176 );
1177 max_consecutive_min3 "Test that the lowest number wins for multiple max-consecutive":
1178 "max-consecutive: 3; max-consecutive: 1; max-consecutive: 5" => password_rules!(
1179 max-consecutive: 1;
1180 allowed: ascii;
1181 );
1182 max_consecutive_min4 "Test that the lowest number wins for multiple max-consecutive":
1183 "required: ascii-printable; max-consecutive: 5; max-consecutive: 3" => password_rules!(
1184 max-consecutive: 3;
1185 required: ascii;
1186 );
1187
1188 require_allow1: "required: [*&^]; allowed: upper" => password_rules!(
1189 allowed: upper;
1190 required: ['&' '*' '^'];
1191 );
1192 require_allow2: "required: [*&^ABC]; allowed: upper" => password_rules!(
1193 allowed: upper;
1194 required: ['A' 'B' 'C' '&' '*' '^'];
1195 );
1196 required_allow3: "required: unicode; required: digit" => password_rules!(
1197 required: unicode;
1198 required: digit;
1199 );
1200
1201 require_empty "Test that an empty required set is ignored":
1202 "required: ; required: upper" => password_rules!(
1203 required: upper;
1204 );
1205
1206 custom_unicode_dropped1 "Test that unicode characters in custom classes are ignored":
1207 "allowed: [供应商责任进展]" => password_rules!(
1208 allowed: ascii;
1209 );
1210 custom_unicode_dropped2 "Test that unicode characters in custom classes are ignored":
1211 "allowed: [供应A商B责任C进展]" => password_rules!(
1212 allowed: ['A' 'B' 'C'];
1213 );
1214
1215 collapse_allow1 "Test that several allow rules are collapsed together":
1216 "required: upper; allowed: upper; allowed: lower; minlength: 12; maxlength: 73;" =>
1217 password_rules!(
1218 maxlength: 73;
1219 minlength: 12;
1220 allowed: upper, lower;
1221 required: upper;
1222 );
1223 collapse_allow2 "Test that several allow rules are collapsed together":
1224 "required: upper; allowed: upper; allowed: lower; maxlength: 73; minlength: 12;" =>
1225 password_rules!(
1226 maxlength: 73;
1227 minlength: 12;
1228 allowed: upper, lower;
1229 required: upper;
1230 );
1231 collapse_allow3 "Test that several allow rules are collapsed together":
1232 "required: upper; allowed: upper; allowed: lower; maxlength: 73" => password_rules!(
1233 maxlength: 73;
1234 allowed: upper, lower;
1235 required: upper;
1236 );
1237 collapse_allow4 "Test that several allow rules are collapsed together":
1238 "required: upper; allowed: upper; allowed: lower; minlength: 12;" => password_rules!(
1239 minlength: 12;
1240 allowed: upper, lower;
1241 required: upper;
1242 );
1243
1244 minlength_max1 "Test that the largest number wins for multiple minlength":
1245 "minlength: 12; minlength: 7; minlength: 23" => password_rules!(
1246 minlength: 23;
1247 allowed: ascii;
1248 );
1249 minlength_max2 "Test that the largest number wins for multiple minlength":
1250 "minlength: 12; maxlength: 17; minlength: 10" => password_rules!(
1251 maxlength: 17;
1252 minlength: 12;
1253 allowed: ascii;
1254 );
1255
1256 bad_syntax1: "allowed: upper,," => None;
1257 bad_syntax2: "allowed: upper,;" => None;
1258 bad_syntax3: "allowed: upper [a]" => None;
1259 bad_syntax4: "dummy: upper" => None;
1260 bad_syntax5: "upper: lower" => None;
1261 bad_syntax6: "max-consecutive: [ABC]" => None;
1262 bad_syntax7: "max-consecutive: upper" => None;
1263 bad_syntax8: "max-consecutive: 1+1" => None;
1264 bad_syntax9: "max-consecutive: 供" => None;
1265 bad_syntax10: "required: 1" => None;
1266 bad_syntax11: "required: 1+1" => None;
1267 bad_syntax12: "required: 供" => None;
1268 bad_syntax13: "required: A" => None;
1269 bad_syntax14: "required: required: upper" => None;
1270 bad_syntax15: "allowed: 1" => None;
1271 bad_syntax16: "allowed: 1+1" => None;
1272 bad_syntax17: "allowed: 供" => None;
1273 bad_syntax18: "allowed: A" => None;
1274 bad_syntax19: "allowed: allowed: upper" => None;
1275
1276 custom_class1
1277 "Test that a - and ] are only accepted as the first and last characters in a class":
1278 "required: digit ; required: [-]];" =>
1279 password_rules!(
1280 required: digit;
1281 required: ['-' ']'];
1282 );
1283 custom_class2
1284 "Test that a - and ] are only accepted as the first and last characters in a class":
1285 "required: digit ; required: [-ABC]];" =>
1286 password_rules!(
1287 required: digit;
1288 required: ['A' 'B' 'C' '-' ']'];
1289 );
1290 custom_class3
1291 "Test that a - and ] are only accepted as the first and last characters in a class":
1292 "required: digit ; required: [-];" =>
1293 password_rules!(
1294 required: digit;
1295 required: ['-'];
1296 );
1297 custom_class4
1298 "Test that a - and ] are only accepted as the first and last characters in a class":
1299 "required: digit ; required: []];" =>
1300 password_rules!(
1301 required: digit;
1302 required: [']'];
1303 );
1304
1305 bad_custom_class1 "Test that a hyphen is only accepted as the first character in a class":
1306 "required: digit ; required: [a-];" => None;
1307 bad_custom_class2 "Test that a hyphen is only accepted as the first character in a class":
1308 "required: digit ; required: []-];" => None;
1309 bad_custom_class3 "Test that a hyphen is only accepted as the first character in a class":
1310 "required: digit ; required: [--];" => None;
1311 bad_custom_class4 "Test that a hyphen is only accepted as the first character in a class":
1312 "required: digit ; required: [-a--------];" => None;
1313 bad_custom_class5 "Test that a hyphen is only accepted as the first character in a class":
1314 "required: digit ; required: [-a--------] ];" => None;
1315
1316 canonical1 "Test that a custom character class is converted into a named class":
1317 "required: [abcdefghijklmnopqrstuvwxyz]" => password_rules!(
1318 required: lower;
1319 );
1320 canonical2 "Test that a custom character class is converted into a named class":
1321 "required: [abcdefghijklmnopqrstuvwxy]" => password_rules!(
1322 required: [
1323 'a''b''c''d''e''f''g''h''i''j''k''l''m''n''o''p''q''r''s''t''u''v''w''x''y'
1324 ];
1325 );
1326 }
1327 }
1328 }
1329}