1use serde::{Deserialize, Serialize};
12
13pub mod audit;
14pub mod config;
15pub mod maskers;
16pub mod strategy;
17
18pub use audit::{
20 rule_name, FieldReport, MaskingAuditEntry as DetailedAuditEntry,
21 MaskingAuditLog as DetailedAuditLog, MaskingReport, RuleReport,
22};
23pub use config::{FieldPattern, MaskingConfigManager, MaskingProfile, SensitiveFieldDetector};
24pub use maskers::{wildcard_match, HashAlgorithm, HashMasker, PartialDisplayMasker, PatternMasker};
25pub use strategy::{
26 FieldMaskingRule, MaskingCondition, MaskingPipeline, MaskingStrategyEngine, MaskingValidator,
27 PipelineStage,
28};
29
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38pub enum MaskingRule {
39 Phone,
40 Email,
41 IdCard,
42 BankCard,
43 Name,
44 Address,
45 Ip,
46 Imei,
47 Plate,
48 Custom(String),
49 Password,
50 ApiKey,
51}
52
53pub struct DataMasker;
54
55impl DataMasker {
56 pub fn apply(rule: &MaskingRule, value: &str) -> String {
62 match rule {
63 MaskingRule::Phone => mask_prefix_suffix(value, 3, 4),
64 MaskingRule::Email => mask_email(value),
65 MaskingRule::IdCard => mask_prefix_suffix(value, 4, 4),
66 MaskingRule::BankCard => mask_prefix_suffix(value, 4, 4),
67 MaskingRule::Name => mask_name(value),
68 MaskingRule::Address => mask_address(value, 6),
69 MaskingRule::Ip => mask_ip(value),
70 MaskingRule::Imei => mask_imei(value),
71 MaskingRule::Plate => mask_plate(value),
72 MaskingRule::Custom(spec) => mask_custom(value, spec),
73 MaskingRule::Password => "***".to_string(),
74 MaskingRule::ApiKey => mask_api_key(value),
75 }
76 }
77
78 pub fn apply_many(rules: &[MaskingRule], value: &str) -> String {
84 let mut out = value.to_string();
85 for rule in rules {
86 out = Self::apply(rule, &out);
87 }
88 out
89 }
90
91 pub fn mask_map(
98 rules: &std::collections::HashMap<String, MaskingRule>,
99 data: &std::collections::HashMap<String, String>,
100 ) -> std::collections::HashMap<String, String> {
101 data.iter()
102 .map(|(k, v)| match rules.get(k) {
103 Some(rule) => (k.clone(), Self::apply(rule, v)),
104 None => (k.clone(), v.clone()),
105 })
106 .collect()
107 }
108
109 pub fn mask_json(rules: &std::collections::HashMap<String, MaskingRule>, json: &str) -> String {
116 let Ok(mut value) = serde_json::from_str::<serde_json::Value>(json) else {
117 return json.to_string();
118 };
119 let Some(obj) = value.as_object_mut() else {
120 return json.to_string();
121 };
122 for (field, rule) in rules {
123 if let Some(serde_json::Value::String(s)) = obj.get_mut(field) {
124 *s = Self::apply(rule, s);
125 }
126 }
127 serde_json::to_string(&value).unwrap_or_else(|_| json.to_string())
128 }
129}
130
131fn mask_prefix_suffix(value: &str, prefix: usize, suffix: usize) -> String {
136 let chars: Vec<char> = value.chars().collect();
137 let len = chars.len();
138 if len == 0 {
139 return "***".to_string();
140 }
141 if len <= prefix + suffix {
144 return "***".to_string();
146 }
147 let hidden = len - prefix - suffix;
148 let mut out = String::with_capacity(len);
149 for &c in &chars[..prefix] {
150 out.push(c);
151 }
152 for _ in 0..hidden {
153 out.push('*');
154 }
155 for &c in &chars[len - suffix..] {
156 out.push(c);
157 }
158 out
159}
160
161fn mask_api_key(value: &str) -> String {
165 mask_prefix_suffix(value, 4, 4)
166}
167
168fn mask_email(value: &str) -> String {
169 let parts: Vec<&str> = value.splitn(2, '@').collect();
170 if parts.len() != 2 {
171 return "***".to_string();
173 }
174 let local = parts[0];
175 let domain = parts[1];
176 let local_chars: Vec<char> = local.chars().collect();
177 if local_chars.is_empty() {
178 return "***".to_string();
179 }
180 let mut out = String::with_capacity(value.len());
181 out.push(local_chars[0]);
182 for _ in 1..local_chars.len() {
184 out.push('*');
185 }
186 out.push('@');
187 out.push_str(domain);
188 out
189}
190
191fn mask_name(value: &str) -> String {
192 let chars: Vec<char> = value.chars().collect();
193 if chars.is_empty() {
194 return String::new();
195 }
196 let mut out = String::with_capacity(chars.len());
197 out.push(chars[0]);
198 for _ in 1..chars.len() {
199 out.push('*');
200 }
201 out
202}
203
204fn mask_address(value: &str, keep: usize) -> String {
205 let chars: Vec<char> = value.chars().collect();
206 if chars.is_empty() {
207 return String::new();
208 }
209 if chars.len() <= keep {
210 return "*".repeat(chars.len());
213 }
214 let hidden = chars.len() - keep;
215 let mut out = String::with_capacity(chars.len());
216 for &c in &chars[..keep] {
217 out.push(c);
218 }
219 for _ in 0..hidden {
220 out.push('*');
221 }
222 out
223}
224
225fn mask_ip(ip: &str) -> String {
232 if let Some(last_dot) = ip.rfind('.') {
233 format!("{}.*", &ip[..last_dot])
234 } else if let Some(last_colon) = ip.rfind(':') {
235 format!("{}:*", &ip[..last_colon])
236 } else {
237 ip.to_string()
238 }
239}
240
241fn mask_imei(imei: &str) -> String {
245 let chars: Vec<char> = imei.chars().collect();
246 if chars.len() < 7 {
247 return "*".repeat(chars.len());
248 }
249 let mut out = String::with_capacity(chars.len() + 4);
250 for &c in &chars[..6] {
251 out.push(c);
252 }
253 out.push_str("****");
254 out.push(chars[chars.len() - 1]);
255 out
256}
257
258fn mask_plate(plate: &str) -> String {
263 let chars: Vec<char> = plate.chars().collect();
264 let len = chars.len();
265 if len < 4 {
266 return "*".repeat(len);
267 }
268 let mut out = String::with_capacity(len + 2);
269 for &c in &chars[..len - 2] {
270 out.push(c);
271 }
272 out.push_str("**");
273 for &c in &chars[len - 2..] {
274 out.push(c);
275 }
276 out
277}
278
279fn mask_custom(value: &str, spec: &str) -> String {
280 let (prefix, suffix) = match parse_custom_spec(spec) {
281 Some(parsed) => parsed,
282 None => return "***".to_string(),
283 };
284 mask_prefix_suffix(value, prefix, suffix)
285}
286
287fn parse_custom_spec(spec: &str) -> Option<(usize, usize)> {
290 let parts: Vec<&str> = spec.split(',').collect();
291 if parts.len() != 2 {
292 return None;
293 }
294 let prefix: usize = parts[0].trim().parse().ok()?;
295 let suffix: usize = parts[1].trim().parse().ok()?;
296 Some((prefix, suffix))
297}
298
299#[derive(Debug, Clone, Default)]
305pub struct MaskingPolicy {
306 rules: std::collections::HashMap<String, MaskingRule>,
307}
308
309impl MaskingPolicy {
310 pub fn new() -> Self {
312 Self::default()
313 }
314
315 pub fn add_rule(&mut self, field: &str, rule: MaskingRule) -> &mut Self {
317 self.rules.insert(field.to_string(), rule);
318 self
319 }
320
321 pub fn get_rule(&self, field: &str) -> Option<&MaskingRule> {
323 self.rules.get(field)
324 }
325
326 pub fn remove_rule(&mut self, field: &str) -> Option<MaskingRule> {
328 self.rules.remove(field)
329 }
330
331 pub fn field_count(&self) -> usize {
333 self.rules.len()
334 }
335
336 pub fn rules(&self) -> &std::collections::HashMap<String, MaskingRule> {
338 &self.rules
339 }
340
341 pub fn apply_to_map(
343 &self,
344 data: &std::collections::HashMap<String, String>,
345 ) -> std::collections::HashMap<String, String> {
346 DataMasker::mask_map(&self.rules, data)
347 }
348
349 pub fn apply_to_json(&self, json: &str) -> String {
351 DataMasker::mask_json(&self.rules, json)
352 }
353}
354
355#[derive(Debug, Clone)]
357pub struct MaskAuditEntry {
358 field: String,
359 original_len: usize,
360 masked_len: usize,
361}
362
363impl MaskAuditEntry {
364 pub fn field(&self) -> &str {
366 &self.field
367 }
368
369 pub fn original_len(&self) -> usize {
371 self.original_len
372 }
373
374 pub fn masked_len(&self) -> usize {
376 self.masked_len
377 }
378
379 pub fn was_truncated(&self) -> bool {
381 self.masked_len < self.original_len
382 }
383}
384
385#[derive(Debug, Clone, Default)]
387pub struct MaskAuditLog {
388 entries: Vec<MaskAuditEntry>,
389}
390
391impl MaskAuditLog {
392 pub fn new() -> Self {
394 Self::default()
395 }
396
397 pub fn log(&mut self, field: &str, original_len: usize, masked_len: usize) {
399 self.entries.push(MaskAuditEntry {
400 field: field.to_string(),
401 original_len,
402 masked_len,
403 });
404 }
405
406 pub fn entries(&self) -> &[MaskAuditEntry] {
408 &self.entries
409 }
410
411 pub fn entry_count(&self) -> usize {
413 self.entries.len()
414 }
415
416 pub fn total_masked_chars(&self) -> usize {
418 self.entries
419 .iter()
420 .map(|e| e.original_len.saturating_sub(e.masked_len))
421 .sum()
422 }
423
424 pub fn clear(&mut self) {
426 self.entries.clear();
427 }
428}
429
430#[derive(Debug, Clone)]
432pub struct MaskingConfig {
433 mask_char: char,
434 min_mask_length: usize,
435 fallback: String,
436}
437
438impl Default for MaskingConfig {
439 fn default() -> Self {
440 Self {
441 mask_char: '*',
442 min_mask_length: 3,
443 fallback: "***".to_string(),
444 }
445 }
446}
447
448impl MaskingConfig {
449 pub fn new() -> Self {
451 Self::default()
452 }
453
454 pub fn mask_char(&self) -> char {
456 self.mask_char
457 }
458
459 pub fn with_mask_char(mut self, c: char) -> Self {
461 self.mask_char = c;
462 self
463 }
464
465 pub fn min_mask_length(&self) -> usize {
467 self.min_mask_length
468 }
469
470 pub fn with_min_mask_length(mut self, min: usize) -> Self {
472 self.min_mask_length = min;
473 self
474 }
475
476 pub fn fallback_value(&self) -> &str {
478 &self.fallback
479 }
480
481 pub fn with_fallback_value(mut self, value: &str) -> Self {
483 self.fallback = value.to_string();
484 self
485 }
486}
487
488#[derive(Debug, Clone, Default)]
490pub struct MaskingStats {
491 counts: std::collections::HashMap<String, u64>,
492}
493
494impl MaskingStats {
495 pub fn new() -> Self {
497 Self::default()
498 }
499
500 pub fn record(&mut self, field: &str) {
502 *self.counts.entry(field.to_string()).or_insert(0) += 1;
503 }
504
505 pub fn total_operations(&self) -> u64 {
507 self.counts.values().sum()
508 }
509
510 pub fn field_operations(&self, field: &str) -> u64 {
512 self.counts.get(field).copied().unwrap_or(0)
513 }
514
515 pub fn most_masked_field(&self) -> Option<&str> {
517 self.counts
518 .iter()
519 .max_by_key(|(_, v)| *v)
520 .map(|(k, _)| k.as_str())
521 }
522}
523
524#[cfg(test)]
525mod tests {
526 use super::*;
527
528 #[test]
530 fn test_phone_standard() {
531 let result = DataMasker::apply(&MaskingRule::Phone, "13812345678");
532 assert_eq!(result, "138****5678");
533 }
534
535 #[test]
536 fn test_phone_too_short() {
537 assert_eq!(DataMasker::apply(&MaskingRule::Phone, "12345"), "***");
539 assert_eq!(DataMasker::apply(&MaskingRule::Phone, "1234567"), "***");
540 }
541
542 #[test]
543 fn test_phone_boundary_seven_plus_one() {
544 assert_eq!(
546 DataMasker::apply(&MaskingRule::Phone, "12345678"),
547 "123*5678"
548 );
549 }
550
551 #[test]
552 fn test_phone_empty() {
553 assert_eq!(DataMasker::apply(&MaskingRule::Phone, ""), "***");
554 }
555
556 #[test]
558 fn test_email_standard() {
559 assert_eq!(
560 DataMasker::apply(&MaskingRule::Email, "test@example.com"),
561 "t***@example.com"
562 );
563 }
564
565 #[test]
566 fn test_email_single_char_local() {
567 assert_eq!(
568 DataMasker::apply(&MaskingRule::Email, "a@example.com"),
569 "a@example.com"
570 );
571 }
572
573 #[test]
574 fn test_email_no_at() {
575 assert_eq!(DataMasker::apply(&MaskingRule::Email, "notanemail"), "***");
576 }
577
578 #[test]
579 fn test_email_empty_local() {
580 assert_eq!(
581 DataMasker::apply(&MaskingRule::Email, "@example.com"),
582 "***"
583 );
584 }
585
586 #[test]
588 fn test_idcard_standard_18() {
589 let id = "110101199001012345";
590 let masked = DataMasker::apply(&MaskingRule::IdCard, id);
591 assert_eq!(masked, "1101**********2345");
593 assert_eq!(masked.len(), id.len());
594 }
595
596 #[test]
597 fn test_idcard_too_short() {
598 assert_eq!(DataMasker::apply(&MaskingRule::IdCard, "1234567"), "***");
599 assert_eq!(DataMasker::apply(&MaskingRule::IdCard, "12345678"), "***");
600 }
601
602 #[test]
603 fn test_idcard_empty() {
604 assert_eq!(DataMasker::apply(&MaskingRule::IdCard, ""), "***");
605 }
606
607 #[test]
609 fn test_bankcard_standard_16() {
610 let card = "6222020200112345";
611 let masked = DataMasker::apply(&MaskingRule::BankCard, card);
612 assert_eq!(masked, "6222********2345");
613 }
614
615 #[test]
616 fn test_bankcard_too_short() {
617 assert_eq!(DataMasker::apply(&MaskingRule::BankCard, "1234567"), "***");
618 }
619
620 #[test]
621 fn test_bankcard_empty() {
622 assert_eq!(DataMasker::apply(&MaskingRule::BankCard, ""), "***");
623 }
624
625 #[test]
627 fn test_name_chinese_two_chars() {
628 assert_eq!(DataMasker::apply(&MaskingRule::Name, "张三"), "张*");
629 }
630
631 #[test]
632 fn test_name_chinese_three_chars() {
633 assert_eq!(DataMasker::apply(&MaskingRule::Name, "诸葛亮"), "诸**");
634 }
635
636 #[test]
637 fn test_name_single_char() {
638 assert_eq!(DataMasker::apply(&MaskingRule::Name, "李"), "李");
639 }
640
641 #[test]
642 fn test_name_empty() {
643 assert_eq!(DataMasker::apply(&MaskingRule::Name, ""), "");
644 }
645
646 #[test]
647 fn test_name_english() {
648 assert_eq!(DataMasker::apply(&MaskingRule::Name, "Alice"), "A****");
649 }
650
651 #[test]
653 fn test_address_standard() {
654 let addr = "北京市海淀区中关村大街1号";
655 let masked = DataMasker::apply(&MaskingRule::Address, addr);
656 let expected = "北京市海淀区*******";
658 assert_eq!(masked, expected);
659 assert_eq!(masked.chars().count(), addr.chars().count());
660 }
661
662 #[test]
663 fn test_address_exactly_six_chars() {
664 let addr = "北京市海淀区";
665 assert_eq!(DataMasker::apply(&MaskingRule::Address, addr), "******");
666 }
667
668 #[test]
669 fn test_address_short() {
670 assert_eq!(DataMasker::apply(&MaskingRule::Address, "北京"), "**");
671 }
672
673 #[test]
674 fn test_address_empty() {
675 assert_eq!(DataMasker::apply(&MaskingRule::Address, ""), "");
676 }
677
678 #[test]
680 fn test_custom_prefix_suffix() {
681 let rule = MaskingRule::Custom("3,2".to_string());
682 assert_eq!(DataMasker::apply(&rule, "ABCDEFGHIJ"), "ABC*****IJ");
683 }
684
685 #[test]
686 fn test_custom_too_short() {
687 let rule = MaskingRule::Custom("4,4".to_string());
688 assert_eq!(DataMasker::apply(&rule, "ABC"), "***");
689 }
690
691 #[test]
692 fn test_custom_invalid_spec() {
693 let rule = MaskingRule::Custom("not_a_number".to_string());
694 assert_eq!(DataMasker::apply(&rule, "ABCDEF"), "***");
695 }
696
697 #[test]
698 fn test_custom_invalid_spec_two_parts() {
699 let rule = MaskingRule::Custom("1,2,3".to_string());
700 assert_eq!(DataMasker::apply(&rule, "ABCDEF"), "***");
701 }
702
703 #[test]
704 fn test_custom_empty_value() {
705 let rule = MaskingRule::Custom("2,2".to_string());
706 assert_eq!(DataMasker::apply(&rule, ""), "***");
707 }
708
709 #[test]
711 fn test_unicode_no_panic() {
712 let value = "你好🌍世界AB";
714 let masked = DataMasker::apply(&MaskingRule::Address, value);
715 assert!(masked.contains('*'));
716 }
717
718 #[test]
719 fn test_long_string() {
720 let value = "1".repeat(10000);
721 let masked = DataMasker::apply(&MaskingRule::Phone, &value);
722 assert!(masked.starts_with("111"));
724 assert!(masked.ends_with("1111"));
725 assert_eq!(masked.matches('*').count(), 10000 - 7);
726 }
727
728 #[test]
729 fn test_single_char_inputs() {
730 assert_eq!(DataMasker::apply(&MaskingRule::Phone, "1"), "***");
731 assert_eq!(DataMasker::apply(&MaskingRule::IdCard, "1"), "***");
732 assert_eq!(DataMasker::apply(&MaskingRule::BankCard, "1"), "***");
733 assert_eq!(DataMasker::apply(&MaskingRule::Name, "张"), "张");
734 assert_eq!(DataMasker::apply(&MaskingRule::Address, "张"), "*");
735 }
736
737 #[test]
739 fn test_ip_v4_standard() {
740 assert_eq!(
741 DataMasker::apply(&MaskingRule::Ip, "192.168.1.100"),
742 "192.168.1.*"
743 );
744 }
745
746 #[test]
747 fn test_ip_v4_loopback() {
748 assert_eq!(
749 DataMasker::apply(&MaskingRule::Ip, "127.0.0.1"),
750 "127.0.0.*"
751 );
752 }
753
754 #[test]
755 fn test_ip_v6_standard() {
756 assert_eq!(
758 DataMasker::apply(&MaskingRule::Ip, "2001:db8::1"),
759 "2001:db8::*"
760 );
761 }
762
763 #[test]
764 fn test_ip_no_separator() {
765 assert_eq!(
766 DataMasker::apply(&MaskingRule::Ip, "localhost"),
767 "localhost"
768 );
769 }
770
771 #[test]
772 fn test_ip_empty() {
773 assert_eq!(DataMasker::apply(&MaskingRule::Ip, ""), "");
774 }
775
776 #[test]
778 fn test_imei_standard_15() {
779 assert_eq!(
781 DataMasker::apply(&MaskingRule::Imei, "123456789012345"),
782 "123456****5"
783 );
784 }
785
786 #[test]
787 fn test_imei_too_short() {
788 assert_eq!(DataMasker::apply(&MaskingRule::Imei, "123456"), "******");
789 assert_eq!(DataMasker::apply(&MaskingRule::Imei, "123"), "***");
790 }
791
792 #[test]
793 fn test_imei_empty() {
794 assert_eq!(DataMasker::apply(&MaskingRule::Imei, ""), "");
795 }
796
797 #[test]
799 fn test_plate_chinese_standard() {
800 assert_eq!(
802 DataMasker::apply(&MaskingRule::Plate, "京A12345"),
803 "京A123**45"
804 );
805 }
806
807 #[test]
808 fn test_plate_with_separator() {
809 assert_eq!(
811 DataMasker::apply(&MaskingRule::Plate, "京A·12345"),
812 "京A·123**45"
813 );
814 }
815
816 #[test]
817 fn test_plate_too_short() {
818 assert_eq!(DataMasker::apply(&MaskingRule::Plate, "京A"), "**");
819 assert_eq!(DataMasker::apply(&MaskingRule::Plate, "京"), "*");
820 assert_eq!(DataMasker::apply(&MaskingRule::Plate, "京A1"), "***");
821 }
822
823 #[test]
824 fn test_plate_empty() {
825 assert_eq!(DataMasker::apply(&MaskingRule::Plate, ""), "");
826 }
827
828 #[test]
829 fn test_plate_boundary_four_chars() {
830 assert_eq!(DataMasker::apply(&MaskingRule::Plate, "ABCD"), "AB**CD");
832 }
833}
834
835#[cfg(test)]
836mod api_tests {
837 use super::*;
838 use std::collections::HashMap;
839
840 #[test]
841 fn test_apply_many_stacked_rules() {
842 let rules = vec![MaskingRule::Phone, MaskingRule::Custom("1,2".to_string())];
844 let out = DataMasker::apply_many(&rules, "13812345678");
845 assert!(
846 out.contains('*'),
847 "stacked masking should keep stars: {out}"
848 );
849 }
850
851 #[test]
852 fn test_mask_map_partial_rules() {
853 let mut rules = HashMap::new();
854 rules.insert("phone".to_string(), MaskingRule::Phone);
855 let mut data = HashMap::new();
856 data.insert("phone".to_string(), "13812345678".to_string());
857 data.insert("name".to_string(), "Alice".to_string());
858
859 let out = DataMasker::mask_map(&rules, &data);
860 assert!(
861 out["phone"].contains('*'),
862 "phone should be masked: {}",
863 out["phone"]
864 );
865 assert_eq!(out["name"], "Alice", "unlisted field passes through");
866 }
867
868 #[test]
869 fn test_mask_json_top_level_fields() {
870 let mut rules = HashMap::new();
871 rules.insert("phone".to_string(), MaskingRule::Phone);
872 rules.insert("password".to_string(), MaskingRule::Password);
873
874 let json = r#"{"id":1,"phone":"13812345678","password":"secret","name":"Bob"}"#;
875 let out = DataMasker::mask_json(&rules, json);
876 assert!(out.contains('*'), "masked json should contain stars: {out}");
877 assert!(!out.contains("13812345678"), "phone value must not leak");
878 assert!(!out.contains("secret"), "password value must not leak");
879 assert!(out.contains("Bob"), "unlisted field passes through");
880 }
881
882 #[test]
883 fn test_mask_json_invalid_input_unchanged() {
884 let rules = HashMap::new();
885 assert_eq!(DataMasker::mask_json(&rules, "not-json"), "not-json");
886 }
887
888 #[test]
889 fn test_mask_json_non_object_unchanged() {
890 let rules = HashMap::new();
891 assert_eq!(DataMasker::mask_json(&rules, "[1,2,3]"), "[1,2,3]");
892 }
893
894 #[test]
895 fn test_mask_map_empty_rules() {
896 let data = HashMap::new();
897 let out = DataMasker::mask_map(&HashMap::new(), &data);
898 assert!(out.is_empty());
899 }
900
901 #[test]
902 fn test_apply_many_empty_rules_identity() {
903 assert_eq!(DataMasker::apply_many(&[], "hello"), "hello");
904 }
905}
906
907#[cfg(test)]
908mod policy_tests {
909 use super::*;
910 use std::collections::HashMap;
911
912 #[test]
915 fn policy_new_empty() {
916 let p = MaskingPolicy::new();
917 assert_eq!(p.field_count(), 0);
918 }
919
920 #[test]
921 fn policy_add_and_get_rule() {
922 let mut p = MaskingPolicy::new();
923 p.add_rule("phone", MaskingRule::Phone);
924 assert_eq!(p.field_count(), 1);
925 assert!(p.get_rule("phone").is_some());
926 assert!(p.get_rule("email").is_none());
927 }
928
929 #[test]
930 fn policy_remove_rule() {
931 let mut p = MaskingPolicy::new();
932 p.add_rule("phone", MaskingRule::Phone);
933 let removed = p.remove_rule("phone");
934 assert!(removed.is_some());
935 assert_eq!(p.field_count(), 0);
936 }
937
938 #[test]
939 fn policy_apply_to_map() {
940 let mut p = MaskingPolicy::new();
941 p.add_rule("phone", MaskingRule::Phone);
942 let mut data = HashMap::new();
943 data.insert("phone".to_string(), "13812345678".to_string());
944 data.insert("name".to_string(), "Alice".to_string());
945 let out = p.apply_to_map(&data);
946 assert!(out["phone"].contains('*'));
947 assert_eq!(out["name"], "Alice");
948 }
949
950 #[test]
951 fn policy_apply_to_json() {
952 let mut p = MaskingPolicy::new();
953 p.add_rule("phone", MaskingRule::Phone);
954 let json = r#"{"phone":"13812345678","name":"Bob"}"#;
955 let out = p.apply_to_json(json);
956 assert!(out.contains('*'));
957 assert!(!out.contains("13812345678"));
958 }
959
960 #[test]
961 fn policy_rules_ref() {
962 let mut p = MaskingPolicy::new();
963 p.add_rule("a", MaskingRule::Name);
964 assert_eq!(p.rules().len(), 1);
965 }
966
967 #[test]
970 fn audit_log_new_empty() {
971 let log = MaskAuditLog::new();
972 assert_eq!(log.entry_count(), 0);
973 assert!(log.entries().is_empty());
974 }
975
976 #[test]
977 fn audit_log_record() {
978 let mut log = MaskAuditLog::new();
979 log.log("phone", 11, 11);
980 log.log("name", 5, 5);
981 assert_eq!(log.entry_count(), 2);
982 }
983
984 #[test]
985 fn audit_log_total_masked_chars() {
986 let mut log = MaskAuditLog::new();
987 log.log("phone", 11, 7);
988 log.log("name", 5, 3);
989 assert_eq!(log.total_masked_chars(), 6);
990 }
991
992 #[test]
993 fn audit_log_clear() {
994 let mut log = MaskAuditLog::new();
995 log.log("a", 1, 1);
996 log.clear();
997 assert_eq!(log.entry_count(), 0);
998 }
999
1000 #[test]
1001 fn audit_entry_was_truncated() {
1002 let mut log = MaskAuditLog::new();
1003 log.log("short", 10, 5);
1004 log.log("same", 5, 5);
1005 assert!(log.entries()[0].was_truncated());
1006 assert!(!log.entries()[1].was_truncated());
1007 }
1008
1009 #[test]
1012 fn config_defaults() {
1013 let c = MaskingConfig::new();
1014 assert_eq!(c.mask_char(), '*');
1015 assert_eq!(c.min_mask_length(), 3);
1016 assert_eq!(c.fallback_value(), "***");
1017 }
1018
1019 #[test]
1020 fn config_builder() {
1021 let c = MaskingConfig::new()
1022 .with_mask_char('#')
1023 .with_min_mask_length(5)
1024 .with_fallback_value("UNK");
1025 assert_eq!(c.mask_char(), '#');
1026 assert_eq!(c.min_mask_length(), 5);
1027 assert_eq!(c.fallback_value(), "UNK");
1028 }
1029
1030 #[test]
1033 fn stats_new_empty() {
1034 let s = MaskingStats::new();
1035 assert_eq!(s.total_operations(), 0);
1036 assert_eq!(s.most_masked_field(), None);
1037 }
1038
1039 #[test]
1040 fn stats_record_and_total() {
1041 let mut s = MaskingStats::new();
1042 s.record("phone");
1043 s.record("phone");
1044 s.record("name");
1045 assert_eq!(s.total_operations(), 3);
1046 assert_eq!(s.field_operations("phone"), 2);
1047 assert_eq!(s.field_operations("name"), 1);
1048 }
1049
1050 #[test]
1051 fn stats_most_masked_field() {
1052 let mut s = MaskingStats::new();
1053 s.record("a");
1054 s.record("b");
1055 s.record("b");
1056 assert_eq!(s.most_masked_field(), Some("b"));
1057 }
1058
1059 #[test]
1060 fn stats_field_operations_zero_for_unknown() {
1061 let s = MaskingStats::new();
1062 assert_eq!(s.field_operations("nonexistent"), 0);
1063 }
1064}