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 Ip,
29 Imei,
30 Plate,
31 Custom(String),
32}
33
34pub struct DataMasker;
35
36impl DataMasker {
37 pub fn apply(rule: &MaskingRule, value: &str) -> String {
43 match rule {
44 MaskingRule::Phone => mask_prefix_suffix(value, 3, 4),
45 MaskingRule::Email => mask_email(value),
46 MaskingRule::IdCard => mask_prefix_suffix(value, 4, 4),
47 MaskingRule::BankCard => mask_prefix_suffix(value, 4, 4),
48 MaskingRule::Name => mask_name(value),
49 MaskingRule::Address => mask_address(value, 6),
50 MaskingRule::Ip => mask_ip(value),
51 MaskingRule::Imei => mask_imei(value),
52 MaskingRule::Plate => mask_plate(value),
53 MaskingRule::Custom(spec) => mask_custom(value, spec),
54 }
55 }
56}
57
58fn mask_prefix_suffix(value: &str, prefix: usize, suffix: usize) -> String {
63 let chars: Vec<char> = value.chars().collect();
64 let len = chars.len();
65 if len == 0 {
66 return "***".to_string();
67 }
68 if len <= prefix + suffix {
71 return "***".to_string();
73 }
74 let hidden = len - prefix - suffix;
75 let mut out = String::with_capacity(len);
76 for &c in &chars[..prefix] {
77 out.push(c);
78 }
79 for _ in 0..hidden {
80 out.push('*');
81 }
82 for &c in &chars[len - suffix..] {
83 out.push(c);
84 }
85 out
86}
87
88fn mask_email(value: &str) -> String {
89 let parts: Vec<&str> = value.splitn(2, '@').collect();
90 if parts.len() != 2 {
91 return "***".to_string();
93 }
94 let local = parts[0];
95 let domain = parts[1];
96 let local_chars: Vec<char> = local.chars().collect();
97 if local_chars.is_empty() {
98 return "***".to_string();
99 }
100 let mut out = String::with_capacity(value.len());
101 out.push(local_chars[0]);
102 for _ in 1..local_chars.len() {
104 out.push('*');
105 }
106 out.push('@');
107 out.push_str(domain);
108 out
109}
110
111fn mask_name(value: &str) -> String {
112 let chars: Vec<char> = value.chars().collect();
113 if chars.is_empty() {
114 return String::new();
115 }
116 let mut out = String::with_capacity(chars.len());
117 out.push(chars[0]);
118 for _ in 1..chars.len() {
119 out.push('*');
120 }
121 out
122}
123
124fn mask_address(value: &str, keep: usize) -> String {
125 let chars: Vec<char> = value.chars().collect();
126 if chars.is_empty() {
127 return String::new();
128 }
129 if chars.len() <= keep {
130 return "*".repeat(chars.len());
133 }
134 let hidden = chars.len() - keep;
135 let mut out = String::with_capacity(chars.len());
136 for &c in &chars[..keep] {
137 out.push(c);
138 }
139 for _ in 0..hidden {
140 out.push('*');
141 }
142 out
143}
144
145fn mask_ip(ip: &str) -> String {
152 if let Some(last_dot) = ip.rfind('.') {
153 format!("{}.*", &ip[..last_dot])
154 } else if let Some(last_colon) = ip.rfind(':') {
155 format!("{}:*", &ip[..last_colon])
156 } else {
157 ip.to_string()
158 }
159}
160
161fn mask_imei(imei: &str) -> String {
165 let chars: Vec<char> = imei.chars().collect();
166 if chars.len() < 7 {
167 return "*".repeat(chars.len());
168 }
169 let mut out = String::with_capacity(chars.len() + 4);
170 for &c in &chars[..6] {
171 out.push(c);
172 }
173 out.push_str("****");
174 out.push(chars[chars.len() - 1]);
175 out
176}
177
178fn mask_plate(plate: &str) -> String {
183 let chars: Vec<char> = plate.chars().collect();
184 let len = chars.len();
185 if len < 4 {
186 return "*".repeat(len);
187 }
188 let mut out = String::with_capacity(len + 2);
189 for &c in &chars[..len - 2] {
190 out.push(c);
191 }
192 out.push_str("**");
193 for &c in &chars[len - 2..] {
194 out.push(c);
195 }
196 out
197}
198
199fn mask_custom(value: &str, spec: &str) -> String {
200 let (prefix, suffix) = match parse_custom_spec(spec) {
201 Some(parsed) => parsed,
202 None => return "***".to_string(),
203 };
204 mask_prefix_suffix(value, prefix, suffix)
205}
206
207fn parse_custom_spec(spec: &str) -> Option<(usize, usize)> {
210 let parts: Vec<&str> = spec.split(',').collect();
211 if parts.len() != 2 {
212 return None;
213 }
214 let prefix: usize = parts[0].trim().parse().ok()?;
215 let suffix: usize = parts[1].trim().parse().ok()?;
216 Some((prefix, suffix))
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 #[test]
225 fn test_phone_standard() {
226 let result = DataMasker::apply(&MaskingRule::Phone, "13812345678");
227 assert_eq!(result, "138****5678");
228 }
229
230 #[test]
231 fn test_phone_too_short() {
232 assert_eq!(DataMasker::apply(&MaskingRule::Phone, "12345"), "***");
234 assert_eq!(DataMasker::apply(&MaskingRule::Phone, "1234567"), "***");
235 }
236
237 #[test]
238 fn test_phone_boundary_seven_plus_one() {
239 assert_eq!(
241 DataMasker::apply(&MaskingRule::Phone, "12345678"),
242 "123*5678"
243 );
244 }
245
246 #[test]
247 fn test_phone_empty() {
248 assert_eq!(DataMasker::apply(&MaskingRule::Phone, ""), "***");
249 }
250
251 #[test]
253 fn test_email_standard() {
254 assert_eq!(
255 DataMasker::apply(&MaskingRule::Email, "test@example.com"),
256 "t***@example.com"
257 );
258 }
259
260 #[test]
261 fn test_email_single_char_local() {
262 assert_eq!(
263 DataMasker::apply(&MaskingRule::Email, "a@example.com"),
264 "a@example.com"
265 );
266 }
267
268 #[test]
269 fn test_email_no_at() {
270 assert_eq!(DataMasker::apply(&MaskingRule::Email, "notanemail"), "***");
271 }
272
273 #[test]
274 fn test_email_empty_local() {
275 assert_eq!(
276 DataMasker::apply(&MaskingRule::Email, "@example.com"),
277 "***"
278 );
279 }
280
281 #[test]
283 fn test_idcard_standard_18() {
284 let id = "110101199001012345";
285 let masked = DataMasker::apply(&MaskingRule::IdCard, id);
286 assert_eq!(masked, "1101**********2345");
288 assert_eq!(masked.len(), id.len());
289 }
290
291 #[test]
292 fn test_idcard_too_short() {
293 assert_eq!(DataMasker::apply(&MaskingRule::IdCard, "1234567"), "***");
294 assert_eq!(DataMasker::apply(&MaskingRule::IdCard, "12345678"), "***");
295 }
296
297 #[test]
298 fn test_idcard_empty() {
299 assert_eq!(DataMasker::apply(&MaskingRule::IdCard, ""), "***");
300 }
301
302 #[test]
304 fn test_bankcard_standard_16() {
305 let card = "6222020200112345";
306 let masked = DataMasker::apply(&MaskingRule::BankCard, card);
307 assert_eq!(masked, "6222********2345");
308 }
309
310 #[test]
311 fn test_bankcard_too_short() {
312 assert_eq!(DataMasker::apply(&MaskingRule::BankCard, "1234567"), "***");
313 }
314
315 #[test]
316 fn test_bankcard_empty() {
317 assert_eq!(DataMasker::apply(&MaskingRule::BankCard, ""), "***");
318 }
319
320 #[test]
322 fn test_name_chinese_two_chars() {
323 assert_eq!(DataMasker::apply(&MaskingRule::Name, "张三"), "张*");
324 }
325
326 #[test]
327 fn test_name_chinese_three_chars() {
328 assert_eq!(DataMasker::apply(&MaskingRule::Name, "诸葛亮"), "诸**");
329 }
330
331 #[test]
332 fn test_name_single_char() {
333 assert_eq!(DataMasker::apply(&MaskingRule::Name, "李"), "李");
334 }
335
336 #[test]
337 fn test_name_empty() {
338 assert_eq!(DataMasker::apply(&MaskingRule::Name, ""), "");
339 }
340
341 #[test]
342 fn test_name_english() {
343 assert_eq!(DataMasker::apply(&MaskingRule::Name, "Alice"), "A****");
344 }
345
346 #[test]
348 fn test_address_standard() {
349 let addr = "北京市海淀区中关村大街1号";
350 let masked = DataMasker::apply(&MaskingRule::Address, addr);
351 let expected = "北京市海淀区*******";
353 assert_eq!(masked, expected);
354 assert_eq!(masked.chars().count(), addr.chars().count());
355 }
356
357 #[test]
358 fn test_address_exactly_six_chars() {
359 let addr = "北京市海淀区";
360 assert_eq!(DataMasker::apply(&MaskingRule::Address, addr), "******");
361 }
362
363 #[test]
364 fn test_address_short() {
365 assert_eq!(DataMasker::apply(&MaskingRule::Address, "北京"), "**");
366 }
367
368 #[test]
369 fn test_address_empty() {
370 assert_eq!(DataMasker::apply(&MaskingRule::Address, ""), "");
371 }
372
373 #[test]
375 fn test_custom_prefix_suffix() {
376 let rule = MaskingRule::Custom("3,2".to_string());
377 assert_eq!(DataMasker::apply(&rule, "ABCDEFGHIJ"), "ABC*****IJ");
378 }
379
380 #[test]
381 fn test_custom_too_short() {
382 let rule = MaskingRule::Custom("4,4".to_string());
383 assert_eq!(DataMasker::apply(&rule, "ABC"), "***");
384 }
385
386 #[test]
387 fn test_custom_invalid_spec() {
388 let rule = MaskingRule::Custom("not_a_number".to_string());
389 assert_eq!(DataMasker::apply(&rule, "ABCDEF"), "***");
390 }
391
392 #[test]
393 fn test_custom_invalid_spec_two_parts() {
394 let rule = MaskingRule::Custom("1,2,3".to_string());
395 assert_eq!(DataMasker::apply(&rule, "ABCDEF"), "***");
396 }
397
398 #[test]
399 fn test_custom_empty_value() {
400 let rule = MaskingRule::Custom("2,2".to_string());
401 assert_eq!(DataMasker::apply(&rule, ""), "***");
402 }
403
404 #[test]
406 fn test_unicode_no_panic() {
407 let value = "你好🌍世界AB";
409 let masked = DataMasker::apply(&MaskingRule::Address, value);
410 assert!(masked.contains('*'));
411 }
412
413 #[test]
414 fn test_long_string() {
415 let value = "1".repeat(10000);
416 let masked = DataMasker::apply(&MaskingRule::Phone, &value);
417 assert!(masked.starts_with("111"));
419 assert!(masked.ends_with("1111"));
420 assert_eq!(masked.matches('*').count(), 10000 - 7);
421 }
422
423 #[test]
424 fn test_single_char_inputs() {
425 assert_eq!(DataMasker::apply(&MaskingRule::Phone, "1"), "***");
426 assert_eq!(DataMasker::apply(&MaskingRule::IdCard, "1"), "***");
427 assert_eq!(DataMasker::apply(&MaskingRule::BankCard, "1"), "***");
428 assert_eq!(DataMasker::apply(&MaskingRule::Name, "张"), "张");
429 assert_eq!(DataMasker::apply(&MaskingRule::Address, "张"), "*");
430 }
431
432 #[test]
434 fn test_ip_v4_standard() {
435 assert_eq!(
436 DataMasker::apply(&MaskingRule::Ip, "192.168.1.100"),
437 "192.168.1.*"
438 );
439 }
440
441 #[test]
442 fn test_ip_v4_loopback() {
443 assert_eq!(
444 DataMasker::apply(&MaskingRule::Ip, "127.0.0.1"),
445 "127.0.0.*"
446 );
447 }
448
449 #[test]
450 fn test_ip_v6_standard() {
451 assert_eq!(
453 DataMasker::apply(&MaskingRule::Ip, "2001:db8::1"),
454 "2001:db8::*"
455 );
456 }
457
458 #[test]
459 fn test_ip_no_separator() {
460 assert_eq!(
461 DataMasker::apply(&MaskingRule::Ip, "localhost"),
462 "localhost"
463 );
464 }
465
466 #[test]
467 fn test_ip_empty() {
468 assert_eq!(DataMasker::apply(&MaskingRule::Ip, ""), "");
469 }
470
471 #[test]
473 fn test_imei_standard_15() {
474 assert_eq!(
476 DataMasker::apply(&MaskingRule::Imei, "123456789012345"),
477 "123456****5"
478 );
479 }
480
481 #[test]
482 fn test_imei_too_short() {
483 assert_eq!(DataMasker::apply(&MaskingRule::Imei, "123456"), "******");
484 assert_eq!(DataMasker::apply(&MaskingRule::Imei, "123"), "***");
485 }
486
487 #[test]
488 fn test_imei_empty() {
489 assert_eq!(DataMasker::apply(&MaskingRule::Imei, ""), "");
490 }
491
492 #[test]
494 fn test_plate_chinese_standard() {
495 assert_eq!(
497 DataMasker::apply(&MaskingRule::Plate, "京A12345"),
498 "京A123**45"
499 );
500 }
501
502 #[test]
503 fn test_plate_with_separator() {
504 assert_eq!(
506 DataMasker::apply(&MaskingRule::Plate, "京A·12345"),
507 "京A·123**45"
508 );
509 }
510
511 #[test]
512 fn test_plate_too_short() {
513 assert_eq!(DataMasker::apply(&MaskingRule::Plate, "京A"), "**");
514 assert_eq!(DataMasker::apply(&MaskingRule::Plate, "京"), "*");
515 assert_eq!(DataMasker::apply(&MaskingRule::Plate, "京A1"), "***");
516 }
517
518 #[test]
519 fn test_plate_empty() {
520 assert_eq!(DataMasker::apply(&MaskingRule::Plate, ""), "");
521 }
522
523 #[test]
524 fn test_plate_boundary_four_chars() {
525 assert_eq!(DataMasker::apply(&MaskingRule::Plate, "ABCD"), "AB**CD");
527 }
528}