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