sql_splitter/redactor/strategy/
hash.rs1use super::{RedactValue, Strategy, StrategyKind};
4use sha2::{Digest, Sha256};
5
6#[derive(Debug, Clone)]
8pub struct HashStrategy {
9 preserve_domain: bool,
11}
12
13impl HashStrategy {
14 pub fn new(preserve_domain: bool) -> Self {
15 Self { preserve_domain }
16 }
17
18 fn hash_value(&self, value: &str) -> String {
20 if self.preserve_domain && value.contains('@') {
21 if let Some((local, domain)) = value.rsplit_once('@') {
23 let hash = self.compute_hash(local);
24 return format!("{}@{}", &hash[..8], domain);
25 }
26 }
27
28 let hash = self.compute_hash(value);
30 hash[..16].to_string()
31 }
32
33 fn compute_hash(&self, value: &str) -> String {
35 let mut hasher = Sha256::new();
36 hasher.update(value.as_bytes());
37 let result = hasher.finalize();
38 hex::encode(result)
39 }
40}
41
42impl Strategy for HashStrategy {
43 fn apply(&self, value: &RedactValue, _rng: &mut dyn rand::RngCore) -> RedactValue {
44 match value {
45 RedactValue::Null => RedactValue::Null,
46 RedactValue::String(s) => RedactValue::String(self.hash_value(s)),
47 RedactValue::Integer(i) => RedactValue::String(self.hash_value(&i.to_string())),
48 RedactValue::Bytes(b) => {
49 let s = String::from_utf8_lossy(b);
50 RedactValue::String(self.hash_value(&s))
51 }
52 }
53 }
54
55 fn kind(&self) -> StrategyKind {
56 StrategyKind::Hash {
57 preserve_domain: self.preserve_domain,
58 }
59 }
60}
61
62#[cfg(test)]
63mod tests {
64 use super::*;
65 use rand::SeedableRng;
66
67 #[test]
68 fn test_hash_strategy() {
69 let strategy = HashStrategy::new(false);
70 let mut rng = rand::rngs::StdRng::seed_from_u64(42);
71
72 let result = strategy.apply(&RedactValue::String("secret".to_string()), &mut rng);
73 match result {
74 RedactValue::String(s) => {
75 assert_eq!(s.len(), 16);
76 let result2 = strategy.apply(&RedactValue::String("secret".to_string()), &mut rng);
78 match result2 {
79 RedactValue::String(s2) => assert_eq!(s, s2),
80 _ => panic!("Expected String"),
81 }
82 }
83 _ => panic!("Expected String"),
84 }
85 }
86
87 #[test]
88 fn test_hash_preserve_domain() {
89 let strategy = HashStrategy::new(true);
90 let mut rng = rand::rngs::StdRng::seed_from_u64(42);
91
92 let result = strategy.apply(
93 &RedactValue::String("john.doe@example.com".to_string()),
94 &mut rng,
95 );
96 match result {
97 RedactValue::String(s) => {
98 assert!(s.ends_with("@example.com"));
99 assert!(s.len() > "@example.com".len());
100 }
101 _ => panic!("Expected String"),
102 }
103 }
104
105 #[test]
106 fn test_hash_deterministic() {
107 let strategy = HashStrategy::new(false);
108 let mut rng = rand::rngs::StdRng::seed_from_u64(42);
109
110 let result1 = strategy.apply(
112 &RedactValue::String("test@example.com".to_string()),
113 &mut rng,
114 );
115 let result2 = strategy.apply(
116 &RedactValue::String("test@example.com".to_string()),
117 &mut rng,
118 );
119
120 match (result1, result2) {
121 (RedactValue::String(s1), RedactValue::String(s2)) => assert_eq!(s1, s2),
122 _ => panic!("Expected Strings"),
123 }
124 }
125
126 #[test]
127 fn test_hash_null() {
128 let strategy = HashStrategy::new(false);
129 let mut rng = rand::rngs::StdRng::seed_from_u64(42);
130
131 let result = strategy.apply(&RedactValue::Null, &mut rng);
132 assert!(matches!(result, RedactValue::Null));
133 }
134}