1use std::borrow::Cow;
7
8use regex::Regex;
9
10pub struct RedactionRule<'a> {
22 pub pattern: &'a str,
24 pub limit: usize,
26 pub replacement: &'a str,
28}
29
30impl<'a> RedactionRule<'a> {
31 pub fn new(pattern: &'a str, replacement: &'a str, limit: usize) -> Self {
43 Self {
44 pattern,
45 limit,
46 replacement,
47 }
48 }
49}
50
51#[track_caller]
61pub(crate) fn apply_redactions<'a>(
62 data: &'a str,
63 redaction_rules: &[RedactionRule],
64) -> Cow<'a, str> {
65 let mut result = Cow::from(data);
66 for RedactionRule {
67 pattern,
68 limit,
69 replacement,
70 } in redaction_rules
71 {
72 let regex = Regex::new(pattern).expect("regex parse error in redaction rule");
73 let redacted = regex.replacen(&result, *limit, *replacement);
74 if let Cow::Owned(modified) = redacted {
75 result = Cow::from(modified);
76 }
77 }
78 result
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84
85 #[test]
86 fn test_apply_redactions_empty_rules() {
87 let data = "hello world";
88 let rules = [];
89 let result = apply_redactions(data, &rules);
90 assert_eq!(result, "hello world");
91 assert!(matches!(result, Cow::Borrowed(_)));
92 }
93
94 #[test]
95 fn test_apply_redactions_no_match() {
96 let data = "hello world";
97 let rules = [RedactionRule {
98 pattern: "foo",
99 limit: 0,
100 replacement: "bar",
101 }];
102 let result = apply_redactions(data, &rules);
103 assert_eq!(result, "hello world");
104 assert!(matches!(result, Cow::Borrowed(_)));
105 }
106
107 #[test]
108 fn test_apply_redactions_single_replacement() {
109 let data = "hello world hello";
110 let rules = [RedactionRule {
111 pattern: "hello",
112 limit: 1,
113 replacement: "hi",
114 }];
115 let result = apply_redactions(data, &rules);
116 assert_eq!(result, "hi world hello");
117 assert!(matches!(result, Cow::Owned(_)));
118 }
119
120 #[test]
121 fn test_apply_redactions_limit_zero_replaces_all() {
122 let data = "hello world hello";
123 let rules = [RedactionRule {
124 pattern: "hello",
125 limit: 0,
126 replacement: "hi",
127 }];
128 let result = apply_redactions(data, &rules);
129 assert_eq!(result, "hi world hi");
130 }
131
132 #[test]
133 fn test_apply_redactions_multiple_rules() {
134 let data = "hello world";
135 let rules = [
136 RedactionRule {
137 pattern: "hello",
138 limit: 1,
139 replacement: "hi",
140 },
141 RedactionRule {
142 pattern: "world",
143 limit: 1,
144 replacement: "earth",
145 },
146 ];
147 let result = apply_redactions(data, &rules);
148 assert_eq!(result, "hi earth");
149 }
150
151 #[test]
152 fn test_apply_redactions_capture_groups() {
153 let data = "hello world";
154 let rules = [RedactionRule {
155 pattern: r"hello (\w+)",
156 limit: 0,
157 replacement: "hi $1",
158 }];
159 let result = apply_redactions(data, &rules);
160 assert_eq!(result, "hi world");
161 }
162
163 #[test]
164 #[should_panic(expected = "regex parse error")]
165 fn test_apply_redactions_invalid_regex() {
166 let data = "hello world";
167 let rules = [RedactionRule {
168 pattern: "(invalid",
169 limit: 0,
170 replacement: "hi",
171 }];
172 let _result = apply_redactions(data, &rules);
173 }
174}