use super::config::Detector;
use regex::Regex;
use std::sync::OnceLock;
pub fn detects(detector: Detector, value: &str) -> bool {
match detector {
Detector::Email => email_re().is_match(value),
Detector::Ssn => is_ssn(value),
Detector::Phone => is_phone(value),
Detector::Ipv4 => is_ipv4(value),
Detector::CreditCard => is_credit_card(value),
}
}
fn email_re() -> &'static Regex {
static RE: OnceLock<Regex> = OnceLock::new();
RE.get_or_init(|| Regex::new(r"(?i)^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,}$").unwrap())
}
fn is_ssn(value: &str) -> bool {
static RE: OnceLock<Regex> = OnceLock::new();
let re = RE.get_or_init(|| Regex::new(r"^(\d{3})-(\d{2})-(\d{4})$").unwrap());
let Some(caps) = re.captures(value) else {
return false;
};
let area = &caps[1];
let group = &caps[2];
let serial = &caps[3];
area != "000" && area != "666" && !area.starts_with('9') && group != "00" && serial != "0000"
}
fn is_phone(value: &str) -> bool {
let mut digits = 0usize;
for (i, ch) in value.chars().enumerate() {
match ch {
'0'..='9' => digits += 1,
'+' if i == 0 => {}
' ' | '.' | '-' | '(' | ')' => {}
_ => return false,
}
}
(10..=15).contains(&digits)
}
fn is_ipv4(value: &str) -> bool {
let mut octets = 0;
for part in value.split('.') {
octets += 1;
if octets > 4 {
return false;
}
if part.is_empty() || part.len() > 3 || !part.bytes().all(|b| b.is_ascii_digit()) {
return false;
}
if part.len() > 1 && part.starts_with('0') {
return false;
}
if part.parse::<u16>().unwrap_or(256) > 255 {
return false;
}
}
octets == 4
}
fn is_credit_card(value: &str) -> bool {
let mut digits: Vec<u8> = Vec::with_capacity(19);
let mut prev_sep = true; for ch in value.chars() {
match ch {
'0'..='9' => {
digits.push(ch as u8 - b'0');
prev_sep = false;
}
' ' | '-' => {
if prev_sep {
return false; }
prev_sep = true;
}
_ => return false,
}
}
if prev_sep && !digits.is_empty() && (value.ends_with(' ') || value.ends_with('-')) {
return false; }
if digits.len() < 13 || digits.len() > 19 {
return false;
}
luhn_valid(&digits)
}
fn luhn_valid(digits: &[u8]) -> bool {
let mut sum = 0u32;
let mut double = false;
for &d in digits.iter().rev() {
let mut v = d as u32;
if double {
v *= 2;
if v > 9 {
v -= 9;
}
}
sum += v;
double = !double;
}
sum.is_multiple_of(10)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn email_detector() {
assert!(detects(Detector::Email, "alice@example.com"));
assert!(detects(Detector::Email, "a.b+tag@sub.domain.co"));
assert!(!detects(Detector::Email, "not an email"));
assert!(!detects(Detector::Email, "alice@localhost")); assert!(!detects(Detector::Email, "prefix alice@x.com suffix")); }
#[test]
fn ssn_detector() {
assert!(detects(Detector::Ssn, "123-45-6789"));
assert!(!detects(Detector::Ssn, "000-45-6789")); assert!(!detects(Detector::Ssn, "666-45-6789"));
assert!(!detects(Detector::Ssn, "900-45-6789"));
assert!(!detects(Detector::Ssn, "123-00-6789")); assert!(!detects(Detector::Ssn, "123-45-0000")); assert!(!detects(Detector::Ssn, "123456789")); }
#[test]
fn credit_card_detector_uses_luhn() {
assert!(detects(Detector::CreditCard, "4111111111111111")); assert!(detects(Detector::CreditCard, "4111 1111 1111 1111"));
assert!(detects(Detector::CreditCard, "4111-1111-1111-1111"));
assert!(detects(Detector::CreditCard, "378282246310005")); assert!(!detects(Detector::CreditCard, "4111111111111112")); assert!(!detects(Detector::CreditCard, "1234567890")); assert!(!detects(Detector::CreditCard, "4111 1111 1111 111a")); assert!(!detects(Detector::CreditCard, "-4111111111111111")); }
#[test]
fn phone_detector() {
assert!(detects(Detector::Phone, "+14155552671"));
assert!(detects(Detector::Phone, "415-555-2671"));
assert!(detects(Detector::Phone, "(415) 555-2671"));
assert!(!detects(Detector::Phone, "12345")); assert!(!detects(Detector::Phone, "not a phone"));
}
#[test]
fn ipv4_detector() {
assert!(detects(Detector::Ipv4, "192.168.0.1"));
assert!(detects(Detector::Ipv4, "0.0.0.0"));
assert!(detects(Detector::Ipv4, "255.255.255.255"));
assert!(!detects(Detector::Ipv4, "256.0.0.1")); assert!(!detects(Detector::Ipv4, "192.168.01.1")); assert!(!detects(Detector::Ipv4, "192.168.0")); assert!(!detects(Detector::Ipv4, "1.2.3.4.5")); }
}