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