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 s.len() >= 11 && (MOBILE_RE.is_match(s) || s.len() == 11 && s.starts_with('1')) {
49 return Value::String(mask_mobile(s));
50 }
51 if s.len() >= 15 && s.len() <= 20 && contains_alpha_numeric(s) {
52 if ID_CARD_RE.is_match(s) {
53 return Value::String(mask_id_card(s));
54 }
55 }
56 if EMAIL_RE.is_match(s) {
57 return Value::String(mask_email(s));
58 }
59 val
60 }
61 _ => val,
62 }
63}
64
65fn contains_alpha_numeric(s: &str) -> bool {
66 s.chars().any(|c| c.is_ascii_digit())
67}
68
69fn mask_mobile(s: &str) -> String {
70 if s.len() < 7 { return s.to_string(); }
71 format!("{}****{}", &s[..3], &s[s.len()-4..])
72}
73
74fn mask_id_card(s: &str) -> String {
75 if s.len() < 8 { return s.to_string(); }
76 format!("{}****{}", &s[..4], &s[s.len()-4..])
77}
78
79fn mask_email(s: &str) -> String {
80 if let Some(at) = s.find('@') {
81 let prefix = &s[..at];
82 if prefix.len() <= 2 { format!("*{}", &s[at..]) }
83 else { format!("{}***{}", &prefix[..1], &s[at..]) }
84 } else { s.to_string() }
85}
86
87pub struct Mask;
97
98impl Mask {
99 pub fn mobile(phone: &str) -> String { mask_mobile(phone) }
101 pub fn email(email: &str) -> String { mask_email(email) }
103 pub fn id_card(id: &str) -> String { mask_id_card(id) }
105 pub fn bank_card(card: &str) -> String {
107 if card.len() < 8 { return card.to_string(); }
108 format!("{} **** {}", &card[..4], &card[card.len()-4..])
109 }
110 pub fn name(name: &str) -> String {
112 let chars: Vec<char> = name.chars().collect();
113 if chars.len() <= 1 { return name.to_string(); }
114 let mut result = String::new();
115 result.push(chars[0]);
116 for _ in 1..chars.len() { result.push('*'); }
117 result
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124 #[test]
125 fn test_mobile() { assert_eq!(Mask::mobile("13812345678"), "138****5678"); }
126 #[test]
127 fn test_email() { assert_eq!(Mask::email("alice@mail.com"), "a***@mail.com"); }
128}