Skip to main content

teaql_tool_std/
desensitize.rs

1pub struct DesensitizeTool;
2
3impl DesensitizeTool {
4    pub fn new() -> Self {
5        Self
6    }
7
8    // ================== Chinese Formats ==================
9
10    /// Desensitize Chinese ID Card (e.g. 110105199001011234 -> 110105********1234)
11    pub fn chinese_id_card(&self, id_card: impl AsRef<str>) -> String {
12        let s = id_card.as_ref();
13        if s.len() == 18 {
14            format!("{}********{}", &s[0..6], &s[14..18])
15        } else if s.len() == 15 {
16            format!("{}******{}", &s[0..6], &s[12..15])
17        } else {
18            s.to_string()
19        }
20    }
21
22    /// Desensitize Chinese Phone (e.g. 13812345678 -> 138****5678)
23    pub fn chinese_phone(&self, phone: impl AsRef<str>) -> String {
24        let s = phone.as_ref();
25        if s.len() == 11 {
26            format!("{}****{}", &s[0..3], &s[7..11])
27        } else {
28            s.to_string()
29        }
30    }
31
32    /// Desensitize Chinese Name (e.g. 张三 -> 张*, 王大拿 -> 王*拿)
33    pub fn chinese_name(&self, name: impl AsRef<str>) -> String {
34        let s = name.as_ref();
35        let chars: Vec<char> = s.chars().collect();
36        match chars.len() {
37            0 => String::new(),
38            1 => s.to_string(),
39            2 => format!("{}*", chars[0]),
40            _ => format!("{}*{}", chars[0], chars[chars.len() - 1]),
41        }
42    }
43
44    // ================== US / International Formats ==================
45
46    /// Desensitize US Social Security Number (SSN) (e.g. 123-45-6789 -> ***-**-6789)
47    pub fn us_ssn(&self, ssn: impl AsRef<str>) -> String {
48        let s = ssn.as_ref();
49        if s.len() == 11 && s.chars().nth(3) == Some('-') && s.chars().nth(6) == Some('-') {
50            format!("***-**-{}", &s[7..11])
51        } else if s.len() == 9 {
52            format!("*****{}", &s[5..9])
53        } else {
54            s.to_string()
55        }
56    }
57
58    /// Desensitize US Phone Number (e.g. +1-555-123-4567 -> +1-***-***-4567 or 555-123-4567 -> ***-***-4567)
59    pub fn us_phone(&self, phone: impl AsRef<str>) -> String {
60        let s = phone.as_ref();
61        // A simple approach: mask all digits except the last 4
62        let mut result = String::with_capacity(s.len());
63        let digit_count = s.chars().filter(|c| c.is_ascii_digit()).count();
64        let mut digits_seen = 0;
65        
66        for c in s.chars() {
67            if c.is_ascii_digit() {
68                digits_seen += 1;
69                if digits_seen <= digit_count.saturating_sub(4) {
70                    result.push('*');
71                } else {
72                    result.push(c);
73                }
74            } else {
75                result.push(c);
76            }
77        }
78        result
79    }
80
81    /// Desensitize Credit/Debit Card (e.g. 1234 5678 1234 5678 -> **** **** **** 5678)
82    pub fn credit_card(&self, card: impl AsRef<str>) -> String {
83        let s = card.as_ref();
84        let mut result = String::with_capacity(s.len());
85        let digit_count = s.chars().filter(|c| c.is_ascii_digit()).count();
86        let mut digits_seen = 0;
87
88        for c in s.chars() {
89            if c.is_ascii_digit() {
90                digits_seen += 1;
91                if digits_seen <= digit_count.saturating_sub(4) {
92                    result.push('*');
93                } else {
94                    result.push(c);
95                }
96            } else {
97                result.push(c);
98            }
99        }
100        result
101    }
102
103    // ================== Generic ==================
104
105    /// Desensitize Email (e.g. user@example.com -> u***@example.com)
106    pub fn email(&self, email: impl AsRef<str>) -> String {
107        let s = email.as_ref();
108        if let Some(idx) = s.find('@') {
109            let prefix = &s[..idx];
110            let domain = &s[idx..];
111            if prefix.len() <= 1 {
112                format!("*{}", domain)
113            } else {
114                format!("{}***{}", &prefix[..1], domain)
115            }
116        } else {
117            s.to_string()
118        }
119    }
120
121    /// Mask password to fixed asterisks
122    pub fn password(&self, _pwd: impl AsRef<str>) -> String {
123        "******".to_string()
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn test_desensitize() {
133        let t = DesensitizeTool::new();
134        assert_eq!(t.chinese_phone("13812345678"), "138****5678");
135        assert_eq!(t.chinese_id_card("110105199001011234"), "110105********1234");
136        assert_eq!(t.chinese_name("张三"), "张*");
137        assert_eq!(t.chinese_name("王大拿"), "王*拿");
138        assert_eq!(t.us_ssn("123-45-6789"), "***-**-6789");
139        assert_eq!(t.us_phone("+1-555-123-4567"), "+*-***-***-4567");
140        assert_eq!(t.credit_card("1234-5678-1234-5678"), "****-****-****-5678");
141        assert_eq!(t.email("john.doe@example.com"), "j***@example.com");
142    }
143}