veil_engine/detect/
mod.rs1mod card;
4mod custom;
5mod dictionary;
6mod email;
7mod iban;
8mod ip;
9mod natid;
10#[cfg(feature = "phone")]
11mod phone;
12mod secret;
13mod url;
14
15use std::sync::Arc;
16
17use crate::kind::EntityKind;
18use crate::rules::{RulesError, RulesFile};
19
20#[derive(Clone, Debug, PartialEq, Eq)]
22#[non_exhaustive]
23pub enum Source {
24 Dictionary,
26 BuiltIn,
28 Regex(Arc<str>),
30 Ner,
32}
33
34#[derive(Clone, Debug)]
35pub(crate) struct Candidate {
36 pub start: usize,
37 pub end: usize,
38 pub kind: EntityKind,
39 pub source: Source,
40 pub priority: u8,
42}
43
44pub(crate) trait Detector: Send + Sync {
45 fn find(&self, text: &str, out: &mut Vec<Candidate>);
46}
47
48pub(crate) const P_DICTIONARY: u8 = 0;
50pub(crate) const P_CUSTOM: u8 = 5;
51pub(crate) const P_SECRET: u8 = 10;
52pub(crate) const P_CARD: u8 = 11;
53pub(crate) const P_IBAN: u8 = 12;
54pub(crate) const P_NATID: u8 = 13;
55pub(crate) const P_EMAIL: u8 = 14;
56pub(crate) const P_URL: u8 = 15;
57pub(crate) const P_IP: u8 = 16;
58#[cfg(feature = "phone")]
59pub(crate) const P_PHONE: u8 = 17;
60pub(crate) const P_NER: u8 = 20;
61
62pub(crate) fn build(rules: &RulesFile) -> Result<Vec<Box<dyn Detector>>, RulesError> {
63 let d = &rules.detectors;
64 let mut out: Vec<Box<dyn Detector>> = Vec::new();
65 if !rules.dictionary.is_empty() {
66 out.push(Box::new(dictionary::Dictionary::new(&rules.dictionary)?));
67 }
68 for rule in &rules.regex {
69 out.push(Box::new(custom::Custom::new(rule)?));
70 }
71 if d.secret.enabled {
72 out.push(Box::new(secret::Secret));
73 }
74 if d.card.enabled {
75 out.push(Box::new(card::Card));
76 }
77 if d.iban.enabled {
78 out.push(Box::new(iban::Iban));
79 }
80 if d.natid.enabled {
81 out.push(Box::new(natid::NatId::new(&d.natid.options.locales)?));
82 }
83 if d.email.enabled {
84 out.push(Box::new(email::Email));
85 }
86 if d.url.enabled {
87 out.push(Box::new(url::Url));
88 }
89 if d.ip.enabled {
90 out.push(Box::new(ip::Ip { include_private: d.ip.options.include_private }));
91 }
92 if d.phone.enabled {
93 #[cfg(feature = "phone")]
94 out.push(Box::new(phone::Phone::new(&d.phone.options.regions)?));
95 #[cfg(not(feature = "phone"))]
96 return Err(RulesError::NotBuiltIn("phone"));
97 }
98 Ok(out)
99}
100
101pub(crate) fn resolve(mut candidates: Vec<Candidate>) -> Vec<Candidate> {
104 candidates.sort_by(|a, b| {
105 (b.end - b.start).cmp(&(a.end - a.start)).then(a.priority.cmp(&b.priority)).then(a.start.cmp(&b.start))
106 });
107 let mut kept: Vec<Candidate> = Vec::new();
108 for c in candidates {
109 if c.start < c.end && kept.iter().all(|k| c.end <= k.start || c.start >= k.end) {
110 kept.push(c);
111 }
112 }
113 kept.sort_by_key(|k| k.start);
114 kept
115}
116
117pub(crate) fn word_bounded(text: &str, start: usize, end: usize) -> bool {
120 let before = text[..start].chars().next_back();
121 let after = text[end..].chars().next();
122 let is_word = |c: char| c.is_alphanumeric() || c == '_';
123 !before.is_some_and(is_word) && !after.is_some_and(is_word)
124}
125
126pub(crate) fn builtin(start: usize, end: usize, kind: &'static str, priority: u8) -> Candidate {
127 Candidate { start, end, kind: EntityKind::builtin(kind), source: Source::BuiltIn, priority }
128}
129
130pub(crate) fn luhn(digits: &str) -> bool {
132 let mut sum = 0;
133 for (i, b) in digits.bytes().rev().enumerate() {
134 let mut d = u32::from(b - b'0');
135 if i % 2 == 1 {
136 d *= 2;
137 if d > 9 {
138 d -= 9;
139 }
140 }
141 sum += d;
142 }
143 sum % 10 == 0
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 fn c(start: usize, end: usize, priority: u8) -> Candidate {
151 builtin(start, end, "EMAIL", priority)
152 }
153
154 #[test]
155 fn longest_wins_then_priority() {
156 let kept = resolve(vec![c(0, 5, 1), c(3, 12, 9), c(12, 14, 1), c(12, 14, 0)]);
157 let spans: Vec<_> = kept.iter().map(|k| (k.start, k.end, k.priority)).collect();
158 assert_eq!(spans, [(3, 12, 9), (12, 14, 0)]);
159 }
160
161 #[test]
162 fn luhn_checks() {
163 assert!(luhn("4111111111111111"));
164 assert!(!luhn("4111111111111112"));
165 assert!(luhn("378282246310005"));
166 }
167
168 #[test]
169 fn word_bounds() {
170 assert!(word_bounded("a acme b", 2, 6));
171 assert!(!word_bounded("a acmes b", 2, 6));
172 assert!(word_bounded("acme", 0, 4));
173 assert!(word_bounded("(acme)", 1, 5));
174 }
175}