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_owned();
99            #[allow(clippy::arithmetic_side_effects)]
100            result.replace_range(pos..pos + self.pattern.len(), &self.replacement);
101
102            // Recursively handle multiple occurrences
103            if result.to_lowercase().contains(&pattern_lower) {
104                return self.apply(&result);
105            }
106            result
107        } else {
108            input.to_owned()
109        }
110    }
111}
112
113/// Compose multiple transformations
114pub struct CompositeTransform {
115    transforms: Vec<Box<dyn PatternTransform + Send + Sync>>,
116}
117
118impl CompositeTransform {
119    #[must_use]
120    pub fn new() -> Self {
121        Self {
122            transforms: Vec::new(),
123        }
124    }
125
126    #[must_use]
127    pub fn add_transform<T: PatternTransform + Send + Sync + 'static>(
128        mut self,
129        transform: T,
130    ) -> Self {
131        self.transforms.push(Box::new(transform));
132        self
133    }
134}
135
136impl Default for CompositeTransform {
137    fn default() -> Self {
138        Self::new()
139    }
140}
141
142impl PatternTransform for CompositeTransform {
143    fn apply(&self, input: &str) -> String {
144        self.transforms
145            .iter()
146            .fold(input.to_owned(), |acc, transform| transform.apply(&acc))
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn test_simple_pattern() {
156        let pattern = SimplePattern::new("password", "[REDACTED]");
157        let result = pattern.apply("The password is secret123");
158        assert!(result.contains("[REDACTED]"));
159        assert!(!result.contains("password"));
160    }
161
162    #[test]
163    fn test_case_insensitive_pattern() {
164        let pattern = CaseInsensitivePattern::new("password", "[REDACTED]");
165        let result = pattern.apply("The PASSWORD is secret123");
166        assert!(result.contains("[REDACTED]"));
167    }
168
169    #[test]
170    fn test_composite_transform() {
171        let composite = CompositeTransform::new()
172            .add_transform(SimplePattern::new("password", "[REDACTED]"))
173            .add_transform(SimplePattern::new("token", "[REDACTED]"));
174
175        let result = composite.apply("password is secret, token is abc123");
176        assert!(result.contains("[REDACTED]"));
177        assert!(!result.contains("password"));
178        assert!(!result.contains("token"));
179    }
180
181    #[test]
182    fn test_simple_pattern_no_match() {
183        let pattern = SimplePattern::new("password", "[REDACTED]");
184        let result = pattern.apply("This text has no sensitive data");
185        assert_eq!(result, "This text has no sensitive data");
186    }
187
188    #[test]
189    fn test_simple_pattern_multiple_occurrences() {
190        let pattern = SimplePattern::new("key", "[REDACTED]");
191        let result = pattern.apply("key1, key2, key3");
192        assert_eq!(result.matches("[REDACTED]").count(), 3);
193    }
194
195    #[test]
196    fn test_case_insensitive_pattern_various_cases() {
197        let pattern = CaseInsensitivePattern::new("secret", "[REDACTED]");
198
199        let result1 = pattern.apply("SECRET value");
200        assert!(result1.contains("[REDACTED]"));
201
202        let result2 = pattern.apply("Secret value");
203        assert!(result2.contains("[REDACTED]"));
204
205        let result3 = pattern.apply("secret value");
206        assert!(result3.contains("[REDACTED]"));
207    }
208
209    #[test]
210    fn test_case_insensitive_pattern_no_match() {
211        let pattern = CaseInsensitivePattern::new("password", "[REDACTED]");
212        let result = pattern.apply("No sensitive data here");
213        assert_eq!(result, "No sensitive data here");
214    }
215
216    #[test]
217    fn test_case_insensitive_pattern_multiple_occurrences() {
218        let pattern = CaseInsensitivePattern::new("pass", "[REDACTED]");
219        let result = pattern.apply("pass PASS Pass");
220        // Should handle recursive replacements
221        assert!(result.contains("[REDACTED]"));
222    }
223
224    #[test]
225    fn test_composite_transform_empty() {
226        let composite = CompositeTransform::new();
227        let result = composite.apply("test string");
228        assert_eq!(result, "test string");
229    }
230
231    #[test]
232    fn test_composite_transform_default() {
233        let composite = CompositeTransform::default();
234        let result = composite.apply("test");
235        assert_eq!(result, "test");
236    }
237
238    #[test]
239    fn test_composite_transform_single() {
240        let composite = CompositeTransform::new().add_transform(SimplePattern::new("test", "TEST"));
241
242        let result = composite.apply("test string");
243        assert_eq!(result, "TEST string");
244    }
245
246    #[test]
247    fn test_composite_transform_chained() {
248        let composite = CompositeTransform::new()
249            .add_transform(SimplePattern::new("a", "b"))
250            .add_transform(SimplePattern::new("b", "c"))
251            .add_transform(SimplePattern::new("c", "d"));
252
253        let result = composite.apply("a");
254        assert_eq!(result, "d");
255    }
256
257    #[test]
258    fn test_pattern_transform_trait() {
259        let pattern: Box<dyn PatternTransform> = Box::new(SimplePattern::new("test", "replaced"));
260        let result = pattern.apply("test value");
261        assert_eq!(result, "replaced value");
262    }
263}