Skip to main content

assert_snap/
redaction.rs

1use std::borrow::Cow;
2
3use regex::Regex;
4
5/// A rule specifying how to redact data matching a specific pattern.
6pub struct RedactionRule<'a> {
7    /// The regular expression pattern to match.
8    pub(crate) pattern: &'a str,
9    /// The maximum number of replacements to make. If `0`, all occurrences are replaced.
10    pub(crate) limit: usize,
11    /// The replacement template (supports capture groups like `$1`).
12    pub(crate) replacement: &'a str,
13}
14
15impl<'a> RedactionRule<'a> {
16    /// Creates a new redaction rule.
17    ///
18    /// # Arguments
19    ///
20    /// * `pattern` - The regular expression pattern to match against the input data.
21    /// * `replacement` - The replacement template. Supports capture group references like `$1`, `$2`, etc.
22    /// * `limit` - The maximum number of replacements to make. If `0`, all occurrences are replaced.
23    ///
24    /// # Returns
25    ///
26    /// A new `RedactionRule` instance.
27    pub fn new(pattern: &'a str, replacement: &'a str, limit: usize) -> Self {
28        Self {
29            pattern,
30            limit,
31            replacement,
32        }
33    }
34}
35/// Applies a series of redaction rules to the input string sequentially.
36///
37/// Each rule defines a regular expression pattern, a replacement template (which supports
38/// capture groups like `$1`), and a limit on the number of matches to replace. If the limit
39/// is `0`, all occurrences matching the pattern are replaced.
40///
41/// # Panics
42///
43/// Panics if any of the regular expression patterns in the rules are invalid.
44#[track_caller]
45pub(crate) fn apply_redactions<'a>(
46    data: &'a str,
47    redaction_rules: &[RedactionRule],
48) -> Cow<'a, str> {
49    let mut result = Cow::from(data);
50    for RedactionRule {
51        pattern,
52        limit,
53        replacement,
54    } in redaction_rules
55    {
56        let regex = Regex::new(pattern).unwrap();
57        let redacted = regex.replacen(&result, *limit, *replacement);
58        if let Cow::Owned(modified) = redacted {
59            result = Cow::from(modified);
60        }
61    }
62    result
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    #[test]
70    fn test_apply_redactions_empty_rules() {
71        let data = "hello world";
72        let rules = [];
73        let result = apply_redactions(data, &rules);
74        assert_eq!(result, "hello world");
75        assert!(matches!(result, Cow::Borrowed(_)));
76    }
77
78    #[test]
79    fn test_apply_redactions_no_match() {
80        let data = "hello world";
81        let rules = [RedactionRule {
82            pattern: "foo",
83            limit: 0,
84            replacement: "bar",
85        }];
86        let result = apply_redactions(data, &rules);
87        assert_eq!(result, "hello world");
88        assert!(matches!(result, Cow::Borrowed(_)));
89    }
90
91    #[test]
92    fn test_apply_redactions_single_replacement() {
93        let data = "hello world hello";
94        let rules = [RedactionRule {
95            pattern: "hello",
96            limit: 1,
97            replacement: "hi",
98        }];
99        let result = apply_redactions(data, &rules);
100        assert_eq!(result, "hi world hello");
101        assert!(matches!(result, Cow::Owned(_)));
102    }
103
104    #[test]
105    fn test_apply_redactions_limit_zero_replaces_all() {
106        let data = "hello world hello";
107        let rules = [RedactionRule {
108            pattern: "hello",
109            limit: 0,
110            replacement: "hi",
111        }];
112        let result = apply_redactions(data, &rules);
113        assert_eq!(result, "hi world hi");
114    }
115
116    #[test]
117    fn test_apply_redactions_multiple_rules() {
118        let data = "hello world";
119        let rules = [
120            RedactionRule {
121                pattern: "hello",
122                limit: 1,
123                replacement: "hi",
124            },
125            RedactionRule {
126                pattern: "world",
127                limit: 1,
128                replacement: "earth",
129            },
130        ];
131        let result = apply_redactions(data, &rules);
132        assert_eq!(result, "hi earth");
133    }
134
135    #[test]
136    fn test_apply_redactions_capture_groups() {
137        let data = "hello world";
138        let rules = [RedactionRule {
139            pattern: r"hello (\w+)",
140            limit: 0,
141            replacement: "hi $1",
142        }];
143        let result = apply_redactions(data, &rules);
144        assert_eq!(result, "hi world");
145    }
146
147    #[test]
148    #[should_panic(expected = "regex parse error")]
149    fn test_apply_redactions_invalid_regex() {
150        let data = "hello world";
151        let rules = [RedactionRule {
152            pattern: "(invalid",
153            limit: 0,
154            replacement: "hi",
155        }];
156        let _result = apply_redactions(data, &rules);
157    }
158}