#![forbid(unsafe_code)]
pub mod error;
use crate::error::{PasswordRulesError, PasswordRulesErrorContext};
use nom::error::FromExternalError;
use nom::{
self,
branch::alt,
bytes::complete::{is_not, tag_no_case as nom_tag},
character::complete::{char, digit1, multispace0},
combinator::{complete, cut, map, map_res, opt, peek, recognize, value},
error::ParseError,
sequence::{delimited, tuple},
IResult,
};
use std::{cmp::max, cmp::min, ops::RangeInclusive};
const ASCII_RANGE: RangeInclusive<char> = ' '..='~';
const UPPER_RANGE: RangeInclusive<char> = 'A'..='Z';
const LOWER_RANGE: RangeInclusive<char> = 'a'..='z';
const DIGIT_RANGE: RangeInclusive<char> = '0'..='9';
const SPECIAL_CHARS: &str = " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum CharacterClass {
Upper,
Lower,
Digit,
Special,
AsciiPrintable,
Unicode,
Custom(Vec<char>),
}
impl CharacterClass {
pub fn chars(&self) -> Vec<char> {
use CharacterClass::*;
match self {
Upper => UPPER_RANGE.collect(),
Lower => LOWER_RANGE.collect(),
Digit => DIGIT_RANGE.collect(),
Special => SPECIAL_CHARS.chars().collect(),
AsciiPrintable | Unicode => ASCII_RANGE.collect(),
Custom(custom) => custom.clone(),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PasswordRules {
pub max_consecutive: Option<u32>,
pub min_length: Option<u32>,
pub max_length: Option<u32>,
pub allowed: Vec<CharacterClass>,
pub required: Vec<Vec<CharacterClass>>,
}
#[derive(Debug, Clone)]
enum PasswordRule {
Allow(Vec<CharacterClass>),
Require(Vec<CharacterClass>),
MinLength(Option<u32>),
MaxLength(Option<u32>),
MaxConsecutive(Option<u32>),
}
fn space_surround<'a, P, O, E>(parser: P) -> impl FnMut(&'a str) -> IResult<&'a str, O, E>
where
P: FnMut(&'a str) -> IResult<&'a str, O, E>,
E: ParseError<&'a str>,
{
delimited(multispace0, parser, multispace0)
}
fn fold_separated_terminated<P, S, R, F, T, I, O, O2, O3, E>(
mut parser: P,
mut sep: S,
terminator: R,
allow_trailing_separator: bool,
init: T,
mut fold: F,
) -> impl FnMut(I) -> IResult<I, T, E>
where
P: FnMut(I) -> IResult<I, O, E>,
S: FnMut(I) -> IResult<I, O2, E>,
R: FnMut(I) -> IResult<I, O3, E>,
I: Clone,
T: Clone,
F: FnMut(T, O) -> T,
E: ParseError<I>,
{
let mut terminator_lookahead = peek(terminator);
move |mut input| {
let mut accum = init.clone();
let (fold_err, tail) = match parser(input.clone()) {
Err(nom::Err::Error(err)) => (err, input),
Err(err) => return Err(err),
Ok((tail, output)) => {
accum = fold(accum, output);
input = tail;
loop {
match sep(input.clone()) {
Err(nom::Err::Error(err)) => break (err, input),
Err(err) => return Err(err),
Ok((tail, _)) => input = tail,
}
match parser(input.clone()) {
Err(err) if !allow_trailing_separator => return Err(err),
Err(nom::Err::Error(err)) => break (err, input),
Err(err) => return Err(err),
Ok((tail, output)) => {
accum = fold(accum, output);
input = tail;
}
}
}
}
};
match terminator_lookahead(tail.clone()) {
Ok(..) => Ok((tail, accum)),
Err(err) => Err(err.map(move |err| fold_err.or(err))),
}
}
}
fn opt_terminated<P, R, I, O, O2, E>(
mut parser: P,
terminator: R,
) -> impl FnMut(I) -> IResult<I, Option<O>, E>
where
P: FnMut(I) -> IResult<I, O, E>,
R: FnMut(I) -> IResult<I, O2, E>,
E: ParseError<I>,
I: Clone,
{
let mut terminator_lookahead = peek(terminator);
move |input| match parser(input.clone()) {
Ok((tail, value)) => Ok((tail, Some(value))),
Err(nom::Err::Error(opt_err)) => match terminator_lookahead(input.clone()) {
Ok(..) => Ok((input, None)),
Err(err) => Err(err.map(move |err| opt_err.or(err))),
},
Err(err) => Err(err),
}
}
fn eof<'a, E>(input: &'a str) -> IResult<&'a str, (), E>
where
E: ParseError<&'a str>,
{
if input.is_empty() {
Ok((input, ()))
} else {
Err(nom::Err::Error(E::from_error_kind(
input,
nom::error::ErrorKind::Eof,
)))
}
}
fn tag_no_case<'a, E>(tag: &'static str) -> impl FnMut(&'a str) -> IResult<&'a str, &'a str, E>
where
E: error::WithTagError<&'a str>,
{
let nom_tag = nom_tag(tag);
move |input| match nom_tag(input) {
Ok(result) => Ok(result),
Err(err) => Err(err.map(|()| E::from_tag(input, tag))),
}
}
fn parse_custom_character_class<'a>(
input: &'a str,
) -> IResult<&'a str, Vec<char>, PasswordRulesErrorContext<'a>> {
let opt_dash = opt(char('-'));
let inner = opt(is_not("-]"));
let opt_bracket = |input: &'a str| {
if input.starts_with("]]") {
Ok((&input[1..], Some(']')))
} else {
Ok((input, None))
}
};
let body = recognize(tuple((opt_dash, inner, opt_bracket)));
let body = map(body, |s| {
s.chars()
.filter(|&c| c.is_ascii_graphic() || c == ' ')
.collect()
});
delimited(char('['), body, cut(char(']')))(input)
}
fn parse_character_class<'a>(
input: &'a str,
) -> IResult<&'a str, CharacterClass, PasswordRulesErrorContext<'a>> {
alt((
value(CharacterClass::Upper, tag_no_case("upper")),
value(CharacterClass::Lower, tag_no_case("lower")),
value(CharacterClass::Digit, tag_no_case("digit")),
value(CharacterClass::Special, tag_no_case("special")),
value(
CharacterClass::AsciiPrintable,
tag_no_case("ascii-printable"),
),
value(CharacterClass::Unicode, tag_no_case("unicode")),
map(parse_custom_character_class, CharacterClass::Custom),
))(input)
}
fn parse_character_classes<'a>(
input: &'a str,
) -> IResult<&'a str, Vec<CharacterClass>, PasswordRulesErrorContext<'a>> {
let comma = space_surround(char(','));
let terminator = space_surround(alt((value((), char(';')), eof)));
fold_separated_terminated(
parse_character_class,
comma,
terminator,
false,
Vec::new(),
|mut classes, class| {
classes.push(class);
classes
},
)(input)
}
fn parse_number<'a, E>(input: &'a str) -> IResult<&'a str, u32, E>
where
E: ParseError<&'a str> + FromExternalError<&'a str, std::num::ParseIntError>,
{
map_res(digit1, str::parse)(input)
}
fn parse_generic_rule<'a, P, O>(
name: &'static str,
parser: P,
) -> impl FnMut(&'a str) -> IResult<&'a str, O, PasswordRulesErrorContext<'a>>
where
P: Fn(&'a str) -> IResult<&'a str, O, PasswordRulesErrorContext<'a>>,
{
let colon = space_surround(char(':'));
let pattern = tuple((tag_no_case(name), cut(colon), cut(parser)));
map(pattern, |(_, _, out)| out)
}
fn parse_optional_rule_number<'a, E>(input: &'a str) -> IResult<&'a str, Option<u32>, E>
where
E: ParseError<&'a str> + FromExternalError<&'a str, std::num::ParseIntError>,
{
opt_terminated(
parse_number,
space_surround(alt((value((), char(';')), eof))),
)(input)
}
fn parse_rule<'a>(input: &'a str) -> IResult<&'a str, PasswordRule, PasswordRulesErrorContext<'a>> {
alt((
map(
parse_generic_rule("required", parse_character_classes),
PasswordRule::Require,
),
map(
parse_generic_rule("allowed", parse_character_classes),
PasswordRule::Allow,
),
map(
parse_generic_rule("max-consecutive", parse_optional_rule_number),
PasswordRule::MaxConsecutive,
),
map(
parse_generic_rule("minlength", parse_optional_rule_number),
PasswordRule::MinLength,
),
map(
parse_generic_rule("maxlength", parse_optional_rule_number),
PasswordRule::MaxLength,
),
))(input)
}
fn apply<T>(source: &mut Option<T>, new: T, mut cmp: impl FnMut(T, T) -> T) {
*source = match source.take() {
None => Some(new),
Some(old) => Some(cmp(old, new)),
}
}
fn parse_rule_list<'a>(
input: &'a str,
) -> IResult<&'a str, PasswordRules, PasswordRulesErrorContext<'a>> {
fold_separated_terminated(
parse_rule,
space_surround(char(';')),
space_surround(eof),
true,
PasswordRules::default(),
|mut rules, rule| {
match rule {
PasswordRule::Allow(classes) => rules.allowed.extend(classes),
PasswordRule::Require(classes) => {
let classes = canonicalize(classes);
if !classes.is_empty() {
rules.required.push(canonicalize(classes));
}
}
PasswordRule::MinLength(Some(length)) => apply(&mut rules.min_length, length, max),
PasswordRule::MaxLength(Some(length)) => apply(&mut rules.max_length, length, min),
PasswordRule::MaxConsecutive(Some(length)) => {
apply(&mut rules.max_consecutive, length, min)
}
_ => {}
};
rules
},
)(input)
}
pub fn parse_password_rules(
s: &str,
supply_default: bool,
) -> Result<PasswordRules, PasswordRulesError> {
let s = s.trim();
if s.is_empty() {
return Err(PasswordRulesError::empty());
}
let mut parse_rules = complete(parse_rule_list);
let mut rules = match parse_rules(s) {
Ok((_, rules)) => rules,
Err(nom::Err::Incomplete(..)) => unreachable!(),
Err(nom::Err::Error(err)) | Err(nom::Err::Failure(err)) => {
return Err(err.extract_context(s))
}
};
rules.allowed = canonicalize(rules.allowed);
if supply_default && rules.allowed.is_empty() && rules.required.is_empty() {
rules.allowed.push(CharacterClass::AsciiPrintable);
}
Ok(rules)
}
struct AsciiTable {
table: [bool; 128],
}
#[derive(Debug, Clone)]
enum CheckResult<I> {
Match,
Mismatch(I),
}
impl AsciiTable {
fn new() -> Self {
Self {
table: [false; 128],
}
}
fn set(&mut self, b: char) {
self.table[b as usize] = true;
}
fn set_range(&mut self, range: impl IntoIterator<Item = char>) {
range.into_iter().for_each(|c| self.set(c))
}
fn check(&self, b: char) -> bool {
self.table.get(b as usize).copied().unwrap_or(false)
}
fn check_range(&self, range: impl IntoIterator<Item = char>) -> bool {
range.into_iter().all(move |b| self.check(b))
}
fn check_or_extract<'s>(
&'s self,
range: impl IntoIterator<Item = char> + Clone + 's,
) -> CheckResult<impl Iterator<Item = char> + 's> {
if self.check_range(range.clone()) {
CheckResult::Match
} else {
CheckResult::Mismatch(range.into_iter().filter(move |&b| self.check(b)))
}
}
}
fn canonicalize(mut classes: Vec<CharacterClass>) -> Vec<CharacterClass> {
let mut table = AsciiTable::new();
if classes.contains(&CharacterClass::Unicode) {
return vec![CharacterClass::Unicode];
}
if classes.contains(&CharacterClass::AsciiPrintable) {
return vec![CharacterClass::AsciiPrintable];
}
for class in classes.drain(..) {
match class {
CharacterClass::Upper => table.set_range(UPPER_RANGE),
CharacterClass::Lower => table.set_range(LOWER_RANGE),
CharacterClass::Digit => table.set_range(DIGIT_RANGE),
CharacterClass::Special => table.set_range(SPECIAL_CHARS.chars()),
CharacterClass::Custom(chars) => table.set_range(chars.into_iter()),
_ => unreachable!(),
}
}
let mut custom_characters = vec![];
if let CheckResult::Match = table.check_or_extract(ASCII_RANGE) {
return vec![CharacterClass::AsciiPrintable];
}
match table.check_or_extract(UPPER_RANGE) {
CheckResult::Match => classes.push(CharacterClass::Upper),
CheckResult::Mismatch(chars) => custom_characters.extend(chars),
}
match table.check_or_extract(LOWER_RANGE) {
CheckResult::Match => classes.push(CharacterClass::Lower),
CheckResult::Mismatch(chars) => custom_characters.extend(chars),
}
match table.check_or_extract(DIGIT_RANGE) {
CheckResult::Match => classes.push(CharacterClass::Digit),
CheckResult::Mismatch(chars) => custom_characters.extend(chars),
}
match table.check_or_extract(SPECIAL_CHARS.chars()) {
CheckResult::Match => classes.push(CharacterClass::Special),
CheckResult::Mismatch(chars) => custom_characters.extend(chars),
}
if !custom_characters.is_empty() {
classes.push(CharacterClass::Custom(custom_characters));
}
classes
}
#[cfg(test)]
mod test {
use super::*;
mod canonicalize {
use super::*;
fn test_canonicalizer(input: &str, expected: Vec<CharacterClass>) {
let input = input.chars().collect();
let classes = vec![CharacterClass::Custom(input)];
let res = canonicalize(classes);
assert_eq!(res, expected)
}
#[test]
fn few_characters() {
test_canonicalizer("abc", vec![CharacterClass::Custom(vec!['a', 'b', 'c'])])
}
#[test]
fn all_lower() {
test_canonicalizer("abcdefghijklmnopqrstuvwxyz", vec![CharacterClass::Lower])
}
#[test]
fn all_alpha() {
test_canonicalizer(
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
vec![CharacterClass::Upper, CharacterClass::Lower],
)
}
#[test]
fn all_special() {
test_canonicalizer(
r##"- !"#$%&'()*+,./:;<=>?@[\^_`{|}~]"##,
vec![CharacterClass::Special],
)
}
#[test]
fn digits_and_some_lowers() {
test_canonicalizer(
"67abc1def0ghijk2ln8op9qr4stuv5wxy3z",
vec![
CharacterClass::Digit,
CharacterClass::Custom("abcdefghijklnopqrstuvwxyz".chars().collect()),
],
)
}
#[test]
fn alphanumeric_and_some_specials() {
test_canonicalizer(
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ()*+.",
vec![
CharacterClass::Upper,
CharacterClass::Lower,
CharacterClass::Digit,
CharacterClass::Custom(vec!['(', ')', '*', '+', '.']),
],
)
}
#[test]
fn everything() {
test_canonicalizer(
r##"-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!"#$%&'()*+,./:;<=>?@[\^_`{|}~ ]"##,
vec![CharacterClass::AsciiPrintable],
)
}
}
mod parser {
use super::*;
macro_rules! tests {
($($test:ident $($doc:literal)? : $input:literal => $expected:expr;)*) => {$(
$(#[doc = $doc])?
#[test]
fn $test() {
assert_eq!(
parse_password_rules($input, true).ok(),
$expected,
"{doc}\ninput: {input:?}",
doc = None $(.or(Some($doc)))? .unwrap_or(""),
input = $input
);
}
)*};
}
macro_rules! password_rules {
(
$(max-consecutive: $consecutive:expr;)?
$(maxlength: $maxlength:expr;)?
$(minlength: $minlength:expr;)?
$(allowed: $($allowed_class:tt),* $(,)? ;)?
$(required: $($required_class:tt),* $(,)? ;)*
) => {
Some(PasswordRules{
max_consecutive: None $(.or(Some($consecutive)))?,
max_length: None $(.or(Some($maxlength)))?,
min_length: None $(.or(Some($minlength)))?,
allowed: vec![$($(
character_class!($allowed_class),
)*)?],
required: vec![$(
vec![$(
character_class!($required_class),
)*],
)*],
})
}
}
macro_rules! character_class {
(upper) => {CharacterClass::Upper};
(lower) => {CharacterClass::Lower};
(digit) => {CharacterClass::Digit};
(special) => {CharacterClass::Special};
(ascii) => {CharacterClass::AsciiPrintable};
(unicode) => {CharacterClass::Unicode};
([$($c:expr)*]) => {CharacterClass::Custom(vec![$($c,)*])};
}
tests! {
empty_string: "" => None;
missing_property: "allowed:;" => password_rules!(allowed: ascii;);
multiple_classes: "allowed: digit, special;" => password_rules!(
allowed: digit, special;
);
missing_integers: "max-consecutive:;minlength:;maxlength:;" => password_rules!(
allowed: ascii;
);
empty_custom_class: "allowed:[];" => password_rules!(allowed: ascii;);
multiple_length_constraints:
"maxlength:50;\
max-consecutive:40;\
minlength:10;\
max-consecutive:30;\
minlength:12;\
maxlength:20;"
=>
password_rules!(
max-consecutive: 30;
maxlength: 20;
minlength: 12;
allowed: ascii;
);
custom_class_bracket_hyphen: "allowed: [-]]; required: [[]]; allowed:[-];" => password_rules!(
allowed: ['-' ']'];
required: ['[' ']'];
);
invalid_hyphen: "allowed: [a-];" => None;
invalid_bracket: "allowed: []a];" => None;
complex_input:
"allowed:special;\
max-consecutive:3;\
required: upper, digit, ['*/];\
allowed: [abc], digit,special;\
minlength:20;"
=>
password_rules!(
max-consecutive: 3;
minlength: 20;
allowed: digit, special, ['a' 'b' 'c'];
required: upper, digit, ['\'' '*' '/'];
);
skip_unicode_characters: "allowed: [供应A商B责任C进展];" => password_rules!(
allowed: ['A' 'B' 'C'];
);
unicode_overpowers_everything:
"allowed: \
[abcdefghijklmnopqrstuvwxyz], \
upper, digit, ascii-printable, \
special, unicode;"
=>
password_rules!(allowed: unicode;);
ascii_overpowers_everything_else:
"allowed: lower; \
allowed: [ABCDEFGHIJKLMNOPQRSTUVWXYZ]; \
allowed: special; \
allowed: [0123456789]; \
allowed: ascii-printable;"
=>
password_rules!(allowed: ascii;);
allow_missing_trailing_semicolon: "allowed: lower; required: upper" => password_rules!(
allowed: lower;
required: upper;
);
multiple_required_sets:
"required: upper, lower; required: digit; allowed: digit; allowed: upper;"
=>
password_rules!(
allowed: upper, digit;
required: upper, lower;
required: digit;
);
}
mod apple_suite {
use super::*;
tests! {
empty_string: "" => None;
req_upper1: " required: upper" => password_rules!(required: upper;);
req_upper2: " required: upper;" => password_rules!(required: upper;);
req_upper3: " required: upper " => password_rules!(required: upper;);
req_upper4: "required:upper" => password_rules!(required: upper;);
req_upper6: "required: upper" => password_rules!(required: upper;);
req_upper_case "Test that character class names are case insensitive":
"required: uPPeR" => password_rules!(required: upper;);
all_upper1: "allowed:upper" => password_rules!(allowed: upper;);
all_upper2: "allowed: upper" => password_rules!(allowed: upper;);
required_canonical "Test that a custom character set that overlaps a class is omitted":
"required: upper, [AZ];" => password_rules!(required: upper;);
allowed_reduction "Test that multiple allowed rules are collapsed together":
"required: upper; allowed: upper; allowed: lower" => password_rules!(
allowed: upper, lower;
required: upper;
);
max_consecutive1: "max-consecutive: 5" => password_rules!(
max-consecutive: 5;
allowed: ascii;
);
max_consecutive2: "max-consecutive:5" => password_rules!(
max-consecutive: 5;
allowed: ascii;
);
max_consecutive3: " max-consecutive:5" => password_rules!(
max-consecutive: 5;
allowed: ascii;
);
max_consecutive_min1 "Test that the lowest number wins for multiple max-consecutive":
"max-consecutive: 5; max-consecutive: 3" => password_rules!(
max-consecutive: 3;
allowed: ascii;
);
max_consecutive_min2 "Test that the lowest number wins for multiple max-consecutive":
"max-consecutive: 3; max-consecutive: 5" => password_rules!(
max-consecutive: 3;
allowed: ascii;
);
max_consecutive_min3 "Test that the lowest number wins for multiple max-consecutive":
"max-consecutive: 3; max-consecutive: 1; max-consecutive: 5" => password_rules!(
max-consecutive: 1;
allowed: ascii;
);
max_consecutive_min4 "Test that the lowest number wins for multiple max-consecutive":
"required: ascii-printable; max-consecutive: 5; max-consecutive: 3" => password_rules!(
max-consecutive: 3;
required: ascii;
);
require_allow1: "required: [*&^]; allowed: upper" => password_rules!(
allowed: upper;
required: ['&' '*' '^'];
);
require_allow2: "required: [*&^ABC]; allowed: upper" => password_rules!(
allowed: upper;
required: ['A' 'B' 'C' '&' '*' '^'];
);
required_allow3: "required: unicode; required: digit" => password_rules!(
required: unicode;
required: digit;
);
require_empty "Test that an empty required set is ignored":
"required: ; required: upper" => password_rules!(
required: upper;
);
custom_unicode_dropped1 "Test that unicode characters in custom classes are ignored":
"allowed: [供应商责任进展]" => password_rules!(
allowed: ascii;
);
custom_unicode_dropped2 "Test that unicode characters in custom classes are ignored":
"allowed: [供应A商B责任C进展]" => password_rules!(
allowed: ['A' 'B' 'C'];
);
collapse_allow1 "Test that several allow rules are collapsed together":
"required: upper; allowed: upper; allowed: lower; minlength: 12; maxlength: 73;" =>
password_rules!(
maxlength: 73;
minlength: 12;
allowed: upper, lower;
required: upper;
);
collapse_allow2 "Test that several allow rules are collapsed together":
"required: upper; allowed: upper; allowed: lower; maxlength: 73; minlength: 12;" =>
password_rules!(
maxlength: 73;
minlength: 12;
allowed: upper, lower;
required: upper;
);
collapse_allow3 "Test that several allow rules are collapsed together":
"required: upper; allowed: upper; allowed: lower; maxlength: 73" => password_rules!(
maxlength: 73;
allowed: upper, lower;
required: upper;
);
collapse_allow4 "Test that several allow rules are collapsed together":
"required: upper; allowed: upper; allowed: lower; minlength: 12;" => password_rules!(
minlength: 12;
allowed: upper, lower;
required: upper;
);
minlength_max1 "Test that the largest number wins for multiple minlength":
"minlength: 12; minlength: 7; minlength: 23" => password_rules!(
minlength: 23;
allowed: ascii;
);
minlength_max2 "Test that the largest number wins for multiple minlength":
"minlength: 12; maxlength: 17; minlength: 10" => password_rules!(
maxlength: 17;
minlength: 12;
allowed: ascii;
);
bad_syntax1: "allowed: upper,," => None;
bad_syntax2: "allowed: upper,;" => None;
bad_syntax3: "allowed: upper [a]" => None;
bad_syntax4: "dummy: upper" => None;
bad_syntax5: "upper: lower" => None;
bad_syntax6: "max-consecutive: [ABC]" => None;
bad_syntax7: "max-consecutive: upper" => None;
bad_syntax8: "max-consecutive: 1+1" => None;
bad_syntax9: "max-consecutive: 供" => None;
bad_syntax10: "required: 1" => None;
bad_syntax11: "required: 1+1" => None;
bad_syntax12: "required: 供" => None;
bad_syntax13: "required: A" => None;
bad_syntax14: "required: required: upper" => None;
bad_syntax15: "allowed: 1" => None;
bad_syntax16: "allowed: 1+1" => None;
bad_syntax17: "allowed: 供" => None;
bad_syntax18: "allowed: A" => None;
bad_syntax19: "allowed: allowed: upper" => None;
custom_class1
"Test that a - and ] are only accepted as the first and last characters in a class":
"required: digit ; required: [-]];" =>
password_rules!(
required: digit;
required: ['-' ']'];
);
custom_class2
"Test that a - and ] are only accepted as the first and last characters in a class":
"required: digit ; required: [-ABC]];" =>
password_rules!(
required: digit;
required: ['A' 'B' 'C' '-' ']'];
);
custom_class3
"Test that a - and ] are only accepted as the first and last characters in a class":
"required: digit ; required: [-];" =>
password_rules!(
required: digit;
required: ['-'];
);
custom_class4
"Test that a - and ] are only accepted as the first and last characters in a class":
"required: digit ; required: []];" =>
password_rules!(
required: digit;
required: [']'];
);
bad_custom_class1 "Test that a hyphen is only accepted as the first character in a class":
"required: digit ; required: [a-];" => None;
bad_custom_class2 "Test that a hyphen is only accepted as the first character in a class":
"required: digit ; required: []-];" => None;
bad_custom_class3 "Test that a hyphen is only accepted as the first character in a class":
"required: digit ; required: [--];" => None;
bad_custom_class4 "Test that a hyphen is only accepted as the first character in a class":
"required: digit ; required: [-a--------];" => None;
bad_custom_class5 "Test that a hyphen is only accepted as the first character in a class":
"required: digit ; required: [-a--------] ];" => None;
canonical1 "Test that a custom character class is converted into a named class":
"required: [abcdefghijklmnopqrstuvwxyz]" => password_rules!(
required: lower;
);
canonical2 "Test that a custom character class is converted into a named class":
"required: [abcdefghijklmnopqrstuvwxy]" => password_rules!(
required: [
'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'
];
);
}
}
}
}