inkly_core/
lib.rs

1use regex_lite::Regex;
2use serde::{Deserialize, Serialize};
3use wasm_bindgen::prelude::*;
4
5// Initialize panic hook for better error messages
6#[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() // Simplified for WASM size
61            }
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(), // Format: XX1234567
64            Rule::Custom { regex, .. } => Regex::new(regex).unwrap_or_else(|_| Regex::new(r"a^").unwrap()), // Never matches if invalid
65        }
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    // Sort spans by start position
88    spans.sort_by_key(|span| span.start);
89
90    // Convert to JS
91    Ok(serde_wasm_bindgen::to_value(&spans)?)
92}
93
94// Helper function to check if a credit card number passes Luhn algorithm
95// Not exposed to JS to keep WASM size small
96fn 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// Include tests module
126#[cfg(test)]
127mod tests;