1use regex::Regex;
4use serde_json::{Map, Value};
5use std::sync::LazyLock;
6
7static MOBILE_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^1[3-9]\d{9}$").unwrap());
8static ID_CARD_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\d{17}[\dXx]$").unwrap());
9static EMAIL_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^[\w.\-]+@[\w.\-]+\.\w+$").unwrap());
10
11pub fn mask_json_value(value: Value, sensitive_fields: &[&str]) -> Value {
21 match value {
22 Value::Object(map) => mask_object(map, sensitive_fields),
23 Value::Array(arr) => Value::Array(arr.into_iter().map(|v| mask_json_value(v, sensitive_fields)).collect()),
24 other => mask_scalar_if_needed(other),
25 }
26}
27
28fn mask_object(map: Map<String, Value>, sensitive_fields: &[&str]) -> Value {
29 let mut masked = Map::new();
30 for (key, val) in map {
31 let is_sensitive = sensitive_fields.iter().any(|f| {
32 f.eq_ignore_ascii_case(&key)
33 });
34 if is_sensitive {
35 masked.insert(key, Value::String("****".into()));
36 } else {
37 masked.insert(key, mask_json_value(val, sensitive_fields));
38 }
39 }
40 Value::Object(masked)
41}
42
43fn mask_scalar_if_needed(val: Value) -> Value {
44 match &val {
45 Value::String(s) => {
46 let s = s.trim();
47 if s.is_empty() { return val; }
48 if MOBILE_RE.is_match(s) {
50 return Value::String(mask_mobile(s));
51 }
52 if s.len() >= 15 && s.len() <= 20 && contains_alpha_numeric(s) {
53 if ID_CARD_RE.is_match(s) {
54 return Value::String(mask_id_card(s));
55 }
56 }
57 if EMAIL_RE.is_match(s) {
58 return Value::String(mask_email(s));
59 }
60 val
61 }
62 _ => val,
63 }
64}
65
66fn contains_alpha_numeric(s: &str) -> bool {
67 s.chars().any(|c| c.is_ascii_digit())
68}
69
70fn mask_mobile(s: &str) -> String {
71 let chars: Vec<char> = s.chars().collect();
73 if chars.len() < 7 { return s.to_string(); }
74 let prefix: String = chars[..3].iter().collect();
75 let suffix: String = chars[chars.len() - 4..].iter().collect();
76 format!("{}****{}", prefix, suffix)
77}
78
79fn mask_id_card(s: &str) -> String {
80 let chars: Vec<char> = s.chars().collect();
81 if chars.len() < 8 { return s.to_string(); }
82 let prefix: String = chars[..4].iter().collect();
83 let suffix: String = chars[chars.len() - 4..].iter().collect();
84 format!("{}****{}", prefix, suffix)
85}
86
87fn mask_email(s: &str) -> String {
88 if let Some(at) = s.find('@') {
89 let prefix = &s[..at];
90 if prefix.len() <= 2 { format!("*{}", &s[at..]) }
91 else { format!("{}***{}", &prefix[..1], &s[at..]) }
92 } else { s.to_string() }
93}
94
95pub struct Mask;
105
106impl Mask {
107 pub fn mobile(phone: &str) -> String { mask_mobile(phone) }
109 pub fn email(email: &str) -> String { mask_email(email) }
111 pub fn id_card(id: &str) -> String { mask_id_card(id) }
113 pub fn bank_card(card: &str) -> String {
115 if card.len() < 8 { return card.to_string(); }
116 format!("{} **** {}", &card[..4], &card[card.len()-4..])
117 }
118 pub fn name(name: &str) -> String {
120 let chars: Vec<char> = name.chars().collect();
121 if chars.len() <= 1 { return name.to_string(); }
122 let mut result = String::new();
123 result.push(chars[0]);
124 for _ in 1..chars.len() { result.push('*'); }
125 result
126 }
127 pub fn user_id(id: &str) -> String {
129 if id.len() <= 4 { return "****".to_string(); }
130 format!("{}****{}", &id[..2], &id[id.len()-2..])
131 }
132 pub fn password(_password: &str) -> String {
134 "******".to_string()
135 }
136 pub fn address(addr: &str) -> String {
138 if addr.len() <= 6 { return "****".to_string(); }
139 format!("{}****", &addr[..addr.len().min(6)])
140 }
141 pub fn license_plate(plate: &str) -> String {
143 let chars: Vec<char> = plate.chars().collect();
144 if chars.len() < 3 { return "****".to_string(); }
145 format!("{}****{}", chars[0], chars[chars.len()-1])
146 }
147 pub fn mask_by_type(data_type: &str, value: &str) -> String {
153 match data_type {
154 "user_id" => Self::user_id(value),
155 "mobile" => Self::mobile(value),
156 "password" => Self::password(value),
157 "address" => Self::address(value),
158 "bank_card" => Self::bank_card(value),
159 "license_plate" => Self::license_plate(value),
160 "email" => Self::email(value),
161 "id_card" => Self::id_card(value),
162 "name" => Self::name(value),
163 _ => format!("{}***{}", &value[..value.len().min(2)], &value[value.len()-value.len().min(2)..]),
164 }
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 #[test]
172 fn test_mobile() { assert_eq!(Mask::mobile("13812345678"), "138****5678"); }
173 #[test]
174 fn test_email() { assert_eq!(Mask::email("alice@mail.com"), "a***@mail.com"); }
175}