use regex::Regex;
use super::registry::{RedactionPattern, RedactionPatternKind};
pub fn builtin_specs() -> Vec<BuiltinSpec> {
vec![
BuiltinSpec {
name: "email",
regex: r"[A-Za-z0-9._%+\-]{1,64}@[A-Za-z0-9.\-]{1,253}\.[A-Za-z]{2,24}",
kind: RedactionPatternKind::Literal,
},
BuiltinSpec {
name: "ssn",
regex: r"\b[0-9]{3}-[0-9]{2}-[0-9]{4}\b",
kind: RedactionPatternKind::Literal,
},
BuiltinSpec {
name: "us_phone",
regex: r"(?:\(\s*[0-9]{3}\s*\)\s*[0-9]{3}-[0-9]{4}|\b[0-9]{3}-[0-9]{3}-[0-9]{4}\b)",
kind: RedactionPatternKind::Literal,
},
BuiltinSpec {
name: "credit_card",
regex: r"\b(?:[0-9][ \-]?){12,18}[0-9]\b",
kind: RedactionPatternKind::CreditCardWithLuhn,
},
BuiltinSpec {
name: "aws_access_key",
regex: r"\bAKIA[A-Z0-9]{16}\b",
kind: RedactionPatternKind::Literal,
},
BuiltinSpec {
name: "github_pat",
regex: r"\bgh[pousr]_[A-Za-z0-9]{36,255}\b",
kind: RedactionPatternKind::Literal,
},
]
}
#[derive(Debug, Clone)]
pub struct BuiltinSpec {
pub name: &'static str,
pub regex: &'static str,
pub kind: RedactionPatternKind,
}
impl BuiltinSpec {
pub fn into_pattern(self) -> RedactionPattern {
let re = Regex::new(self.regex).unwrap_or_else(|e| {
panic!("built-in redaction pattern `{}` failed to compile: {e}", self.name)
});
RedactionPattern {
name: self.name.to_string(),
regex: re,
replacement: format!("[REDACTED:{}]", self.name),
enabled: true,
kind: self.kind,
}
}
}
pub fn luhn_check(s: &str) -> bool {
let digits: Vec<u32> = s
.chars()
.filter_map(|c| c.to_digit(10))
.collect();
if !(13..=19).contains(&digits.len()) {
return false;
}
let mut sum: u32 = 0;
for (i, d) in digits.iter().rev().enumerate() {
let mut v = *d;
if i % 2 == 1 {
v *= 2;
if v > 9 {
v -= 9;
}
}
sum += v;
}
sum.is_multiple_of(10)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn luhn_accepts_canonical_test_cards() {
assert!(luhn_check("4111111111111111"));
assert!(luhn_check("5555555555554444"));
assert!(luhn_check("378282246310005"));
}
#[test]
fn luhn_rejects_random_digit_runs() {
assert!(!luhn_check("1111111111111111"));
assert!(!luhn_check("4111111111111112"));
}
#[test]
fn luhn_rejects_too_short_or_too_long() {
assert!(!luhn_check("411111111111"));
assert!(!luhn_check("41111111111111111119"));
}
#[test]
fn luhn_strips_separators_before_check() {
assert!(luhn_check("4111-1111-1111-1111"));
assert!(luhn_check("4111 1111 1111 1111"));
}
#[test]
fn all_builtin_regexes_compile() {
for spec in builtin_specs() {
let p = spec.into_pattern();
assert!(p.enabled);
assert!(p.replacement.starts_with("[REDACTED:"));
}
}
}