sql_splitter/redactor/strategy/
mask.rs1use super::{RedactValue, Strategy, StrategyKind};
4use rand::Rng;
5
6#[derive(Debug, Clone)]
14pub struct MaskStrategy {
15 pattern: String,
16}
17
18impl MaskStrategy {
19 pub fn new(pattern: String) -> Self {
20 Self { pattern }
21 }
22
23 fn mask_value(&self, value: &str, rng: &mut dyn rand::RngCore) -> String {
25 let chars: Vec<char> = value.chars().collect();
26 let mut result = String::with_capacity(self.pattern.len());
27
28 let mut value_idx = 0;
29
30 for pattern_char in self.pattern.chars() {
31 match pattern_char {
32 '*' => {
33 result.push('*');
34 value_idx += 1;
35 }
36 'X' => {
37 if value_idx < chars.len() {
38 result.push(chars[value_idx]);
39 }
40 value_idx += 1;
41 }
42 '#' => {
43 result.push(char::from_digit(rng.random_range(0..10), 10).unwrap());
44 value_idx += 1;
45 }
46 c => {
47 result.push(c);
49 if value_idx < chars.len() && chars[value_idx] == c {
51 value_idx += 1;
52 }
53 }
54 }
55 }
56
57 result
58 }
59}
60
61impl Strategy for MaskStrategy {
62 fn apply(&self, value: &RedactValue, rng: &mut dyn rand::RngCore) -> RedactValue {
63 match value {
64 RedactValue::Null => RedactValue::Null,
65 RedactValue::String(s) => RedactValue::String(self.mask_value(s, rng)),
66 RedactValue::Integer(i) => RedactValue::String(self.mask_value(&i.to_string(), rng)),
67 RedactValue::Bytes(b) => {
68 let s = String::from_utf8_lossy(b);
69 RedactValue::String(self.mask_value(&s, rng))
70 }
71 }
72 }
73
74 fn kind(&self) -> StrategyKind {
75 StrategyKind::Mask {
76 pattern: self.pattern.clone(),
77 }
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84 use rand::SeedableRng;
85
86 #[test]
87 fn test_mask_credit_card() {
88 let strategy = MaskStrategy::new("****-****-****-XXXX".to_string());
90 let mut rng = rand::rngs::StdRng::seed_from_u64(42);
91
92 let result = strategy.apply(
93 &RedactValue::String("4532-0151-1283-0366".to_string()),
94 &mut rng,
95 );
96 match result {
97 RedactValue::String(s) => {
98 assert!(s.starts_with("****-****-****-"));
99 assert!(s.ends_with("0366"));
100 }
101 _ => panic!("Expected String"),
102 }
103 }
104
105 #[test]
106 fn test_mask_email_first_char() {
107 let strategy = MaskStrategy::new("X***@*****".to_string());
109 let mut rng = rand::rngs::StdRng::seed_from_u64(42);
110
111 let result = strategy.apply(
112 &RedactValue::String("john@example.com".to_string()),
113 &mut rng,
114 );
115 match result {
116 RedactValue::String(s) => {
117 assert!(s.starts_with('j'));
118 assert!(s.contains('@'));
119 }
120 _ => panic!("Expected String"),
121 }
122 }
123
124 #[test]
125 fn test_mask_random_digits() {
126 let strategy = MaskStrategy::new("###-##-####".to_string());
127 let mut rng = rand::rngs::StdRng::seed_from_u64(42);
128
129 let result = strategy.apply(&RedactValue::String("123-45-6789".to_string()), &mut rng);
130 match result {
131 RedactValue::String(s) => {
132 assert_eq!(s.len(), 11);
133 assert!(s.chars().nth(3) == Some('-'));
134 assert!(s.chars().nth(6) == Some('-'));
135 }
136 _ => panic!("Expected String"),
137 }
138 }
139
140 #[test]
141 fn test_mask_null() {
142 let strategy = MaskStrategy::new("****".to_string());
143 let mut rng = rand::rngs::StdRng::seed_from_u64(42);
144
145 let result = strategy.apply(&RedactValue::Null, &mut rng);
146 assert!(matches!(result, RedactValue::Null));
147 }
148}