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