#![deny(missing_docs)]
#![deny(rust_2018_idioms)]
#![deny(rustdoc::broken_intra_doc_links)]
pub type PatternEntry = (&'static str, &'static str);
pub const PATTERNS: &[PatternEntry] = &[
("BIC", r"(?-i:\b[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?\b)"),
("IBAN", r"\b[A-Z]{2}\d{2}[A-Z0-9]{11,30}\b"),
(
"EMAIL",
r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b",
),
(
"JWT",
r"\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b",
),
("IPV4", r"\b(?:\d{1,3}\.){3}\d{1,3}\b"),
(
"PEM",
r"-----BEGIN (?:[A-Z ]+)-----[\s\S]+?-----END (?:[A-Z ]+)-----",
),
(
"TAXID_DE",
r"(?:steuer[-_ ]?id|steueridentifikationsnummer|tax[-_ ]?id)[:\s=]*\d{11}",
),
];
pub mod labels {
pub const IBAN: &str = "IBAN";
pub const EMAIL: &str = "EMAIL";
}
#[cfg(test)]
mod tests {
use super::*;
use regex::RegexBuilder;
#[test]
fn every_pattern_compiles_case_insensitively() {
for (label, pat) in PATTERNS {
RegexBuilder::new(pat)
.case_insensitive(true)
.build()
.unwrap_or_else(|e| panic!("{label} pattern failed to compile: {e}"));
}
}
#[test]
fn bic_precedes_iban_in_order() {
let positions: std::collections::HashMap<&str, usize> = PATTERNS
.iter()
.enumerate()
.map(|(i, (label, _))| (*label, i))
.collect();
assert!(positions["BIC"] < positions["IBAN"]);
}
#[test]
fn taxid_de_requires_prefix() {
let re = RegexBuilder::new(PATTERNS.iter().find(|(l, _)| *l == "TAXID_DE").unwrap().1)
.case_insensitive(true)
.build()
.unwrap();
assert!(
!re.is_match("12345678901"),
"bare 11-digit string must not match"
);
assert!(re.is_match("steuer-id: 12345678901"));
assert!(re.is_match("Steueridentifikationsnummer: 12345678901"));
}
#[test]
fn iban_pattern_rejects_near_misses() {
let re = RegexBuilder::new(PATTERNS.iter().find(|(l, _)| *l == "IBAN").unwrap().1)
.case_insensitive(true)
.build()
.unwrap();
assert!(
!re.is_match("DE12"),
"short string must not match IBAN pattern"
);
assert!(
!re.is_match("12345678901234"),
"bare numeric string must not match IBAN"
);
assert!(
re.is_match("DE89370400440532013000"),
"valid IBAN must match"
);
}
#[test]
fn email_pattern_rejects_near_misses() {
let re = RegexBuilder::new(PATTERNS.iter().find(|(l, _)| *l == "EMAIL").unwrap().1)
.case_insensitive(true)
.build()
.unwrap();
assert!(
!re.is_match("not-an-email"),
"bare word must not match EMAIL"
);
assert!(
!re.is_match("missing-at-sign.com"),
"missing @ must not match EMAIL"
);
assert!(re.is_match("user@example.com"), "valid email must match");
}
#[test]
fn bic_pattern_rejects_near_misses() {
let re = RegexBuilder::new(PATTERNS.iter().find(|(l, _)| *l == "BIC").unwrap().1)
.case_insensitive(true)
.build()
.unwrap();
assert!(!re.is_match("ABCD12"), "5-char string must not match BIC");
assert!(re.is_match("DEUTDEDB"), "valid 8-char BIC must match");
}
#[test]
fn bic_stays_case_sensitive_under_insensitive_builder() {
let re = RegexBuilder::new(PATTERNS.iter().find(|(l, _)| *l == "BIC").unwrap().1)
.case_insensitive(true)
.build()
.unwrap();
for word in ["individuals", "potentially", "decision", "employee"] {
assert!(
!re.is_match(word),
"ordinary word `{word}` must not match BIC"
);
}
assert!(re.is_match("DEUTDEFF"), "uppercase BIC must still match");
assert!(
!re.is_match("deutdeff"),
"lowercase BIC-shaped token must not match"
);
}
#[test]
fn jwt_pattern_rejects_near_misses() {
let re = RegexBuilder::new(PATTERNS.iter().find(|(l, _)| *l == "JWT").unwrap().1)
.case_insensitive(true)
.build()
.unwrap();
assert!(
!re.is_match("header.payload.sig"),
"non-base64url JWT prefix must not match"
);
assert!(
!re.is_match("eyJhbGc.eyJzdWI"),
"two-segment token must not match JWT pattern"
);
assert!(
re.is_match("eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIn0.abc123"),
"valid three-segment JWT must match"
);
}
}