1use serde::{Deserialize, Serialize};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
21pub enum MaskingRule {
22 Phone,
23 Email,
24 IdCard,
25 BankCard,
26 Name,
27 Address,
28 Custom(String),
29}
30
31pub struct DataMasker;
32
33impl DataMasker {
34 pub fn apply(rule: &MaskingRule, value: &str) -> String {
40 match rule {
41 MaskingRule::Phone => mask_prefix_suffix(value, 3, 4),
42 MaskingRule::Email => mask_email(value),
43 MaskingRule::IdCard => mask_prefix_suffix(value, 4, 4),
44 MaskingRule::BankCard => mask_prefix_suffix(value, 4, 4),
45 MaskingRule::Name => mask_name(value),
46 MaskingRule::Address => mask_address(value, 6),
47 MaskingRule::Custom(spec) => mask_custom(value, spec),
48 }
49 }
50}
51
52fn mask_prefix_suffix(value: &str, prefix: usize, suffix: usize) -> String {
57 let chars: Vec<char> = value.chars().collect();
58 let len = chars.len();
59 if len == 0 {
60 return "***".to_string();
61 }
62 if len <= prefix + suffix {
65 return "***".to_string();
67 }
68 let hidden = len - prefix - suffix;
69 let mut out = String::with_capacity(len);
70 for &c in &chars[..prefix] {
71 out.push(c);
72 }
73 for _ in 0..hidden {
74 out.push('*');
75 }
76 for &c in &chars[len - suffix..] {
77 out.push(c);
78 }
79 out
80}
81
82fn mask_email(value: &str) -> String {
83 let parts: Vec<&str> = value.splitn(2, '@').collect();
84 if parts.len() != 2 {
85 return "***".to_string();
87 }
88 let local = parts[0];
89 let domain = parts[1];
90 let local_chars: Vec<char> = local.chars().collect();
91 if local_chars.is_empty() {
92 return "***".to_string();
93 }
94 let mut out = String::with_capacity(value.len());
95 out.push(local_chars[0]);
96 for _ in 1..local_chars.len() {
98 out.push('*');
99 }
100 out.push('@');
101 out.push_str(domain);
102 out
103}
104
105fn mask_name(value: &str) -> String {
106 let chars: Vec<char> = value.chars().collect();
107 if chars.is_empty() {
108 return String::new();
109 }
110 let mut out = String::with_capacity(chars.len());
111 out.push(chars[0]);
112 for _ in 1..chars.len() {
113 out.push('*');
114 }
115 out
116}
117
118fn mask_address(value: &str, keep: usize) -> String {
119 let chars: Vec<char> = value.chars().collect();
120 if chars.is_empty() {
121 return String::new();
122 }
123 if chars.len() <= keep {
124 return "*".repeat(chars.len());
127 }
128 let hidden = chars.len() - keep;
129 let mut out = String::with_capacity(chars.len());
130 for &c in &chars[..keep] {
131 out.push(c);
132 }
133 for _ in 0..hidden {
134 out.push('*');
135 }
136 out
137}
138
139fn mask_custom(value: &str, spec: &str) -> String {
140 let (prefix, suffix) = match parse_custom_spec(spec) {
141 Some(parsed) => parsed,
142 None => return "***".to_string(),
143 };
144 mask_prefix_suffix(value, prefix, suffix)
145}
146
147fn parse_custom_spec(spec: &str) -> Option<(usize, usize)> {
150 let parts: Vec<&str> = spec.split(',').collect();
151 if parts.len() != 2 {
152 return None;
153 }
154 let prefix: usize = parts[0].trim().parse().ok()?;
155 let suffix: usize = parts[1].trim().parse().ok()?;
156 Some((prefix, suffix))
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[test]
165 fn test_phone_standard() {
166 let result = DataMasker::apply(&MaskingRule::Phone, "13812345678");
167 assert_eq!(result, "138****5678");
168 }
169
170 #[test]
171 fn test_phone_too_short() {
172 assert_eq!(DataMasker::apply(&MaskingRule::Phone, "12345"), "***");
174 assert_eq!(DataMasker::apply(&MaskingRule::Phone, "1234567"), "***");
175 }
176
177 #[test]
178 fn test_phone_boundary_seven_plus_one() {
179 assert_eq!(
181 DataMasker::apply(&MaskingRule::Phone, "12345678"),
182 "123*5678"
183 );
184 }
185
186 #[test]
187 fn test_phone_empty() {
188 assert_eq!(DataMasker::apply(&MaskingRule::Phone, ""), "***");
189 }
190
191 #[test]
193 fn test_email_standard() {
194 assert_eq!(
195 DataMasker::apply(&MaskingRule::Email, "test@example.com"),
196 "t***@example.com"
197 );
198 }
199
200 #[test]
201 fn test_email_single_char_local() {
202 assert_eq!(
203 DataMasker::apply(&MaskingRule::Email, "a@example.com"),
204 "a@example.com"
205 );
206 }
207
208 #[test]
209 fn test_email_no_at() {
210 assert_eq!(DataMasker::apply(&MaskingRule::Email, "notanemail"), "***");
211 }
212
213 #[test]
214 fn test_email_empty_local() {
215 assert_eq!(
216 DataMasker::apply(&MaskingRule::Email, "@example.com"),
217 "***"
218 );
219 }
220
221 #[test]
223 fn test_idcard_standard_18() {
224 let id = "110101199001012345";
225 let masked = DataMasker::apply(&MaskingRule::IdCard, id);
226 assert_eq!(masked, "1101**********2345");
228 assert_eq!(masked.len(), id.len());
229 }
230
231 #[test]
232 fn test_idcard_too_short() {
233 assert_eq!(DataMasker::apply(&MaskingRule::IdCard, "1234567"), "***");
234 assert_eq!(DataMasker::apply(&MaskingRule::IdCard, "12345678"), "***");
235 }
236
237 #[test]
238 fn test_idcard_empty() {
239 assert_eq!(DataMasker::apply(&MaskingRule::IdCard, ""), "***");
240 }
241
242 #[test]
244 fn test_bankcard_standard_16() {
245 let card = "6222020200112345";
246 let masked = DataMasker::apply(&MaskingRule::BankCard, card);
247 assert_eq!(masked, "6222********2345");
248 }
249
250 #[test]
251 fn test_bankcard_too_short() {
252 assert_eq!(DataMasker::apply(&MaskingRule::BankCard, "1234567"), "***");
253 }
254
255 #[test]
256 fn test_bankcard_empty() {
257 assert_eq!(DataMasker::apply(&MaskingRule::BankCard, ""), "***");
258 }
259
260 #[test]
262 fn test_name_chinese_two_chars() {
263 assert_eq!(DataMasker::apply(&MaskingRule::Name, "张三"), "张*");
264 }
265
266 #[test]
267 fn test_name_chinese_three_chars() {
268 assert_eq!(DataMasker::apply(&MaskingRule::Name, "诸葛亮"), "诸**");
269 }
270
271 #[test]
272 fn test_name_single_char() {
273 assert_eq!(DataMasker::apply(&MaskingRule::Name, "李"), "李");
274 }
275
276 #[test]
277 fn test_name_empty() {
278 assert_eq!(DataMasker::apply(&MaskingRule::Name, ""), "");
279 }
280
281 #[test]
282 fn test_name_english() {
283 assert_eq!(DataMasker::apply(&MaskingRule::Name, "Alice"), "A****");
284 }
285
286 #[test]
288 fn test_address_standard() {
289 let addr = "北京市海淀区中关村大街1号";
290 let masked = DataMasker::apply(&MaskingRule::Address, addr);
291 let expected = "北京市海淀区*******";
293 assert_eq!(masked, expected);
294 assert_eq!(masked.chars().count(), addr.chars().count());
295 }
296
297 #[test]
298 fn test_address_exactly_six_chars() {
299 let addr = "北京市海淀区";
300 assert_eq!(DataMasker::apply(&MaskingRule::Address, addr), "******");
301 }
302
303 #[test]
304 fn test_address_short() {
305 assert_eq!(DataMasker::apply(&MaskingRule::Address, "北京"), "**");
306 }
307
308 #[test]
309 fn test_address_empty() {
310 assert_eq!(DataMasker::apply(&MaskingRule::Address, ""), "");
311 }
312
313 #[test]
315 fn test_custom_prefix_suffix() {
316 let rule = MaskingRule::Custom("3,2".to_string());
317 assert_eq!(DataMasker::apply(&rule, "ABCDEFGHIJ"), "ABC*****IJ");
318 }
319
320 #[test]
321 fn test_custom_too_short() {
322 let rule = MaskingRule::Custom("4,4".to_string());
323 assert_eq!(DataMasker::apply(&rule, "ABC"), "***");
324 }
325
326 #[test]
327 fn test_custom_invalid_spec() {
328 let rule = MaskingRule::Custom("not_a_number".to_string());
329 assert_eq!(DataMasker::apply(&rule, "ABCDEF"), "***");
330 }
331
332 #[test]
333 fn test_custom_invalid_spec_two_parts() {
334 let rule = MaskingRule::Custom("1,2,3".to_string());
335 assert_eq!(DataMasker::apply(&rule, "ABCDEF"), "***");
336 }
337
338 #[test]
339 fn test_custom_empty_value() {
340 let rule = MaskingRule::Custom("2,2".to_string());
341 assert_eq!(DataMasker::apply(&rule, ""), "***");
342 }
343
344 #[test]
346 fn test_unicode_no_panic() {
347 let value = "你好🌍世界AB";
349 let masked = DataMasker::apply(&MaskingRule::Address, value);
350 assert!(masked.contains('*'));
351 }
352
353 #[test]
354 fn test_long_string() {
355 let value = "1".repeat(10000);
356 let masked = DataMasker::apply(&MaskingRule::Phone, &value);
357 assert!(masked.starts_with("111"));
359 assert!(masked.ends_with("1111"));
360 assert_eq!(masked.matches('*').count(), 10000 - 7);
361 }
362
363 #[test]
364 fn test_single_char_inputs() {
365 assert_eq!(DataMasker::apply(&MaskingRule::Phone, "1"), "***");
366 assert_eq!(DataMasker::apply(&MaskingRule::IdCard, "1"), "***");
367 assert_eq!(DataMasker::apply(&MaskingRule::BankCard, "1"), "***");
368 assert_eq!(DataMasker::apply(&MaskingRule::Name, "张"), "张");
369 assert_eq!(DataMasker::apply(&MaskingRule::Address, "张"), "*");
370 }
371}