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