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