1use std::cmp::Ordering;
2use std::collections::BTreeSet;
3
4use crate::builtin::BUILTIN_RULES_MAP;
5use crate::{PiiConfig, PiiConfigError, Redaction, RuleSpec, RuleType, SelectorSpec};
6
7const MAX_DEPTH: usize = 200;
9
10#[derive(Debug, Clone)]
15pub struct CompiledPiiConfig {
16 pub(super) applications: Vec<(SelectorSpec, BTreeSet<RuleRef>)>,
17}
18
19impl CompiledPiiConfig {
20 pub fn new(config: &PiiConfig) -> Self {
22 let mut applications = Vec::new();
23 for (selector, rules) in &config.applications {
24 #[allow(clippy::mutable_key_type)]
25 let mut rule_set = BTreeSet::default();
26 let mut seen_ids = BTreeSet::default();
27 for rule_id in rules {
28 collect_rules(config, &mut rule_set, &mut seen_ids, rule_id, None, 0);
29 }
30 applications.push((selector.clone(), rule_set));
31 }
32
33 CompiledPiiConfig { applications }
34 }
35
36 pub fn force_compile(&self) -> Result<(), PiiConfigError> {
40 for rule in self.applications.iter().flat_map(|(_, rules)| rules.iter()) {
41 match &rule.ty {
42 RuleType::Pattern(rule) => {
43 rule.pattern.compiled().map_err(|e| e.clone())?;
44 }
45 RuleType::RedactPair(rule) => {
46 rule.key_pattern.compiled().map_err(|e| e.clone())?;
47 }
48 RuleType::Anything
49 | RuleType::Imei
50 | RuleType::Mac
51 | RuleType::Uuid
52 | RuleType::Email
53 | RuleType::Ip
54 | RuleType::Creditcard
55 | RuleType::Iban
56 | RuleType::Userpath
57 | RuleType::Pemkey
58 | RuleType::UrlAuth
59 | RuleType::UsSsn
60 | RuleType::Bearer
61 | RuleType::Password
62 | RuleType::Multiple(_)
63 | RuleType::Alias(_)
64 | RuleType::Unknown(_) => {}
65 }
66 }
67 Ok(())
68 }
69}
70
71fn get_rule(config: &PiiConfig, id: &str) -> Option<RuleRef> {
72 if let Some(spec) = config.rules.get(id) {
73 Some(RuleRef::new(id.to_owned(), spec))
74 } else {
75 BUILTIN_RULES_MAP
76 .get(id)
77 .map(|spec| RuleRef::new(id.to_owned(), spec))
78 }
79}
80
81#[allow(clippy::mutable_key_type)]
82fn collect_rules(
83 config: &PiiConfig,
84 rules: &mut BTreeSet<RuleRef>,
85 seen_ids: &mut BTreeSet<Box<str>>,
86 rule_id: &str,
87 parent: Option<RuleRef>,
88 depth: usize,
89) {
90 if depth > MAX_DEPTH {
91 return;
92 }
93
94 let rule = match get_rule(config, rule_id) {
95 Some(rule) => rule,
96 None => return,
97 };
98
99 if !seen_ids.insert(rule_id.into()) {
100 return;
101 }
102
103 let rule = match parent {
104 Some(parent) => rule.for_parent(parent),
105 None => rule,
106 };
107
108 match rule.ty {
109 RuleType::Multiple(ref m) => {
110 let parent = if m.hide_inner {
111 Some(rule.clone())
112 } else {
113 None
114 };
115 for rule_id in &m.rules {
116 collect_rules(config, rules, seen_ids, rule_id, parent.clone(), depth + 1);
117 }
118 }
119 RuleType::Alias(ref a) => {
120 let parent = if a.hide_inner {
121 Some(rule.clone())
122 } else {
123 None
124 };
125 collect_rules(config, rules, seen_ids, &a.rule, parent, depth + 1);
126 }
127 RuleType::Unknown(_) => {}
128 _ => {
129 rules.insert(rule);
130 }
131 }
132}
133
134#[derive(Debug, Clone)]
136pub(super) struct RuleRef {
137 pub id: String,
138 pub origin: String,
139 pub ty: RuleType,
140 pub redaction: Redaction,
141}
142
143impl RuleRef {
144 fn new(id: String, spec: &RuleSpec) -> Self {
145 RuleRef {
146 origin: id.clone(),
147 id,
148 ty: spec.ty.clone(),
149 redaction: spec.redaction.clone(),
150 }
151 }
152
153 pub fn for_parent(self, parent: Self) -> Self {
154 RuleRef {
155 id: self.id,
156 origin: parent.origin,
157 ty: self.ty,
158 redaction: match parent.redaction {
159 Redaction::Default => self.redaction,
160 _ => parent.redaction,
161 },
162 }
163 }
164}
165
166impl PartialEq for RuleRef {
167 fn eq(&self, other: &Self) -> bool {
168 self.id == other.id
169 }
170}
171
172impl Eq for RuleRef {}
173
174impl PartialOrd for RuleRef {
175 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
176 Some(self.cmp(other))
177 }
178}
179
180impl Ord for RuleRef {
181 fn cmp(&self, other: &Self) -> Ordering {
182 self.id.cmp(&other.id)
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use std::collections::BTreeMap;
189
190 use crate::AliasRule;
191
192 use super::*;
193
194 #[test]
195 fn cycle_singleton() {
196 let config = PiiConfig {
198 rules: BTreeMap::from([(
199 "a".to_owned(),
200 RuleSpec {
201 ty: RuleType::Alias(AliasRule {
202 rule: "a".to_owned(),
203 hide_inner: false,
204 }),
205 redaction: Redaction::Default,
206 },
207 )]),
208 ..Default::default()
209 };
210 #[allow(clippy::mutable_key_type)]
211 let mut collected_rules = Default::default();
212 let mut seen_ids = Default::default();
213 collect_rules(&config, &mut collected_rules, &mut seen_ids, "a", None, 0);
214
215 assert!(collected_rules.is_empty());
217 }
218
219 #[test]
220 fn cycle_pair() {
221 let config = PiiConfig {
223 rules: BTreeMap::from([
224 (
225 "a".to_owned(),
226 RuleSpec {
227 ty: RuleType::Alias(AliasRule {
228 rule: "b".to_owned(),
229 hide_inner: false,
230 }),
231 redaction: Redaction::Default,
232 },
233 ),
234 (
235 "b".to_owned(),
236 RuleSpec {
237 ty: RuleType::Alias(AliasRule {
238 rule: "a".to_owned(),
239 hide_inner: false,
240 }),
241 redaction: Redaction::Default,
242 },
243 ),
244 ]),
245 ..Default::default()
246 };
247 #[allow(clippy::mutable_key_type)]
248 let mut collected_rules = Default::default();
249 let mut seen_ids = Default::default();
250 collect_rules(&config, &mut collected_rules, &mut seen_ids, "a", None, 0);
251
252 assert!(collected_rules.is_empty());
254 }
255
256 #[test]
257 fn only_one_shared_rule_survives() {
258 let config = PiiConfig {
262 rules: BTreeMap::from([
263 (
264 "a".to_owned(),
265 RuleSpec {
266 ty: RuleType::Alias(AliasRule {
267 rule: "c".to_owned(),
268 hide_inner: true,
269 }),
270 redaction: Redaction::Default,
271 },
272 ),
273 (
274 "b".to_owned(),
275 RuleSpec {
276 ty: RuleType::Alias(AliasRule {
277 rule: "c".to_owned(),
278 hide_inner: true,
279 }),
280 redaction: Redaction::Default,
281 },
282 ),
283 (
284 "c".to_owned(),
285 RuleSpec {
286 ty: RuleType::Anything,
287 redaction: Redaction::Default,
288 },
289 ),
290 ]),
291 ..Default::default()
292 };
293 #[allow(clippy::mutable_key_type)]
294 let mut collected_rules = BTreeSet::new();
295 let mut seen_ids = BTreeSet::new();
296 collect_rules(&config, &mut collected_rules, &mut seen_ids, "a", None, 0);
297 collect_rules(&config, &mut collected_rules, &mut seen_ids, "b", None, 0);
298
299 let collected_rules: Vec<_> = collected_rules
300 .into_iter()
301 .map(|rr| (rr.origin, rr.id))
302 .collect();
303
304 insta::assert_debug_snapshot!(collected_rules, @r#"
305 [
306 (
307 "a",
308 "c",
309 ),
310 ]
311 "#);
312 }
313
314 #[test]
315 fn double_origin() {
316 let config = PiiConfig {
318 rules: BTreeMap::from([
319 (
320 "a".to_owned(),
321 RuleSpec {
322 ty: RuleType::Alias(AliasRule {
323 rule: "b".to_owned(),
324 hide_inner: true,
325 }),
326 redaction: Redaction::Default,
327 },
328 ),
329 (
330 "b".to_owned(),
331 RuleSpec {
332 ty: RuleType::Alias(AliasRule {
333 rule: "c".to_owned(),
334 hide_inner: true,
335 }),
336 redaction: Redaction::Default,
337 },
338 ),
339 (
340 "c".to_owned(),
341 RuleSpec {
342 ty: RuleType::Anything,
343 redaction: Redaction::Default,
344 },
345 ),
346 ]),
347 ..Default::default()
348 };
349 #[allow(clippy::mutable_key_type)]
350 let mut collected_rules = Default::default();
351 let mut seen_ids = Default::default();
352 collect_rules(&config, &mut collected_rules, &mut seen_ids, "a", None, 0);
353
354 let collected_rules: Vec<_> = collected_rules
355 .into_iter()
356 .map(|rr| (rr.origin, rr.id))
357 .collect();
358
359 insta::assert_debug_snapshot!(collected_rules, @r#"
360 [
361 (
362 "a",
363 "c",
364 ),
365 ]
366 "#);
367 }
368
369 #[test]
370 fn depth_bound() {
371 let rule_fn = |i: usize| {
373 (
374 i.to_string(),
375 RuleSpec {
376 ty: RuleType::Alias(AliasRule {
377 rule: (i + 1).to_string(),
378 hide_inner: false,
379 }),
380 redaction: Redaction::Default,
381 },
382 )
383 };
384 let config = PiiConfig {
385 rules: BTreeMap::from_iter((0..1000).map(rule_fn)),
386 ..Default::default()
387 };
388 #[allow(clippy::mutable_key_type)]
389 let mut collected_rules = Default::default();
390 let mut seen_ids = Default::default();
391 collect_rules(&config, &mut collected_rules, &mut seen_ids, "0", None, 0);
392
393 assert!(collected_rules.is_empty()); }
395}