Skip to main content

sz_orm_masking/
maskers.rs

1//! 高级脱敏器:哈希脱敏、部分显示脱敏、模式匹配脱敏。
2//!
3//! 这些脱敏器提供比 [`crate::DataMasker`] 更灵活的脱敏方式:
4//! - [`HashMasker`] — 确定性哈希脱敏,保留哈希前缀用于数据关联
5//! - [`PartialDisplayMasker`] — 灵活的部分显示,自定义掩码字符和保留规则
6//! - [`PatternMasker`] — 通配符模式匹配脱敏
7
8use std::collections::hash_map::DefaultHasher;
9use std::collections::HashMap;
10use std::hash::{Hash, Hasher};
11
12use serde::{Deserialize, Serialize};
13
14// ============================================================================
15// 哈希脱敏器
16// ============================================================================
17
18/// 哈希算法选择
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
20pub enum HashAlgorithm {
21    /// SipHash-1-3(std 默认,确定性,64 位)
22    SipHash,
23    /// FNV-1a 32 位
24    Fnv1a32,
25    /// FNV-1a 64 位
26    Fnv1a64,
27}
28
29impl Default for HashAlgorithm {
30    fn default() -> Self {
31        Self::SipHash
32    }
33}
34
35impl HashAlgorithm {
36    /// 算法名
37    pub fn as_str(&self) -> &'static str {
38        match self {
39            Self::SipHash => "siphash",
40            Self::Fnv1a32 => "fnv1a32",
41            Self::Fnv1a64 => "fnv1a64",
42        }
43    }
44
45    /// 从名称解析算法
46    pub fn parse_name(name: &str) -> Option<Self> {
47        match name.to_lowercase().as_str() {
48            "siphash" => Some(Self::SipHash),
49            "fnv1a32" => Some(Self::Fnv1a32),
50            "fnv1a64" => Some(Self::Fnv1a64),
51            _ => None,
52        }
53    }
54
55    /// 计算哈希并返回十六进制字符串
56    pub fn hash_hex(&self, input: &str) -> String {
57        match self {
58            Self::SipHash => {
59                let mut hasher = DefaultHasher::new();
60                input.hash(&mut hasher);
61                format!("{:016x}", hasher.finish())
62            }
63            Self::Fnv1a32 => format!("{:08x}", fnv1a_32(input.as_bytes())),
64            Self::Fnv1a64 => format!("{:016x}", fnv1a_64(input.as_bytes())),
65        }
66    }
67}
68
69/// FNV-1a 32 位哈希
70fn fnv1a_32(bytes: &[u8]) -> u32 {
71    const FNV_OFFSET: u32 = 0x811c9dc5;
72    const FNV_PRIME: u32 = 0x01000193;
73    let mut hash = FNV_OFFSET;
74    for &byte in bytes {
75        hash ^= u32::from(byte);
76        hash = hash.wrapping_mul(FNV_PRIME);
77    }
78    hash
79}
80
81/// FNV-1a 64 位哈希
82fn fnv1a_64(bytes: &[u8]) -> u64 {
83    const FNV_OFFSET: u64 = 0xcbf29ce484222325;
84    const FNV_PRIME: u64 = 0x00000100000001b3;
85    let mut hash = FNV_OFFSET;
86    for &byte in bytes {
87        hash ^= u64::from(byte);
88        hash = hash.wrapping_mul(FNV_PRIME);
89    }
90    hash
91}
92
93/// 哈希脱敏器:将输入计算为确定性哈希值,保留前 N 位用于数据关联。
94///
95/// 适用于需要脱敏但仍需关联同一实体的场景(如用户 ID 脱敏后仍可聚合统计)。
96/// 同一输入始终产生同一输出,可在不同系统间做关联分析而不泄露原始值。
97#[derive(Debug, Clone)]
98pub struct HashMasker {
99    algorithm: HashAlgorithm,
100    keep_prefix: usize,
101    suffix: String,
102}
103
104impl Default for HashMasker {
105    fn default() -> Self {
106        Self {
107            algorithm: HashAlgorithm::default(),
108            keep_prefix: 12,
109            suffix: "...".to_string(),
110        }
111    }
112}
113
114impl HashMasker {
115    /// 创建默认哈希脱敏器(SipHash,保留 12 位前缀)
116    pub fn new() -> Self {
117        Self::default()
118    }
119
120    /// 设置哈希算法(链式)
121    pub fn with_algorithm(mut self, algo: HashAlgorithm) -> Self {
122        self.algorithm = algo;
123        self
124    }
125
126    /// 设置保留前缀长度(链式)
127    pub fn with_keep_prefix(mut self, n: usize) -> Self {
128        self.keep_prefix = n;
129        self
130    }
131
132    /// 设置后缀(链式)
133    pub fn with_suffix(mut self, suffix: &str) -> Self {
134        self.suffix = suffix.to_string();
135        self
136    }
137
138    /// 哈希算法
139    pub fn algorithm(&self) -> HashAlgorithm {
140        self.algorithm
141    }
142
143    /// 保留前缀长度
144    pub fn keep_prefix(&self) -> usize {
145        self.keep_prefix
146    }
147
148    /// 后缀
149    pub fn suffix(&self) -> &str {
150        &self.suffix
151    }
152
153    /// 脱敏单个值
154    pub fn mask(&self, value: &str) -> String {
155        if value.is_empty() {
156            return self.suffix.clone();
157        }
158        let hex = self.algorithm.hash_hex(value);
159        let prefix = if hex.len() <= self.keep_prefix {
160            hex.as_str()
161        } else {
162            &hex[..self.keep_prefix]
163        };
164        format!("{}{}", prefix, self.suffix)
165    }
166
167    /// 脱敏 HashMap 中的指定字段
168    pub fn mask_fields(
169        &self,
170        fields: &[String],
171        data: &HashMap<String, String>,
172    ) -> HashMap<String, String> {
173        data.iter()
174            .map(|(k, v)| {
175                if fields.contains(k) {
176                    (k.clone(), self.mask(v))
177                } else {
178                    (k.clone(), v.clone())
179                }
180            })
181            .collect()
182    }
183
184    /// 脱敏 JSON 字符串中的指定字段
185    pub fn mask_json(&self, fields: &[String], json: &str) -> String {
186        let Ok(mut value) = serde_json::from_str::<serde_json::Value>(json) else {
187            return json.to_string();
188        };
189        let Some(obj) = value.as_object_mut() else {
190            return json.to_string();
191        };
192        for field in fields {
193            if let Some(serde_json::Value::String(s)) = obj.get_mut(field) {
194                *s = self.mask(s);
195            }
196        }
197        serde_json::to_string(&value).unwrap_or_else(|_| json.to_string())
198    }
199
200    /// 批量脱敏:对同一值列表逐个脱敏,保持顺序
201    pub fn mask_batch(&self, values: &[String]) -> Vec<String> {
202        values.iter().map(|v| self.mask(v)).collect()
203    }
204}
205
206// ============================================================================
207// 部分显示脱敏器
208// ============================================================================
209
210/// 部分显示脱敏器:保留前 N 和后 M 字符,中间用自定义掩码字符替代。
211///
212/// 比 [`crate::DataMasker`] 的固定规则更灵活:可自定义掩码字符、
213/// 最小掩码长度、兜底值,并支持 Unicode 安全操作。
214#[derive(Debug, Clone)]
215pub struct PartialDisplayMasker {
216    prefix_keep: usize,
217    suffix_keep: usize,
218    mask_char: char,
219    min_mask_length: usize,
220    fallback: String,
221}
222
223impl Default for PartialDisplayMasker {
224    fn default() -> Self {
225        Self {
226            prefix_keep: 3,
227            suffix_keep: 4,
228            mask_char: '*',
229            min_mask_length: 3,
230            fallback: "***".to_string(),
231        }
232    }
233}
234
235impl PartialDisplayMasker {
236    /// 创建默认脱敏器(前 3 后 4,`*` 掩码)
237    pub fn new() -> Self {
238        Self::default()
239    }
240
241    /// 设置前缀保留数(链式)
242    pub fn with_prefix(mut self, n: usize) -> Self {
243        self.prefix_keep = n;
244        self
245    }
246
247    /// 设置后缀保留数(链式)
248    pub fn with_suffix_keep(mut self, n: usize) -> Self {
249        self.suffix_keep = n;
250        self
251    }
252
253    /// 设置掩码字符(链式)
254    pub fn with_mask_char(mut self, c: char) -> Self {
255        self.mask_char = c;
256        self
257    }
258
259    /// 设置最小掩码长度(链式)
260    pub fn with_min_mask_length(mut self, n: usize) -> Self {
261        self.min_mask_length = n;
262        self
263    }
264
265    /// 设置兜底值(链式)
266    pub fn with_fallback(mut self, fallback: &str) -> Self {
267        self.fallback = fallback.to_string();
268        self
269    }
270
271    /// 前缀保留数
272    pub fn prefix_keep(&self) -> usize {
273        self.prefix_keep
274    }
275
276    /// 后缀保留数
277    pub fn suffix_keep(&self) -> usize {
278        self.suffix_keep
279    }
280
281    /// 掩码字符
282    pub fn mask_char(&self) -> char {
283        self.mask_char
284    }
285
286    /// 脱敏单个值
287    pub fn mask(&self, value: &str) -> String {
288        let chars: Vec<char> = value.chars().collect();
289        let len = chars.len();
290        if len == 0 {
291            return self.fallback.clone();
292        }
293        let need = self.prefix_keep + self.suffix_keep;
294        if len <= need {
295            return self.fallback.clone();
296        }
297        let hidden = len - need;
298        let mask_len = hidden.max(self.min_mask_length);
299        let mut out = String::with_capacity(len + mask_len);
300        for &c in &chars[..self.prefix_keep] {
301            out.push(c);
302        }
303        for _ in 0..mask_len {
304            out.push(self.mask_char);
305        }
306        for &c in &chars[len - self.suffix_keep..] {
307            out.push(c);
308        }
309        out
310    }
311
312    /// 脱敏 HashMap 中的指定字段
313    pub fn mask_fields(
314        &self,
315        fields: &[String],
316        data: &HashMap<String, String>,
317    ) -> HashMap<String, String> {
318        data.iter()
319            .map(|(k, v)| {
320                if fields.contains(k) {
321                    (k.clone(), self.mask(v))
322                } else {
323                    (k.clone(), v.clone())
324                }
325            })
326            .collect()
327    }
328}
329
330// ============================================================================
331// 模式匹配脱敏器
332// ============================================================================
333
334/// 通配符模式匹配器:支持 `*`(任意序列)和 `?`(单字符)。
335///
336/// 用于按字段名模式批量应用脱敏规则,例如 `user_*` 匹配所有以 `user_` 开头的字段。
337#[derive(Debug, Clone)]
338pub struct PatternMasker {
339    patterns: Vec<(String, crate::MaskingRule)>,
340}
341
342impl Default for PatternMasker {
343    fn default() -> Self {
344        Self {
345            patterns: Vec::new(),
346        }
347    }
348}
349
350impl PatternMasker {
351    /// 创建空模式匹配器
352    pub fn new() -> Self {
353        Self::default()
354    }
355
356    /// 添加模式与对应脱敏规则(链式)
357    pub fn add_pattern(mut self, pattern: &str, rule: crate::MaskingRule) -> Self {
358        self.patterns.push((pattern.to_string(), rule));
359        self
360    }
361
362    /// 模式数量
363    pub fn pattern_count(&self) -> usize {
364        self.patterns.len()
365    }
366
367    /// 清空所有模式
368    pub fn clear(&mut self) {
369        self.patterns.clear();
370    }
371
372    /// 查找字段名匹配的第一个模式对应的规则
373    pub fn match_rule(&self, field: &str) -> Option<&crate::MaskingRule> {
374        self.patterns
375            .iter()
376            .find(|(pat, _)| wildcard_match(pat, field))
377            .map(|(_, rule)| rule)
378    }
379
380    /// 对 HashMap 应用模式匹配脱敏
381    pub fn mask_map(&self, data: &HashMap<String, String>) -> HashMap<String, String> {
382        data.iter()
383            .map(|(k, v)| match self.match_rule(k) {
384                Some(rule) => (k.clone(), crate::DataMasker::apply(rule, v)),
385                None => (k.clone(), v.clone()),
386            })
387            .collect()
388    }
389
390    /// 对 JSON 字符串应用模式匹配脱敏
391    pub fn mask_json(&self, json: &str) -> String {
392        let Ok(mut value) = serde_json::from_str::<serde_json::Value>(json) else {
393            return json.to_string();
394        };
395        let Some(obj) = value.as_object_mut() else {
396            return json.to_string();
397        };
398        let keys: Vec<String> = obj.keys().cloned().collect();
399        for key in keys {
400            if let Some(rule) = self.match_rule(&key) {
401                if let Some(serde_json::Value::String(s)) = obj.get_mut(&key) {
402                    *s = crate::DataMasker::apply(rule, s);
403                }
404            }
405        }
406        serde_json::to_string(&value).unwrap_or_else(|_| json.to_string())
407    }
408}
409
410/// 通配符匹配:`*` 匹配任意字符序列,`?` 匹配单个字符。
411///
412/// 使用动态规划实现,时间复杂度 O(m*n)。
413pub fn wildcard_match(pattern: &str, text: &str) -> bool {
414    let pat: Vec<char> = pattern.chars().collect();
415    let txt: Vec<char> = text.chars().collect();
416    let m = pat.len();
417    let n = txt.len();
418
419    // dp[i][j] = pattern[..i] 匹配 text[..j]
420    let mut dp = vec![vec![false; n + 1]; m + 1];
421    dp[0][0] = true;
422
423    // pattern 以 * 开头时可以匹配空串
424    for i in 1..=m {
425        if pat[i - 1] == '*' {
426            dp[i][0] = dp[i - 1][0];
427        }
428    }
429
430    for i in 1..=m {
431        for j in 1..=n {
432            match pat[i - 1] {
433                '*' => dp[i][j] = dp[i - 1][j] || dp[i][j - 1],
434                '?' => dp[i][j] = dp[i - 1][j - 1],
435                c => dp[i][j] = dp[i - 1][j - 1] && c == txt[j - 1],
436            }
437        }
438    }
439
440    dp[m][n]
441}
442
443#[cfg(test)]
444mod tests {
445    use super::*;
446
447    // ----- HashAlgorithm -----
448
449    #[test]
450    fn hash_algorithm_default_is_siphash() {
451        assert_eq!(HashAlgorithm::default(), HashAlgorithm::SipHash);
452    }
453
454    #[test]
455    fn hash_algorithm_as_str() {
456        assert_eq!(HashAlgorithm::SipHash.as_str(), "siphash");
457        assert_eq!(HashAlgorithm::Fnv1a32.as_str(), "fnv1a32");
458        assert_eq!(HashAlgorithm::Fnv1a64.as_str(), "fnv1a64");
459    }
460
461    #[test]
462    fn hash_algorithm_parse_name_valid() {
463        assert_eq!(
464            HashAlgorithm::parse_name("siphash"),
465            Some(HashAlgorithm::SipHash)
466        );
467        assert_eq!(
468            HashAlgorithm::parse_name("FNV1A32"),
469            Some(HashAlgorithm::Fnv1a32)
470        );
471        assert_eq!(
472            HashAlgorithm::parse_name("fnv1a64"),
473            Some(HashAlgorithm::Fnv1a64)
474        );
475    }
476
477    #[test]
478    fn hash_algorithm_parse_name_invalid() {
479        assert_eq!(HashAlgorithm::parse_name("md5"), None);
480        assert_eq!(HashAlgorithm::parse_name(""), None);
481    }
482
483    #[test]
484    fn hash_algorithm_siphash_deterministic() {
485        let a = HashAlgorithm::SipHash.hash_hex("hello");
486        let b = HashAlgorithm::SipHash.hash_hex("hello");
487        assert_eq!(a, b);
488        assert_eq!(a.len(), 16);
489    }
490
491    #[test]
492    fn hash_algorithm_fnv1a32_deterministic() {
493        let a = HashAlgorithm::Fnv1a32.hash_hex("test");
494        let b = HashAlgorithm::Fnv1a32.hash_hex("test");
495        assert_eq!(a, b);
496        assert_eq!(a.len(), 8);
497    }
498
499    #[test]
500    fn hash_algorithm_fnv1a64_deterministic() {
501        let a = HashAlgorithm::Fnv1a64.hash_hex("test");
502        let b = HashAlgorithm::Fnv1a64.hash_hex("test");
503        assert_eq!(a, b);
504        assert_eq!(a.len(), 16);
505    }
506
507    #[test]
508    fn hash_algorithm_different_inputs_different_hashes() {
509        let a = HashAlgorithm::SipHash.hash_hex("alice");
510        let b = HashAlgorithm::SipHash.hash_hex("bob");
511        assert_ne!(a, b);
512    }
513
514    #[test]
515    fn hash_algorithm_empty_input() {
516        let h = HashAlgorithm::SipHash.hash_hex("");
517        assert!(!h.is_empty());
518    }
519
520    #[test]
521    fn fnv1a_32_known_values() {
522        // FNV-1a 32 位空串 = offset basis
523        assert_eq!(fnv1a_32(b""), 0x811c9dc5);
524    }
525
526    #[test]
527    fn fnv1a_64_known_values() {
528        // FNV-1a 64 位空串 = offset basis
529        assert_eq!(fnv1a_64(b""), 0xcbf29ce484222325);
530    }
531
532    #[test]
533    fn fnv1a_32_single_byte() {
534        let h = fnv1a_32(b"a");
535        // 手动计算:(offset ^ 97) * prime
536        let expected = (0x811c9dc5u32 ^ 97).wrapping_mul(0x01000193);
537        assert_eq!(h, expected);
538    }
539
540    // ----- HashMasker -----
541
542    #[test]
543    fn hash_masker_default() {
544        let m = HashMasker::new();
545        assert_eq!(m.algorithm(), HashAlgorithm::SipHash);
546        assert_eq!(m.keep_prefix(), 12);
547        assert_eq!(m.suffix(), "...");
548    }
549
550    #[test]
551    fn hash_masker_mask_basic() {
552        let m = HashMasker::new();
553        let result = m.mask("13812345678");
554        assert!(result.ends_with("..."));
555        assert!(result.len() > 3);
556    }
557
558    #[test]
559    fn hash_masker_mask_empty() {
560        let m = HashMasker::new();
561        assert_eq!(m.mask(""), "...");
562    }
563
564    #[test]
565    fn hash_masker_deterministic() {
566        let m = HashMasker::new();
567        let a = m.mask("same_value");
568        let b = m.mask("same_value");
569        assert_eq!(a, b);
570    }
571
572    #[test]
573    fn hash_masker_different_values_different_output() {
574        let m = HashMasker::new();
575        let a = m.mask("alice");
576        let b = m.mask("bob");
577        assert_ne!(a, b);
578    }
579
580    #[test]
581    fn hash_masker_with_algorithm_fnv1a32() {
582        let m = HashMasker::new().with_algorithm(HashAlgorithm::Fnv1a32);
583        let result = m.mask("test");
584        assert!(result.ends_with("..."));
585        assert_eq!(m.algorithm(), HashAlgorithm::Fnv1a32);
586    }
587
588    #[test]
589    fn hash_masker_with_keep_prefix() {
590        let m = HashMasker::new().with_keep_prefix(4);
591        let result = m.mask("hello");
592        // 4 hex chars + "..."
593        assert_eq!(result.len(), 7);
594    }
595
596    #[test]
597    fn hash_masker_with_suffix() {
598        let m = HashMasker::new().with_suffix("[hashed]");
599        let result = m.mask("value");
600        assert!(result.ends_with("[hashed]"));
601    }
602
603    #[test]
604    fn hash_masker_keep_prefix_exceeds_hash_len() {
605        // keep_prefix > hash hex length → use full hex
606        let m = HashMasker::new().with_keep_prefix(100);
607        let result = m.mask("test");
608        // SipHash produces 16 hex chars + "..."
609        assert_eq!(result.len(), 19);
610    }
611
612    #[test]
613    fn hash_masker_mask_fields() {
614        let m = HashMasker::new();
615        let mut data = HashMap::new();
616        data.insert("phone".to_string(), "13812345678".to_string());
617        data.insert("name".to_string(), "Alice".to_string());
618        let fields = vec!["phone".to_string()];
619        let result = m.mask_fields(&fields, &data);
620        assert_ne!(result["phone"], "13812345678");
621        assert_eq!(result["name"], "Alice");
622    }
623
624    #[test]
625    fn hash_masker_mask_json() {
626        let m = HashMasker::new();
627        let json = r#"{"phone":"13812345678","name":"Alice"}"#;
628        let fields = vec!["phone".to_string()];
629        let result = m.mask_json(&fields, json);
630        assert!(result.contains("Alice"));
631        assert!(!result.contains("13812345678"));
632    }
633
634    #[test]
635    fn hash_masker_mask_json_invalid() {
636        let m = HashMasker::new();
637        let result = m.mask_json(&["phone".to_string()], "not json");
638        assert_eq!(result, "not json");
639    }
640
641    #[test]
642    fn hash_masker_mask_batch() {
643        let m = HashMasker::new();
644        let values = vec!["a".to_string(), "b".to_string(), "c".to_string()];
645        let result = m.mask_batch(&values);
646        assert_eq!(result.len(), 3);
647        assert_ne!(result[0], result[1]);
648    }
649
650    // ----- PartialDisplayMasker -----
651
652    #[test]
653    fn partial_display_default() {
654        let m = PartialDisplayMasker::new();
655        assert_eq!(m.prefix_keep(), 3);
656        assert_eq!(m.suffix_keep(), 4);
657        assert_eq!(m.mask_char(), '*');
658    }
659
660    #[test]
661    fn partial_display_mask_basic() {
662        let m = PartialDisplayMasker::new();
663        assert_eq!(m.mask("13812345678"), "138****5678");
664    }
665
666    #[test]
667    fn partial_display_mask_too_short() {
668        let m = PartialDisplayMasker::new();
669        assert_eq!(m.mask("123"), "***");
670    }
671
672    #[test]
673    fn partial_display_mask_empty() {
674        let m = PartialDisplayMasker::new();
675        assert_eq!(m.mask(""), "***");
676    }
677
678    #[test]
679    fn partial_display_custom_mask_char() {
680        let m = PartialDisplayMasker::new().with_mask_char('#');
681        assert_eq!(m.mask("13812345678"), "138####5678");
682    }
683
684    #[test]
685    fn partial_display_custom_prefix_suffix() {
686        let m = PartialDisplayMasker::new()
687            .with_prefix(2)
688            .with_suffix_keep(2);
689        assert_eq!(m.mask("abcdefgh"), "ab****gh");
690    }
691
692    #[test]
693    fn partial_display_min_mask_length() {
694        let m = PartialDisplayMasker::new()
695            .with_prefix(3)
696            .with_suffix_keep(4)
697            .with_min_mask_length(6);
698        // 8 chars: 3 prefix + 4 suffix = 7, hidden = 1, min_mask = 6
699        assert_eq!(m.mask("12345678"), "123******5678");
700    }
701
702    #[test]
703    fn partial_display_custom_fallback() {
704        let m = PartialDisplayMasker::new().with_fallback("[hidden]");
705        assert_eq!(m.mask(""), "[hidden]");
706        assert_eq!(m.mask("ab"), "[hidden]");
707    }
708
709    #[test]
710    fn partial_display_unicode() {
711        let m = PartialDisplayMasker::new()
712            .with_prefix(1)
713            .with_suffix_keep(1);
714        // 中文:5 chars, prefix=1, suffix=1, hidden=3
715        assert_eq!(m.mask("张三李四王"), "张***王");
716    }
717
718    #[test]
719    fn partial_display_mask_fields() {
720        let m = PartialDisplayMasker::new();
721        let mut data = HashMap::new();
722        data.insert("phone".to_string(), "13812345678".to_string());
723        data.insert("name".to_string(), "Alice".to_string());
724        let fields = vec!["phone".to_string()];
725        let result = m.mask_fields(&fields, &data);
726        assert_eq!(result["phone"], "138****5678");
727        assert_eq!(result["name"], "Alice");
728    }
729
730    #[test]
731    fn partial_display_exact_boundary() {
732        // len == prefix + suffix → fallback
733        let m = PartialDisplayMasker::new();
734        assert_eq!(m.mask("1234567"), "***");
735    }
736
737    #[test]
738    fn partial_display_one_past_boundary() {
739        // len == prefix + suffix + 1 → hidden=1, but min_mask_length=3 → 3 mask chars
740        let m = PartialDisplayMasker::new();
741        assert_eq!(m.mask("12345678"), "123***5678");
742    }
743
744    // ----- PatternMasker / wildcard_match -----
745
746    #[test]
747    fn wildcard_exact_match() {
748        assert!(wildcard_match("phone", "phone"));
749    }
750
751    #[test]
752    fn wildcard_no_match() {
753        assert!(!wildcard_match("phone", "email"));
754    }
755
756    #[test]
757    fn wildcard_star_match_prefix() {
758        assert!(wildcard_match("user_*", "user_name"));
759        assert!(wildcard_match("user_*", "user_id"));
760    }
761
762    #[test]
763    fn wildcard_star_match_suffix() {
764        assert!(wildcard_match("*_id", "user_id"));
765        assert!(wildcard_match("*_id", "order_id"));
766    }
767
768    #[test]
769    fn wildcard_star_match_entire() {
770        assert!(wildcard_match("*", "anything"));
771        assert!(wildcard_match("*", ""));
772    }
773
774    #[test]
775    fn wildcard_question_match_single() {
776        assert!(wildcard_match("user_?", "user_1"));
777        assert!(!wildcard_match("user_?", "user_12"));
778    }
779
780    #[test]
781    fn wildcard_combined_star_question() {
782        assert!(wildcard_match("u*r?", "user_"));
783        assert!(wildcard_match("?*?", "abc"));
784    }
785
786    #[test]
787    fn wildcard_empty_pattern() {
788        assert!(wildcard_match("", ""));
789        assert!(!wildcard_match("", "a"));
790    }
791
792    #[test]
793    fn wildcard_star_only() {
794        assert!(wildcard_match("***", "test"));
795        assert!(wildcard_match("***", ""));
796    }
797
798    #[test]
799    fn pattern_masker_default_empty() {
800        let m = PatternMasker::new();
801        assert_eq!(m.pattern_count(), 0);
802    }
803
804    #[test]
805    fn pattern_masker_add_pattern() {
806        let m = PatternMasker::new().add_pattern("phone_*", crate::MaskingRule::Phone);
807        assert_eq!(m.pattern_count(), 1);
808    }
809
810    #[test]
811    fn pattern_masker_match_rule() {
812        let m = PatternMasker::new()
813            .add_pattern("phone_*", crate::MaskingRule::Phone)
814            .add_pattern("*_email", crate::MaskingRule::Email);
815        assert!(m.match_rule("phone_primary").is_some());
816        assert!(m.match_rule("user_email").is_some());
817        assert!(m.match_rule("address").is_none());
818    }
819
820    #[test]
821    fn pattern_masker_clear() {
822        let mut m = PatternMasker::new().add_pattern("*", crate::MaskingRule::Phone);
823        m.clear();
824        assert_eq!(m.pattern_count(), 0);
825    }
826
827    #[test]
828    fn pattern_masker_mask_map() {
829        let m = PatternMasker::new()
830            .add_pattern("phone_*", crate::MaskingRule::Phone)
831            .add_pattern("*_email", crate::MaskingRule::Email);
832        let mut data = HashMap::new();
833        data.insert("phone_primary".to_string(), "13812345678".to_string());
834        data.insert("user_email".to_string(), "test@example.com".to_string());
835        data.insert("name".to_string(), "Alice".to_string());
836        let result = m.mask_map(&data);
837        assert_eq!(result["phone_primary"], "138****5678");
838        assert_eq!(result["user_email"], "t***@example.com");
839        assert_eq!(result["name"], "Alice");
840    }
841
842    #[test]
843    fn pattern_masker_mask_json() {
844        let m = PatternMasker::new().add_pattern("phone", crate::MaskingRule::Phone);
845        let json = r#"{"phone":"13812345678","name":"Alice"}"#;
846        let result = m.mask_json(json);
847        assert!(result.contains("138****5678"));
848        assert!(result.contains("Alice"));
849    }
850
851    #[test]
852    fn pattern_masker_mask_json_invalid() {
853        let m = PatternMasker::new().add_pattern("*", crate::MaskingRule::Phone);
854        assert_eq!(m.mask_json("not json"), "not json");
855    }
856
857    #[test]
858    fn pattern_masker_no_match_passthrough() {
859        let m = PatternMasker::new().add_pattern("secret_*", crate::MaskingRule::Password);
860        let mut data = HashMap::new();
861        data.insert("public_field".to_string(), "visible".to_string());
862        let result = m.mask_map(&data);
863        assert_eq!(result["public_field"], "visible");
864    }
865}