Skip to main content

sz_orm_masking/
lib.rs

1//! # SZ-ORM Masking — Data Masking
2//!
3//! Provides masking for sensitive fields such as phone number, email, ID card, bank card, name, address, and supports custom
4//! prefix/suffix retention rules. Unicode safe, with reasonable fallback for short input, does not panic.
5//!
6//! ## Main Types
7//!
8//! - [`MaskingRule`] — Masking rule enum
9//! - [`DataMasker`] — Masking executor
10
11use serde::{Deserialize, Serialize};
12
13pub mod audit;
14pub mod config;
15pub mod maskers;
16pub mod strategy;
17
18// 重导出新模块的主要类型
19pub 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/// Masking rules supported by [`DataMasker`].
31///
32/// `Custom(String)` expects a configuration of the form `"prefix,suffix"`
33/// where `prefix` and `suffix` are the number of characters (Unicode scalar
34/// values) to retain from the start and end of the input. Example:
35/// `Custom("3,2".to_string())` keeps the first 3 and last 2 characters and
36/// replaces everything in between with `*`.
37#[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    /// Applies the given masking `rule` to `value`. The implementation is
57    /// Unicode-safe (works on `char` boundaries rather than byte slices) and
58    /// never panics: inputs shorter than the rule's required visible prefix
59    /// return a sensible fallback (the original value, or `"***"` when even
60    /// the original cannot be safely revealed).
61    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    /// Applies a sequence of rules to the same value, in order.
79    ///
80    /// Useful for stacked masking (e.g. first mask the whole phone, then
81    /// apply a custom prefix/suffix rule). Each rule is applied to the
82    /// output of the previous one.
83    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    /// Masks specific fields of a `HashMap<String, String>` in place,
92    /// returning a new map with only the masked fields replaced.
93    ///
94    /// Fields whose rule is `None` are copied through unchanged. Fields not
95    /// present in `rules` are also copied through, so callers can pass a
96    /// partial rule set without losing data.
97    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    /// Masks a JSON object string by field name.
110    ///
111    /// `rules` maps field names to masking rules. Only top-level fields are
112    /// matched (nested objects are left untouched); non-JSON input is
113    /// returned unchanged so callers can rely on this being lossless for
114    /// malformed payloads.
115    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
131/// Masks the middle of the input, keeping the first `prefix` and last
132/// `suffix` characters visible. Returns `"***"` when the input is too short
133/// to reveal `prefix + suffix` characters (or when `prefix`/`suffix` are
134/// zero, the rule degrades gracefully).
135fn 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    // Need at least one extra char beyond prefix+suffix to mask; otherwise
142    // the value has nothing to hide and we return the original.
143    if len <= prefix + suffix {
144        // Too short to safely mask without revealing the structure; return "***".
145        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
161/// Masks an API key by keeping the first 4 and last 4 characters visible
162/// and replacing everything in between with `*`. Returns `"***"` for inputs
163/// too short to safely mask (≤ 8 characters).
164fn 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        // Not a valid email; do not attempt to mask structurally.
172        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    // Hide the rest of the local part with one `*` per hidden character.
183    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        // Nothing meaningful to mask: hide everything to avoid leaking
211        // the structure of very short addresses.
212        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
225/// IP address masking: 192.168.1.100 → 192.168.1.*
226///
227/// IPv4: Hide last segment (content after last `.`).
228/// IPv6: Hide last `:` group (content after last colon).
229/// Returns as-is if unrecognized. `.` and `:` are ASCII single bytes, `rfind` returns byte position
230/// that is always a character boundary, slice is safe.
231fn 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
241/// IMEI masking: Keep first 6 digits and last 1 digit, replace middle with `****`
242///
243/// IMEI is 15 digits (3GPP TS 23.003), processed by Unicode characters for safety.
244fn 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
258/// License plate masking: 京A12345 → 京A12**45
259///
260/// Keep first (len-2) characters and last 2 characters, replace middle with `**`.
261/// Processed by Unicode characters, supports Chinese license plates (e.g. "京A12345").
262fn 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
287/// Parses a `"prefix,suffix"` spec into `(prefix, suffix)`. Returns `None`
288/// on malformed input or negative/overflowing values.
289fn 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// ---------------------------------------------------------------------------
300// 脱敏策略与审计工具
301// ---------------------------------------------------------------------------
302
303/// Masking strategy: define masking rules for multiple fields, applied in batch
304#[derive(Debug, Clone, Default)]
305pub struct MaskingPolicy {
306    rules: std::collections::HashMap<String, MaskingRule>,
307}
308
309impl MaskingPolicy {
310    /// Create empty strategy
311    pub fn new() -> Self {
312        Self::default()
313    }
314
315    /// Add field masking rule (chainable)
316    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    /// Get masking rule for a field
322    pub fn get_rule(&self, field: &str) -> Option<&MaskingRule> {
323        self.rules.get(field)
324    }
325
326    /// Remove masking rule for a field
327    pub fn remove_rule(&mut self, field: &str) -> Option<MaskingRule> {
328        self.rules.remove(field)
329    }
330
331    /// Number of registered fields
332    pub fn field_count(&self) -> usize {
333        self.rules.len()
334    }
335
336    /// Get all rule references
337    pub fn rules(&self) -> &std::collections::HashMap<String, MaskingRule> {
338        &self.rules
339    }
340
341    /// Apply masking to HashMap data
342    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    /// Apply masking to JSON string
350    pub fn apply_to_json(&self, json: &str) -> String {
351        DataMasker::mask_json(&self.rules, json)
352    }
353}
354
355/// Masking audit entry: records a single masking operation
356#[derive(Debug, Clone)]
357pub struct MaskAuditEntry {
358    field: String,
359    original_len: usize,
360    masked_len: usize,
361}
362
363impl MaskAuditEntry {
364    /// Field name
365    pub fn field(&self) -> &str {
366        &self.field
367    }
368
369    /// Original value length
370    pub fn original_len(&self) -> usize {
371        self.original_len
372    }
373
374    /// Masked length
375    pub fn masked_len(&self) -> usize {
376        self.masked_len
377    }
378
379    /// Whether truncated (masked is shorter than original)
380    pub fn was_truncated(&self) -> bool {
381        self.masked_len < self.original_len
382    }
383}
384
385/// Masking audit log: records all masking operations
386#[derive(Debug, Clone, Default)]
387pub struct MaskAuditLog {
388    entries: Vec<MaskAuditEntry>,
389}
390
391impl MaskAuditLog {
392    /// Create empty audit log
393    pub fn new() -> Self {
394        Self::default()
395    }
396
397    /// Record a masking operation
398    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    /// All audit entries
407    pub fn entries(&self) -> &[MaskAuditEntry] {
408        &self.entries
409    }
410
411    /// Entry count
412    pub fn entry_count(&self) -> usize {
413        self.entries.len()
414    }
415
416    /// Total masked characters (sum of original_len - masked_len)
417    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    /// Clear log
425    pub fn clear(&mut self) {
426        self.entries.clear();
427    }
428}
429
430/// Masking config: custom mask character, minimum mask length, fallback value
431#[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    /// Create default config
450    pub fn new() -> Self {
451        Self::default()
452    }
453
454    /// Mask character
455    pub fn mask_char(&self) -> char {
456        self.mask_char
457    }
458
459    /// Set mask character (chainable)
460    pub fn with_mask_char(mut self, c: char) -> Self {
461        self.mask_char = c;
462        self
463    }
464
465    /// Minimum mask length
466    pub fn min_mask_length(&self) -> usize {
467        self.min_mask_length
468    }
469
470    /// Set minimum mask length (chainable)
471    pub fn with_min_mask_length(mut self, min: usize) -> Self {
472        self.min_mask_length = min;
473        self
474    }
475
476    /// Fallback value
477    pub fn fallback_value(&self) -> &str {
478        &self.fallback
479    }
480
481    /// Set fallback value (chainable)
482    pub fn with_fallback_value(mut self, value: &str) -> Self {
483        self.fallback = value.to_string();
484        self
485    }
486}
487
488/// Masking statistics: tracks masking operation count per field
489#[derive(Debug, Clone, Default)]
490pub struct MaskingStats {
491    counts: std::collections::HashMap<String, u64>,
492}
493
494impl MaskingStats {
495    /// Create empty statistics
496    pub fn new() -> Self {
497        Self::default()
498    }
499
500    /// Record a masking operation
501    pub fn record(&mut self, field: &str) {
502        *self.counts.entry(field.to_string()).or_insert(0) += 1;
503    }
504
505    /// Total operation count
506    pub fn total_operations(&self) -> u64 {
507        self.counts.values().sum()
508    }
509
510    /// Operation count for a field
511    pub fn field_operations(&self, field: &str) -> u64 {
512        self.counts.get(field).copied().unwrap_or(0)
513    }
514
515    /// Field with most operations
516    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    // ----- Phone -----
529    #[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        // Less than 3+4 chars -> cannot safely reveal structure -> "***"
538        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        // 8 chars: prefix=3, suffix=4, hidden=1
545        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    // ----- Email -----
557    #[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    // ----- IdCard -----
587    #[test]
588    fn test_idcard_standard_18() {
589        let id = "110101199001012345";
590        let masked = DataMasker::apply(&MaskingRule::IdCard, id);
591        // First 4 + 10 stars + last 4 ("2345").
592        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    // ----- BankCard -----
608    #[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    // ----- Name -----
626    #[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    // ----- Address -----
652    #[test]
653    fn test_address_standard() {
654        let addr = "北京市海淀区中关村大街1号";
655        let masked = DataMasker::apply(&MaskingRule::Address, addr);
656        // First 6 chars kept ("北京市海淀区"), the rest replaced with one `*` per char.
657        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    // ----- Custom -----
679    #[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    // ----- Unicode safety -----
710    #[test]
711    fn test_unicode_no_panic() {
712        // Mixing CJK + emoji + ascii - just verify no panic and contains stars.
713        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        // Should start with first 3, end with last 4, all stars in between.
723        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    // ----- IP -----
738    #[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        // IPv6:隐藏最后一个冒号后的内容
757        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    // ----- IMEI -----
777    #[test]
778    fn test_imei_standard_15() {
779        // 15 位 IMEI:保留前 6 + **** + 最后 1 位
780        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    // ----- Plate -----
798    #[test]
799    fn test_plate_chinese_standard() {
800        // 京A12345(7 字符):前 5 + ** + 后 2
801        assert_eq!(
802            DataMasker::apply(&MaskingRule::Plate, "京A12345"),
803            "京A123**45"
804        );
805    }
806
807    #[test]
808    fn test_plate_with_separator() {
809        // 京A·12345(8 字符):前 6 + ** + 后 2
810        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        // 4 字符:前 2 + ** + 后 2
831        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        // 先手机号脱敏,再自定义前缀后缀
843        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    // --- MaskingPolicy tests ---
913
914    #[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    // --- MaskAuditLog tests ---
968
969    #[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    // --- MaskingConfig tests ---
1010
1011    #[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    // --- MaskingStats tests ---
1031
1032    #[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}