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