Skip to main content

ash_rpc/
sanitization.rs

1//! Optional error sanitization utilities
2//!
3//! This module provides a trait-based approach for implementing custom
4//! error sanitization. Library users can implement the `Sanitizer` trait
5//! to define their own sanitization logic.
6//!
7//! # Example
8//! ```
9//! use ash_rpc::sanitization::Sanitizer;
10//! use ash_rpc::Error;
11//!
12//! struct MyCustomSanitizer;
13//!
14//! impl Sanitizer for MyCustomSanitizer {
15//!     fn sanitize(&self, error: &Error) -> Error {
16//!         // Your custom logic here
17//!         Error::new(error.code(), "Sanitized message")
18//!     }
19//! }
20//!
21//! # let error = Error::new(-32000, "Test");
22//! let sanitized = error.sanitized_with(|e| MyCustomSanitizer.sanitize(e));
23//! ```
24
25use crate::Error;
26
27/// Trait for implementing custom error sanitization logic
28///
29/// Implement this trait to define how errors should be sanitized
30/// before being sent to clients. This gives you full control over
31/// what information is exposed.
32pub trait Sanitizer {
33    /// Transform an error into a sanitized version
34    ///
35    /// # Arguments
36    /// * `error` - The original error to sanitize
37    ///
38    /// # Returns
39    /// A new Error with sanitized content
40    fn sanitize(&self, error: &Error) -> Error;
41}
42
43/// Trait for applying transformations to strings
44///
45/// Implement this to create custom pattern-based transformations
46/// for error messages and data.
47pub trait PatternTransform {
48    /// Apply the transformation to a string
49    fn apply(&self, input: &str) -> String;
50}
51
52/// Simple pattern replacement implementation
53pub struct SimplePattern {
54    /// Pattern to search for (case-sensitive)
55    pub pattern: String,
56    /// Replacement text
57    pub replacement: String,
58}
59
60impl SimplePattern {
61    pub fn new(pattern: impl Into<String>, replacement: impl Into<String>) -> Self {
62        Self {
63            pattern: pattern.into(),
64            replacement: replacement.into(),
65        }
66    }
67}
68
69impl PatternTransform for SimplePattern {
70    fn apply(&self, input: &str) -> String {
71        input.replace(&self.pattern, &self.replacement)
72    }
73}
74
75/// Case-insensitive pattern replacement
76pub struct CaseInsensitivePattern {
77    /// Pattern to search for (case-insensitive)
78    pub pattern: String,
79    /// Replacement text
80    pub replacement: String,
81}
82
83impl CaseInsensitivePattern {
84    pub fn new(pattern: impl Into<String>, replacement: impl Into<String>) -> Self {
85        Self {
86            pattern: pattern.into(),
87            replacement: replacement.into(),
88        }
89    }
90}
91
92impl PatternTransform for CaseInsensitivePattern {
93    fn apply(&self, input: &str) -> String {
94        let pattern_lower = self.pattern.to_lowercase();
95        let input_lower = input.to_lowercase();
96
97        if let Some(pos) = input_lower.find(&pattern_lower) {
98            let mut result = input.to_string();
99            result.replace_range(pos..pos + self.pattern.len(), &self.replacement);
100
101            // Recursively handle multiple occurrences
102            if result.to_lowercase().contains(&pattern_lower) {
103                return self.apply(&result);
104            }
105            result
106        } else {
107            input.to_string()
108        }
109    }
110}
111
112/// Compose multiple transformations
113pub struct CompositeTransform {
114    transforms: Vec<Box<dyn PatternTransform + Send + Sync>>,
115}
116
117impl CompositeTransform {
118    pub fn new() -> Self {
119        Self {
120            transforms: Vec::new(),
121        }
122    }
123
124    pub fn add_transform<T: PatternTransform + Send + Sync + 'static>(
125        mut self,
126        transform: T,
127    ) -> Self {
128        self.transforms.push(Box::new(transform));
129        self
130    }
131}
132
133impl Default for CompositeTransform {
134    fn default() -> Self {
135        Self::new()
136    }
137}
138
139impl PatternTransform for CompositeTransform {
140    fn apply(&self, input: &str) -> String {
141        self.transforms
142            .iter()
143            .fold(input.to_string(), |acc, transform| transform.apply(&acc))
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn test_simple_pattern() {
153        let pattern = SimplePattern::new("password", "[REDACTED]");
154        let result = pattern.apply("The password is secret123");
155        assert!(result.contains("[REDACTED]"));
156        assert!(!result.contains("password"));
157    }
158
159    #[test]
160    fn test_case_insensitive_pattern() {
161        let pattern = CaseInsensitivePattern::new("password", "[REDACTED]");
162        let result = pattern.apply("The PASSWORD is secret123");
163        assert!(result.contains("[REDACTED]"));
164    }
165
166    #[test]
167    fn test_composite_transform() {
168        let composite = CompositeTransform::new()
169            .add_transform(SimplePattern::new("password", "[REDACTED]"))
170            .add_transform(SimplePattern::new("token", "[REDACTED]"));
171
172        let result = composite.apply("password is secret, token is abc123");
173        assert!(result.contains("[REDACTED]"));
174        assert!(!result.contains("password"));
175        assert!(!result.contains("token"));
176    }
177
178    #[test]
179    fn test_simple_pattern_no_match() {
180        let pattern = SimplePattern::new("password", "[REDACTED]");
181        let result = pattern.apply("This text has no sensitive data");
182        assert_eq!(result, "This text has no sensitive data");
183    }
184
185    #[test]
186    fn test_simple_pattern_multiple_occurrences() {
187        let pattern = SimplePattern::new("key", "[REDACTED]");
188        let result = pattern.apply("key1, key2, key3");
189        assert_eq!(result.matches("[REDACTED]").count(), 3);
190    }
191
192    #[test]
193    fn test_case_insensitive_pattern_various_cases() {
194        let pattern = CaseInsensitivePattern::new("secret", "[REDACTED]");
195
196        let result1 = pattern.apply("SECRET value");
197        assert!(result1.contains("[REDACTED]"));
198
199        let result2 = pattern.apply("Secret value");
200        assert!(result2.contains("[REDACTED]"));
201
202        let result3 = pattern.apply("secret value");
203        assert!(result3.contains("[REDACTED]"));
204    }
205
206    #[test]
207    fn test_case_insensitive_pattern_no_match() {
208        let pattern = CaseInsensitivePattern::new("password", "[REDACTED]");
209        let result = pattern.apply("No sensitive data here");
210        assert_eq!(result, "No sensitive data here");
211    }
212
213    #[test]
214    fn test_case_insensitive_pattern_multiple_occurrences() {
215        let pattern = CaseInsensitivePattern::new("pass", "[REDACTED]");
216        let result = pattern.apply("pass PASS Pass");
217        // Should handle recursive replacements
218        assert!(result.contains("[REDACTED]"));
219    }
220
221    #[test]
222    fn test_composite_transform_empty() {
223        let composite = CompositeTransform::new();
224        let result = composite.apply("test string");
225        assert_eq!(result, "test string");
226    }
227
228    #[test]
229    fn test_composite_transform_default() {
230        let composite = CompositeTransform::default();
231        let result = composite.apply("test");
232        assert_eq!(result, "test");
233    }
234
235    #[test]
236    fn test_composite_transform_single() {
237        let composite = CompositeTransform::new().add_transform(SimplePattern::new("test", "TEST"));
238
239        let result = composite.apply("test string");
240        assert_eq!(result, "TEST string");
241    }
242
243    #[test]
244    fn test_composite_transform_chained() {
245        let composite = CompositeTransform::new()
246            .add_transform(SimplePattern::new("a", "b"))
247            .add_transform(SimplePattern::new("b", "c"))
248            .add_transform(SimplePattern::new("c", "d"));
249
250        let result = composite.apply("a");
251        assert_eq!(result, "d");
252    }
253
254    #[test]
255    fn test_pattern_transform_trait() {
256        let pattern: Box<dyn PatternTransform> = Box::new(SimplePattern::new("test", "replaced"));
257        let result = pattern.apply("test value");
258        assert_eq!(result, "replaced value");
259    }
260}