1use regex_lite::Regex;
2use serde::{Deserialize, Serialize};
3use wasm_bindgen::prelude::*;
4
5#[wasm_bindgen(start)]
7pub fn start() {
8 console_error_panic_hook::set_once();
9}
10
11#[derive(Serialize, Deserialize, Debug, Clone)]
12pub struct Span {
13 pub start: usize,
14 pub end: usize,
15 pub type_: String,
16}
17
18#[wasm_bindgen]
19extern "C" {
20 #[wasm_bindgen(typescript_type = "Rule[]")]
21 pub type RuleArray;
22}
23
24#[derive(Serialize, Deserialize, Debug, Clone)]
25#[serde(tag = "rule_type", rename_all = "camelCase")]
26pub enum Rule {
27 #[serde(rename = "pii.email")]
28 Email,
29 #[serde(rename = "pii.ssn")]
30 Ssn,
31 #[serde(rename = "pii.credit_card")]
32 CreditCard,
33 #[serde(rename = "pii.phone")]
34 Phone,
35 #[serde(rename = "pii.id.somali_nid")]
36 SomaliNid,
37 #[serde(rename_all = "camelCase")]
38 Custom { type_: String, regex: String },
39}
40
41impl Rule {
42 fn get_type(&self) -> String {
43 match self {
44 Rule::Email => "pii.email".to_string(),
45 Rule::Ssn => "pii.ssn".to_string(),
46 Rule::CreditCard => "pii.credit_card".to_string(),
47 Rule::Phone => "pii.phone".to_string(),
48 Rule::SomaliNid => "pii.id.somali_nid".to_string(),
49 Rule::Custom { type_, .. } => type_.clone(),
50 }
51 }
52
53 fn get_regex(&self) -> Regex {
54 match self {
55 Rule::Email => {
56 Regex::new(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}").unwrap()
57 }
58 Rule::Ssn => Regex::new(r"\b\d{3}[-]?\d{2}[-]?\d{4}\b").unwrap(),
59 Rule::CreditCard => {
60 Regex::new(r"\b(?:\d[ -]*?){13,16}\b").unwrap() }
62 Rule::Phone => Regex::new(r"\b(?:\+\d{1,3}[-\s]?)?\(?\d{3}\)?[-\s]?\d{3}[-\s]?\d{4}\b").unwrap(),
63 Rule::SomaliNid => Regex::new(r"\b[A-Z]{2}\d{7}\b").unwrap(), Rule::Custom { regex, .. } => Regex::new(regex).unwrap_or_else(|_| Regex::new(r"a^").unwrap()), }
66 }
67}
68
69#[wasm_bindgen]
70pub fn find_pii(text: &str, rules_js: RuleArray) -> Result<JsValue, JsValue> {
71 let rules: Vec<Rule> = serde_wasm_bindgen::from_value(rules_js.into())?;
72 let mut spans = Vec::new();
73
74 for rule in rules {
75 let rule_type = rule.get_type();
76 let regex = rule.get_regex();
77
78 for mat in regex.find_iter(text) {
79 spans.push(Span {
80 start: mat.start(),
81 end: mat.end(),
82 type_: rule_type.clone(),
83 });
84 }
85 }
86
87 spans.sort_by_key(|span| span.start);
89
90 Ok(serde_wasm_bindgen::to_value(&spans)?)
92}
93
94fn is_valid_credit_card(number: &str) -> bool {
97 let digits: Vec<u32> = number
98 .chars()
99 .filter(|c| c.is_digit(10))
100 .map(|c| c.to_digit(10).unwrap())
101 .collect();
102
103 if digits.len() < 13 || digits.len() > 19 {
104 return false;
105 }
106
107 let mut sum = 0;
108 let mut double = false;
109
110 for &digit in digits.iter().rev() {
111 let mut value = digit;
112 if double {
113 value *= 2;
114 if value > 9 {
115 value -= 9;
116 }
117 }
118 sum += value;
119 double = !double;
120 }
121
122 sum % 10 == 0
123}
124
125#[cfg(test)]
127mod tests;