1use std::collections::HashSet;
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
44#[repr(u32)]
45pub enum PatternId {
46 SsnFormatted = 0,
48 SsnUnformatted = 1,
49 EinFormatted = 2,
50 EinUnformatted = 3,
51
52 Email = 10,
54 PhoneUs = 11,
55 PhoneInternational = 12,
56 Url = 13,
57
58 ZipCode = 20,
60 ZipPlus4 = 21,
61 StateCode = 22,
62
63 Cusip = 30,
65 AbaRouting = 31,
66 AccountNumber = 32,
67
68 DateMmDdYyyy = 40,
70 DateYyyyMmDd = 41,
71 DateIso8601 = 42,
72
73 MoneyUsd = 50,
75 MoneyDecimal = 51,
76}
77
78impl PatternId {
79 pub fn as_u32(self) -> u32 {
81 self as u32
82 }
83
84 pub fn from_u32(id: u32) -> Option<Self> {
86 match id {
87 0 => Some(Self::SsnFormatted),
88 1 => Some(Self::SsnUnformatted),
89 2 => Some(Self::EinFormatted),
90 3 => Some(Self::EinUnformatted),
91 10 => Some(Self::Email),
92 11 => Some(Self::PhoneUs),
93 12 => Some(Self::PhoneInternational),
94 13 => Some(Self::Url),
95 20 => Some(Self::ZipCode),
96 21 => Some(Self::ZipPlus4),
97 22 => Some(Self::StateCode),
98 30 => Some(Self::Cusip),
99 31 => Some(Self::AbaRouting),
100 32 => Some(Self::AccountNumber),
101 40 => Some(Self::DateMmDdYyyy),
102 41 => Some(Self::DateYyyyMmDd),
103 42 => Some(Self::DateIso8601),
104 50 => Some(Self::MoneyUsd),
105 51 => Some(Self::MoneyDecimal),
106 _ => None,
107 }
108 }
109}
110
111#[derive(Debug, Default)]
113pub struct MatchResult {
114 pub matches: HashSet<PatternId>,
116}
117
118impl MatchResult {
119 pub fn new() -> Self {
121 Self::default()
122 }
123
124 pub fn contains(&self, id: PatternId) -> bool {
126 self.matches.contains(&id)
127 }
128
129 pub fn has_matches(&self) -> bool {
131 !self.matches.is_empty()
132 }
133
134 pub fn count(&self) -> usize {
136 self.matches.len()
137 }
138}
139
140pub struct PatternDef {
142 pub id: PatternId,
143 pub pattern: &'static str,
144 pub flags: u32,
145}
146
147pub static BUILTIN_PATTERNS: &[PatternDef] = &[
149 PatternDef {
151 id: PatternId::SsnFormatted,
152 pattern: r"^\d{3}-\d{2}-\d{4}$",
153 flags: 0,
154 },
155 PatternDef {
156 id: PatternId::SsnUnformatted,
157 pattern: r"^\d{9}$",
158 flags: 0,
159 },
160 PatternDef {
162 id: PatternId::EinFormatted,
163 pattern: r"^\d{2}-\d{7}$",
164 flags: 0,
165 },
166 PatternDef {
167 id: PatternId::EinUnformatted,
168 pattern: r"^\d{9}$",
169 flags: 0,
170 },
171 PatternDef {
173 id: PatternId::Email,
174 pattern: r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
175 flags: 0, },
177 PatternDef {
179 id: PatternId::PhoneUs,
180 pattern: r"^(\+1)?[-.\s]?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}$",
181 flags: 0,
182 },
183 PatternDef {
184 id: PatternId::PhoneInternational,
185 pattern: r"^\+\d{1,3}[-.\s]?\d{4,14}$",
186 flags: 0,
187 },
188 PatternDef {
190 id: PatternId::ZipCode,
191 pattern: r"^\d{5}$",
192 flags: 0,
193 },
194 PatternDef {
195 id: PatternId::ZipPlus4,
196 pattern: r"^\d{5}-\d{4}$",
197 flags: 0,
198 },
199 PatternDef {
201 id: PatternId::DateMmDdYyyy,
202 pattern: r"^(0[1-9]|1[0-2])/(0[1-9]|[12]\d|3[01])/\d{4}$",
203 flags: 0,
204 },
205 PatternDef {
206 id: PatternId::DateYyyyMmDd,
207 pattern: r"^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$",
208 flags: 0,
209 },
210 PatternDef {
212 id: PatternId::MoneyUsd,
213 pattern: r"^\$?\d{1,3}(,\d{3})*(\.\d{2})?$",
214 flags: 0,
215 },
216 PatternDef {
217 id: PatternId::MoneyDecimal,
218 pattern: r"^-?\d+(\.\d{1,2})?$",
219 flags: 0,
220 },
221 PatternDef {
223 id: PatternId::Cusip,
224 pattern: r"^[A-Z0-9]{9}$",
225 flags: 0,
226 },
227 PatternDef {
228 id: PatternId::AbaRouting,
229 pattern: r"^\d{9}$",
230 flags: 0,
231 },
232];
233
234#[cfg(feature = "vectorscan")]
239mod vectorscan_impl {
240 use super::*;
241 use vectorscan_rs::{BlockDatabase, BlockScanner, Error, Flag, Pattern, Scan};
242
243 pub struct PatternDb {
248 database: BlockDatabase,
249 }
250
251 impl PatternDb {
252 pub fn with_builtins() -> Result<Self, Error> {
254 let patterns: Vec<Pattern> = BUILTIN_PATTERNS
255 .iter()
256 .map(|def| {
257 Pattern::new(
258 def.pattern.as_bytes().to_vec(),
259 Flag::default(),
260 Some(def.id.as_u32()),
261 )
262 })
263 .collect();
264
265 let database = BlockDatabase::new(patterns)?;
266
267 Ok(Self { database })
268 }
269
270 pub fn scan(&self, input: &str) -> MatchResult {
272 let mut result = MatchResult::new();
273
274 let mut scanner = match BlockScanner::new(&self.database) {
276 Ok(s) => s,
277 Err(_) => return result,
278 };
279
280 let scan_result = scanner.scan(input.as_bytes(), |id, _from, _to, _flags| {
281 if let Some(pattern_id) = PatternId::from_u32(id) {
282 result.matches.insert(pattern_id);
283 }
284 Scan::Continue
285 });
286
287 let _ = scan_result;
289
290 result
291 }
292
293 pub fn scan_batch(&self, inputs: &[&str]) -> Vec<MatchResult> {
295 inputs.iter().map(|s| self.scan(s)).collect()
296 }
297 }
298}
299
300#[cfg(feature = "vectorscan")]
301pub use vectorscan_impl::PatternDb;
302
303#[cfg(not(feature = "vectorscan"))]
308mod fallback_impl {
309 use super::*;
310 use std::sync::LazyLock;
311
312 struct CompiledPatterns {
314 patterns: Vec<(PatternId, regex::Regex)>,
315 }
316
317 impl CompiledPatterns {
318 fn new() -> Self {
319 let patterns: Vec<_> = BUILTIN_PATTERNS
320 .iter()
321 .filter_map(|def| regex::Regex::new(def.pattern).ok().map(|r| (def.id, r)))
322 .collect();
323
324 Self { patterns }
325 }
326 }
327
328 static COMPILED: LazyLock<CompiledPatterns> = LazyLock::new(CompiledPatterns::new);
329
330 pub struct PatternDb;
332
333 impl PatternDb {
334 pub fn with_builtins() -> Result<Self, std::convert::Infallible> {
336 let _ = &*COMPILED;
338 Ok(Self)
339 }
340
341 pub fn scan(&self, input: &str) -> MatchResult {
343 let mut result = MatchResult::new();
344
345 for (id, regex) in &COMPILED.patterns {
346 if regex.is_match(input) {
347 result.matches.insert(*id);
348 }
349 }
350
351 result
352 }
353
354 pub fn scan_batch(&self, inputs: &[&str]) -> Vec<MatchResult> {
356 inputs.iter().map(|s| self.scan(s)).collect()
357 }
358 }
359}
360
361#[cfg(not(feature = "vectorscan"))]
362pub use fallback_impl::PatternDb;
363
364pub fn is_ssn_format(s: &str) -> bool {
370 let bytes = s.as_bytes();
371
372 if bytes.len() == 11 {
374 return bytes[3] == b'-'
375 && bytes[6] == b'-'
376 && bytes[..3].iter().all(|b| b.is_ascii_digit())
377 && bytes[4..6].iter().all(|b| b.is_ascii_digit())
378 && bytes[7..].iter().all(|b| b.is_ascii_digit());
379 }
380
381 if bytes.len() == 9 {
383 return bytes.iter().all(|b| b.is_ascii_digit());
384 }
385
386 false
387}
388
389pub fn is_ein_format(s: &str) -> bool {
391 let bytes = s.as_bytes();
392
393 if bytes.len() == 10 {
395 return bytes[2] == b'-'
396 && bytes[..2].iter().all(|b| b.is_ascii_digit())
397 && bytes[3..].iter().all(|b| b.is_ascii_digit());
398 }
399
400 if bytes.len() == 9 {
402 return bytes.iter().all(|b| b.is_ascii_digit());
403 }
404
405 false
406}
407
408pub fn is_zip_format(s: &str) -> bool {
410 let bytes = s.as_bytes();
411
412 if bytes.len() == 5 {
414 return bytes.iter().all(|b| b.is_ascii_digit());
415 }
416
417 if bytes.len() == 10 {
419 return bytes[5] == b'-'
420 && bytes[..5].iter().all(|b| b.is_ascii_digit())
421 && bytes[6..].iter().all(|b| b.is_ascii_digit());
422 }
423
424 false
425}
426
427pub fn is_email_format(s: &str) -> bool {
429 let at_pos = match s.find('@') {
431 Some(pos) if pos > 0 && pos < s.len() - 1 => pos,
432 _ => return false,
433 };
434
435 let domain = &s[at_pos + 1..];
436 domain.contains('.') && !domain.starts_with('.') && !domain.ends_with('.')
437}
438
439#[cfg(test)]
444mod tests {
445 use super::*;
446
447 #[test]
448 fn test_ssn_format() {
449 assert!(is_ssn_format("123-45-6789"));
450 assert!(is_ssn_format("123456789"));
451 assert!(!is_ssn_format("12345678"));
452 assert!(!is_ssn_format("1234567890"));
453 assert!(!is_ssn_format("12-345-6789"));
454 }
455
456 #[test]
457 fn test_ein_format() {
458 assert!(is_ein_format("12-3456789"));
459 assert!(is_ein_format("123456789"));
460 assert!(!is_ein_format("123-456789"));
461 assert!(!is_ein_format("12345678"));
462 }
463
464 #[test]
465 fn test_zip_format() {
466 assert!(is_zip_format("12345"));
467 assert!(is_zip_format("12345-6789"));
468 assert!(!is_zip_format("1234"));
469 assert!(!is_zip_format("123456"));
470 assert!(!is_zip_format("12345-678"));
471 }
472
473 #[test]
474 fn test_email_format() {
475 assert!(is_email_format("test@example.com"));
476 assert!(is_email_format("a@b.c"));
477 assert!(is_email_format("user.name+tag@example.org"));
478 assert!(!is_email_format("invalid"));
479 assert!(!is_email_format("@example.com"));
480 assert!(!is_email_format("test@"));
481 assert!(!is_email_format("test@.com"));
482 assert!(!is_email_format("test@com."));
483 }
484
485 #[test]
486 fn test_pattern_db() {
487 let db = PatternDb::with_builtins().unwrap();
488
489 let result = db.scan("123-45-6789");
490 assert!(result.contains(PatternId::SsnFormatted));
491
492 let result = db.scan("12345");
493 assert!(result.contains(PatternId::ZipCode));
494
495 let result = db.scan("test@example.com");
496 assert!(result.contains(PatternId::Email));
497 }
498
499 #[test]
500 fn test_batch_scan() {
501 let db = PatternDb::with_builtins().unwrap();
502
503 let inputs = vec!["123-45-6789", "test@example.com", "12345"];
504 let results = db.scan_batch(&inputs);
505
506 assert!(results[0].contains(PatternId::SsnFormatted));
507 assert!(results[1].contains(PatternId::Email));
508 assert!(results[2].contains(PatternId::ZipCode));
509 }
510}