pub struct PassPolicy {
pub min_len: usize,
pub min_char_classes: usize
}
impl PassPolicy {
#[must_use]
pub const fn none() -> Self {
Self {
min_len: 0,
min_char_classes: 0
}
}
#[must_use]
pub const fn strong() -> Self {
Self {
min_len: 12,
min_char_classes: 3
}
}
}
impl PassPolicy {
#[must_use]
pub fn spec(&self) -> String {
let minlen = if self.min_len > 1 {
Some(format!("be at least {} characters", self.min_len))
} else if self.min_len > 0 {
Some("be at least 1 character".to_string())
} else {
None
};
let classes_str = "(upper-case letter, lower-case letter, digit, special)";
let minclasses = if self.min_char_classes > 1 {
Some(format!(
"contain at least {} character classes {classes_str}",
self.min_char_classes
))
} else if self.min_char_classes > 0 {
Some(format!("contain at least 1 character class {classes_str}"))
} else {
None
};
match (minlen, minclasses) {
(Some(minlen), Some(minclasses)) => {
format!("must {minlen} and {minclasses}")
}
(None, Some(minclasses)) => {
format!("must {minclasses}")
}
(Some(minlen), None) => {
format!("must {minlen}")
}
(None, None) => "has no restrictions".into()
}
}
pub fn validate(&self, pass: &str) -> Result<(), String> {
let mut lcase = 0;
let mut ucase = 0;
let mut digit = 0;
let mut special = 0;
let mut len = 0;
for ch in pass.chars() {
len += 1;
if ch.is_ascii_lowercase() {
lcase += 1;
} else if ch.is_ascii_uppercase() {
ucase += 1;
} else if ch.is_ascii_digit() {
digit += 1;
} else {
special += 1;
}
}
let mut classes = 0;
if lcase > 0 {
classes += 1;
}
if ucase > 0 {
classes += 1;
}
if digit > 0 {
classes += 1;
}
if special > 0 {
classes += 1;
}
let lenerr = if len < self.min_len {
Some("too short")
} else {
None
};
let classerr = if classes < self.min_char_classes {
Some("too few character classes")
} else {
None
};
match (lenerr, classerr) {
(Some(lenerr), Some(classerr)) => {
Err(format!("{lenerr} and {classerr}"))
}
(Some(lenerr), None) => Err(lenerr.to_string()),
(None, Some(classerr)) => Err(classerr.to_string()),
(None, None) => Ok(())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn specs() {
let pp = PassPolicy {
min_len: 0,
min_char_classes: 0
};
assert_eq!(pp.spec(), "has no restrictions");
let pp = PassPolicy {
min_len: 1,
min_char_classes: 0
};
assert_eq!(pp.spec(), "must be at least 1 character");
let pp = PassPolicy {
min_len: 2,
min_char_classes: 0
};
assert_eq!(pp.spec(), "must be at least 2 characters");
let pp = PassPolicy {
min_len: 0,
min_char_classes: 1
};
assert_eq!(
pp.spec(),
"must contain at least 1 character class (upper-case letter, \
lower-case letter, digit, special)"
);
let pp = PassPolicy {
min_len: 0,
min_char_classes: 2
};
assert_eq!(
pp.spec(),
"must contain at least 2 character classes (upper-case letter, \
lower-case letter, digit, special)"
);
let pp = PassPolicy {
min_len: 2,
min_char_classes: 2
};
assert_eq!(
pp.spec(),
"must be at least 2 characters and contain at least 2 character \
classes (upper-case letter, lower-case letter, digit, special)"
);
}
#[test]
fn validate() {
PassPolicy {
min_len: 0,
min_char_classes: 0
}
.validate("")
.unwrap();
let Err(e) = PassPolicy {
min_len: 1,
min_char_classes: 0
}
.validate("") else {
panic!("Unexpectedly not Err()");
};
assert_eq!(e, "too short");
let Err(e) = PassPolicy {
min_len: 0,
min_char_classes: 1
}
.validate("") else {
panic!("Unexpectedly not Err()");
};
assert_eq!(e, "too few character classes");
let Err(e) = PassPolicy {
min_len: 1,
min_char_classes: 1
}
.validate("") else {
panic!("Unexpectedly not Err()");
};
assert_eq!(e, "too short and too few character classes");
}
}