Skip to main content

string_analyze/rules/core/
pii.rs

1//string_analyze/src/rules/core/pii.rs
2use crate::rules::lazy_rule;
3
4fn luhn_check(s: &str) -> bool {
5    let digits: Vec<u32> = s
6        .chars()
7        .filter(|c| c.is_ascii_digit())
8        .filter_map(|c| c.to_digit(10))
9        .collect();
10
11    if digits.len() < 13 || digits.len() > 19 {
12        return false;
13    }
14
15    let checksum: u32 = digits
16        .iter()
17        .rev()
18        .enumerate()
19        .map(|(idx, &d)| {
20            if idx % 2 == 1 {
21                let doubled = d * 2;
22                if doubled > 9 { doubled - 9 } else { doubled }
23            } else {
24                d
25            }
26        })
27        .sum();
28
29    checksum % 10 == 0
30}
31fn is_valid_bin(card: &str) -> bool {
32    if card.len() < 6 {
33        return false;
34    }
35    const VALID_BINS: &[&str] = &[
36        "62",   // 银联
37        "4",    // Visa
38        "5",    // Mastercard
39        "35",   // JCB
40        "37",   // Amex
41        "6011", // Discover
42        "622",  // 中国银联
43    ];
44
45    VALID_BINS.iter().any(|&bin| card.starts_with(bin))
46}
47
48fn id_card_check(id: &str) -> bool {
49    if id.len() != 18 {
50        return false;
51    }
52
53    let weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
54    let check_codes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];
55
56    let sum: u32 = id[..17]
57        .chars()
58        .filter_map(|c| c.to_digit(10))
59        .enumerate()
60        .map(|(i, d)| d * weights[i])
61        .sum();
62
63    let expected = check_codes[(sum % 11) as usize];
64    let actual = id.chars().nth(17).unwrap_or(' ').to_ascii_uppercase();
65
66    expected == actual
67}
68
69lazy_rule!(
70    RE_CREDIT_CARD = r"\b[1-9]\d{12,18}\b",
71    "发现 疑似信用卡号",
72    90,
73    |input, _| is_valid_bin(input) && luhn_check(input)
74);
75
76lazy_rule!(
77    RE_CN_ID_CARD =
78        r#"[1-9]\d{5}(?:19|20)\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\d|3[01])\d{3}[\dXx]"#,
79    "发现 疑似中国大陆身份证号",
80    90,
81    |input, _| id_card_check(input)
82);
83
84lazy_rule!(
85    RE_CN_PHONE = r#"\b1[3-9]\d{9}\b"#,
86    "发现 疑似手机号",
87    60,
88    ((10, false, r"(?i)(order|id|stamp|time)"),)
89);
90lazy_rule!(
91    RE_PASSPORT = r#"\b[A-Z]{1,2}[0-9]{6,9}\b"#,
92    "发现 疑似护照号",
93    90
94);
95lazy_rule!(
96    RE_HKID = r#"\b[A-Z]{1,2}\d{6}\([0-9A]\)"#,
97    "发现 香港身份证号",
98    90
99);
100lazy_rule!(
101    RE_EMAIL = r#"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b"#,
102    "发现 邮箱地址",
103    60
104);
105
106fn usci_check(s: &str) -> bool {
107    // 1. 拦截全零或全重复字符的误报
108    if s.len() != 18 || s.chars().all(|c| c == '0') {
109        return false;
110    }
111
112    let dict = "0123456789ABCDEFGHJKLMNPQRTUWXY";
113    let weights = [
114        1, 3, 9, 27, 19, 26, 16, 17, 20, 29, 25, 14, 12, 5, 15, 14, 12,
115    ];
116
117    let mut sum = 0;
118    for (i, c) in s[..17].chars().enumerate() {
119        if let Some(val) = dict.find(c) {
120            sum += val * weights[i];
121        } else {
122            return false;
123        }
124    }
125
126    let check_val = 31 - (sum % 31);
127    let check_char = if check_val == 31 {
128        '0'
129    } else {
130        dict.chars().nth(check_val).unwrap()
131    };
132
133    s.ends_with(check_char)
134}
135
136lazy_rule!(
137    RE_CN_USCI = r#"\b[1-9A-HJ-NPQRTUWXY][0-9A-HJ-NPQRTUWXY]\d{6}[0-9A-HJ-NPQRTUWXY]{10}\b"#,
138    "发现 统一社会信用代码",
139    90,
140    |input, _| usci_check(input)
141);
142
143lazy_rule!(
144    RE_TW_ID = r"\b[A-Z][12]\d{8}\b",
145    "发现 台湾身份证号",
146    90,
147    |input, _| tw_id_check(input)
148);
149
150fn tw_id_check(id: &str) -> bool {
151    if id.len() != 10 {
152        return false;
153    }
154
155    // 第一个字母对应数字
156    let first_num = match id.chars().next() {
157        Some('A') => 10,
158        Some('B') => 11,
159        Some('C') => 12,
160        Some('D') => 13,
161        Some('E') => 14,
162        Some('F') => 15,
163        Some('G') => 16,
164        Some('H') => 17,
165        Some('I') => 34,
166        Some('J') => 18,
167        Some('K') => 19,
168        Some('L') => 20,
169        Some('M') => 21,
170        Some('N') => 22,
171        Some('O') => 35,
172        Some('P') => 23,
173        Some('Q') => 24,
174        Some('R') => 25,
175        Some('S') => 26,
176        Some('T') => 27,
177        Some('U') => 28,
178        Some('V') => 29,
179        Some('W') => 32,
180        Some('X') => 30,
181        Some('Y') => 31,
182        Some('Z') => 33,
183        _ => return false,
184    };
185
186    let weights = [1, 9, 8, 7, 6, 5, 4, 3, 2, 1, 1];
187    let sum: u32 = (first_num / 10) * weights[0]
188        + (first_num % 10) * weights[1]
189        + id[1..]
190            .chars()
191            .filter_map(|c| c.to_digit(10))
192            .zip(&weights[2..])
193            .map(|(d, &w)| d * w)
194            .sum::<u32>();
195
196    sum % 10 == 0
197}
198
199lazy_rule!(RE_MO_ID = r"\b[1578]\d{6}\(\d\)\b", "发现 澳门身份证号", 90);