dd_sds/scoped_ruleset/
mod.rs

1mod bool_set;
2
3use crate::event::{EventVisitor, VisitStringResult};
4use crate::scanner::scope::Scope;
5use crate::scoped_ruleset::bool_set::BoolSet;
6use crate::{Event, Path, PathSegment, ScannerError};
7use ahash::AHashMap;
8
9/// A `ScopedRuleSet` determines which rules will be used to scan each field of an event, and which
10/// paths are considered `excluded`.
11#[derive(Debug)]
12pub struct ScopedRuleSet {
13    tree: RuleTree,
14    // The number of rules stored in this set
15    num_rules: usize,
16    add_implicit_index_wildcards: bool,
17}
18
19impl ScopedRuleSet {
20    /// The scopes for each rule. The indices of the scopes MUST match the rule index.
21    pub fn new(rules_scopes: &[Scope]) -> Self {
22        let mut tree = RuleTree::new();
23
24        for (rule_index, scope) in rules_scopes.iter().enumerate() {
25            match scope {
26                Scope::Include { include, exclude } => {
27                    for path in exclude {
28                        tree.insert_rule_removal(path, rule_index);
29                    }
30
31                    for path in include {
32                        tree.insert_rule_add(path, rule_index);
33                    }
34                }
35                Scope::Exclude(paths) => {
36                    tree.insert_rule_add(&Path::root(), rule_index);
37                    for path in paths {
38                        tree.insert_rule_removal(path, rule_index);
39                    }
40                }
41            }
42        }
43        Self {
44            tree,
45            num_rules: rules_scopes.len(),
46            add_implicit_index_wildcards: false,
47        }
48    }
49
50    pub fn with_implicit_index_wildcards(mut self, value: bool) -> Self {
51        self.add_implicit_index_wildcards = value;
52        self
53    }
54
55    pub fn visit_string_rule_combinations<'path, 'c: 'path>(
56        &'c self,
57        event: &'path mut impl Event,
58        content_visitor: impl ContentVisitor<'path>,
59    ) -> Result<(), ScannerError> {
60        let bool_set = if self.add_implicit_index_wildcards {
61            Some(BoolSet::new(self.num_rules))
62        } else {
63            None
64        };
65        let mut visitor = ScopedRuledSetEventVisitor {
66            content_visitor,
67            tree_nodes: vec![ActiveRuleTree {
68                rule_tree: &self.tree,
69                index_wildcard_match: false,
70            }],
71            active_node_counter: vec![NodeCounter {
72                active_tree_count: 1,
73            }],
74            path: Path::root(),
75            bool_set,
76            add_implicit_index_wildcards: self.add_implicit_index_wildcards,
77            // should_keywords_match_event_paths: self.should_keywords_match_event_paths,
78        };
79
80        event.visit_event(&mut visitor)
81    }
82}
83
84pub struct ExclusionCheck<'a> {
85    tree_nodes: &'a [ActiveRuleTree<'a>],
86}
87
88impl ExclusionCheck<'_> {
89    pub fn is_excluded(&self, rule_index: usize) -> bool {
90        for include_node in self.tree_nodes {
91            for change in &include_node.rule_tree.rule_changes {
92                match change {
93                    RuleChange::Add(_) => { /* ignore */ }
94                    RuleChange::Remove(i) => {
95                        if *i == rule_index {
96                            return true;
97                        }
98                    }
99                }
100            }
101        }
102        false
103    }
104}
105
106pub trait ContentVisitor<'path> {
107    fn visit_content<'content_visitor>(
108        &'content_visitor mut self,
109        path: &Path<'path>,
110        content: &str,
111        rules: RuleIndexVisitor,
112        is_excluded: ExclusionCheck<'content_visitor>,
113    ) -> Result<bool, ScannerError>;
114}
115
116// This is just a reference to a RuleTree with some additional information
117struct ActiveRuleTree<'a> {
118    rule_tree: &'a RuleTree,
119    // If this tree was pushed because of a wildcard index, indices aren't
120    // allowed to match immediately after, so they are ignored.
121    index_wildcard_match: bool,
122}
123
124struct NodeCounter {
125    // This counts how many trees are currently active, which can
126    // happen due to (implicit) wildcard segments. The last value in this
127    // list is the current number of active trees (n), which is the last
128    // n trees in `tree_nodes`.
129    active_tree_count: usize,
130}
131
132struct ScopedRuledSetEventVisitor<'a, C> {
133    // The struct that will receive content / rules.
134    content_visitor: C,
135
136    // This is a list of parent tree nodes, which is a list of all of the "active" rule changes.
137    // If an "Add" exists for a rule in this list, it will be scanned. If a single "Remove" exists, it will cause the `ExclusionCheck` to return true.
138    tree_nodes: Vec<ActiveRuleTree<'a>>,
139
140    // This is a counter that helps keep track of how many elements we have pushed
141    // In the tree_nodes list and in the true_positive_rule_idx list
142    active_node_counter: Vec<NodeCounter>,
143
144    // The current path being visited
145    path: Path<'a>,
146
147    // A re-usable boolean set to de-duplicate rules
148    bool_set: Option<BoolSet>,
149
150    add_implicit_index_wildcards: bool,
151}
152
153impl<'path, C> EventVisitor<'path> for ScopedRuledSetEventVisitor<'path, C>
154where
155    C: ContentVisitor<'path>,
156{
157    fn push_segment(&mut self, segment: PathSegment<'path>) {
158        // update the tree
159        // The current path may go beyond what is stored in the "include" tree, so the tree is only updated if they are at the same height.
160
161        let num_active_trees = self.active_node_counter.last().unwrap().active_tree_count;
162        let tree_nodes_len = self.tree_nodes.len();
163        let active_trees_range = tree_nodes_len - num_active_trees..tree_nodes_len;
164
165        for tree_index in active_trees_range {
166            if !self.tree_nodes[tree_index].index_wildcard_match || !segment.is_index() {
167                if let Some(child) = self.tree_nodes[tree_index].rule_tree.children.get(&segment) {
168                    self.tree_nodes.push(ActiveRuleTree {
169                        rule_tree: child,
170                        index_wildcard_match: false,
171                    });
172                }
173            }
174
175            if self.add_implicit_index_wildcards && segment.is_index() {
176                // Optionally skip the index (it acts as a wildcard) by
177                // pushing the same tree back onto the stack.
178                self.tree_nodes.push(ActiveRuleTree {
179                    rule_tree: self.tree_nodes[tree_index].rule_tree,
180                    index_wildcard_match: true,
181                });
182            }
183        }
184
185        // The new number of active trees is the number of new trees pushed
186        self.active_node_counter.push(NodeCounter {
187            active_tree_count: self.tree_nodes.len() - tree_nodes_len,
188        });
189
190        self.path.segments.push(segment);
191    }
192
193    fn pop_segment(&mut self) {
194        let node_counter = self.active_node_counter.pop().unwrap();
195        for _ in 0..node_counter.active_tree_count {
196            // The rules from the last node are no longer active, so remove them.
197            let _popped = self.tree_nodes.pop();
198        }
199        self.path.segments.pop();
200    }
201
202    fn visit_string<'s>(
203        &'s mut self,
204        value: &str,
205    ) -> Result<VisitStringResult<'s, 'path>, ScannerError> {
206        let will_mutate = self.content_visitor.visit_content(
207            &self.path,
208            value,
209            RuleIndexVisitor {
210                tree_nodes: &self.tree_nodes,
211                used_rule_set: self.bool_set.as_mut(),
212            },
213            ExclusionCheck {
214                tree_nodes: &self.tree_nodes,
215            },
216        );
217        if let Some(bool_set) = &mut self.bool_set {
218            bool_set.reset();
219        }
220        will_mutate.map(|will_mutate| VisitStringResult {
221            might_mutate: will_mutate,
222            path: &self.path,
223        })
224    }
225}
226
227pub struct RuleIndexVisitor<'a> {
228    tree_nodes: &'a Vec<ActiveRuleTree<'a>>,
229    used_rule_set: Option<&'a mut BoolSet>,
230}
231
232impl RuleIndexVisitor<'_> {
233    /// Visits all rules associated with the current string. This may
234    /// potentially return no rule indices at all.
235    pub fn visit_rule_indices(
236        &mut self,
237        mut visit: impl FnMut(usize) -> Result<(), ScannerError>,
238    ) -> Result<(), ScannerError> {
239        // visit rules with an `Include` scope
240        for include_node in self.tree_nodes {
241            if include_node.index_wildcard_match {
242                // This is guaranteed to be a duplicated node. Skip it
243                continue;
244            }
245            for change in &include_node.rule_tree.rule_changes {
246                match change {
247                    RuleChange::Add(rule_index) => {
248                        if let Some(used_rule_set) = &mut self.used_rule_set {
249                            if !used_rule_set.get_and_set(*rule_index) {
250                                (visit)(*rule_index)?;
251                            }
252                        } else {
253                            (visit)(*rule_index)?;
254                        }
255                    }
256                    RuleChange::Remove(_) => { /* Nothing to do here */ }
257                }
258            }
259        }
260        Ok(())
261    }
262}
263
264#[derive(Clone, Copy, PartialEq, Debug)]
265enum RuleChange {
266    Add(usize),
267    Remove(usize),
268}
269
270#[derive(Clone, Debug)]
271struct RuleTree {
272    rule_changes: Vec<RuleChange>,
273    children: AHashMap<PathSegment<'static>, RuleTree>,
274}
275
276impl RuleTree {
277    pub fn new() -> Self {
278        Self {
279            rule_changes: vec![],
280            children: AHashMap::new(),
281        }
282    }
283
284    pub fn insert_rule_add(&mut self, path: &Path<'static>, rule_index: usize) {
285        self.insert_rule_inner(&path.segments, RuleChange::Add(rule_index));
286    }
287
288    pub fn insert_rule_removal(&mut self, path: &Path<'static>, rule_index: usize) {
289        self.insert_rule_inner(&path.segments, RuleChange::Remove(rule_index));
290    }
291
292    fn insert_rule_inner(&mut self, path: &[PathSegment<'static>], rule_change: RuleChange) {
293        if self.rule_changes.contains(&rule_change) {
294            return;
295        }
296        if let Some((first, remaining)) = path.split_first() {
297            let child_tree = self
298                .children
299                .entry(first.clone())
300                .or_insert_with(RuleTree::new);
301            child_tree.insert_rule_inner(remaining, rule_change);
302        } else {
303            // remove any recursive children, since they will be duplicated
304            self.recursively_remove(rule_change);
305            self.rule_changes.push(rule_change);
306        }
307    }
308
309    // Remove the given change from all children (recursive). This is used
310    // to remove duplicates.
311    fn recursively_remove(&mut self, rule_change: RuleChange) {
312        self.rule_changes.retain(|x| *x != rule_change);
313        for child_tree in self.children.values_mut() {
314            child_tree.recursively_remove(rule_change);
315        }
316    }
317}
318
319#[cfg(test)]
320mod test {
321    use crate::simple_event::SimpleEvent;
322
323    use super::*;
324
325    #[derive(Debug, PartialEq, Eq, Clone, PartialOrd, Ord)]
326    struct VisitedPath {
327        path: Path<'static>,
328        content: String,
329        rules: Vec<VisitedRule>,
330    }
331
332    #[derive(Debug, PartialEq, Eq, Clone, PartialOrd, Ord)]
333    struct VisitedRule {
334        rule_index: usize,
335        is_excluded: bool,
336    }
337
338    #[derive(Debug, PartialEq, Eq)]
339    struct Visited {
340        paths: Vec<VisitedPath>,
341    }
342
343    // Visits the event and returns the paths that were visited
344    fn visit_event(event: &mut impl Event, ruleset: &ScopedRuleSet) -> Visited {
345        let mut visited = Visited { paths: vec![] };
346
347        struct RecordingContentVisitor<'a> {
348            visited: &'a mut Visited,
349        }
350
351        impl<'a> ContentVisitor<'a> for RecordingContentVisitor<'a> {
352            fn visit_content<'content_visitor>(
353                &'content_visitor mut self,
354                path: &Path<'a>,
355                content: &str,
356                mut rule_iter: RuleIndexVisitor,
357                exclusion_check: ExclusionCheck<'content_visitor>,
358            ) -> Result<bool, ScannerError> {
359                let mut rules = vec![];
360                rule_iter.visit_rule_indices(|rule_index| {
361                    rules.push(VisitedRule {
362                        rule_index,
363                        is_excluded: exclusion_check.is_excluded(rule_index),
364                    });
365                    Ok(())
366                })?;
367                rules.sort();
368                self.visited.paths.push(VisitedPath {
369                    path: path.into_static(),
370                    content: content.to_string(),
371                    rules,
372                });
373                Ok(true)
374            }
375        }
376
377        ruleset
378            .visit_string_rule_combinations(
379                event,
380                RecordingContentVisitor {
381                    visited: &mut visited,
382                },
383            )
384            .unwrap();
385        visited.paths.sort();
386        visited
387    }
388
389    #[test]
390    fn test_inclusive_scopes() {
391        // Fields are scanned as long as they are a child of any `include` path
392        let ruleset = ScopedRuleSet::new(&[
393            Scope::include(vec![Path::from(vec!["a".into()])]),
394            Scope::include(vec![Path::from(vec!["a".into(), "b".into()])]),
395            Scope::include(vec![]),
396        ]);
397
398        let mut event = SimpleEvent::Map(
399            [
400                (
401                    "a".into(),
402                    SimpleEvent::Map(
403                        [
404                            ("b".into(), SimpleEvent::String("value-ab".into())),
405                            ("c".into(), SimpleEvent::String("value-ac".into())),
406                        ]
407                        .into(),
408                    ),
409                ),
410                ("d".into(), SimpleEvent::String("value-d".into())),
411            ]
412            .into(),
413        );
414
415        let paths = visit_event(&mut event, &ruleset);
416
417        assert_eq!(
418            paths,
419            Visited {
420                paths: vec![
421                    VisitedPath {
422                        path: Path::from(vec!["a".into(), "b".into()]),
423                        content: "value-ab".into(),
424                        rules: vec![
425                            VisitedRule {
426                                rule_index: 0,
427                                is_excluded: false
428                            },
429                            VisitedRule {
430                                rule_index: 1,
431                                is_excluded: false
432                            }
433                        ]
434                    },
435                    VisitedPath {
436                        path: Path::from(vec!["a".into(), "c".into()]),
437                        content: "value-ac".into(),
438                        rules: vec![VisitedRule {
439                            rule_index: 0,
440                            is_excluded: false
441                        }]
442                    },
443                    VisitedPath {
444                        path: Path::from(vec!["d".into()]),
445                        content: "value-d".into(),
446                        rules: vec![]
447                    }
448                ],
449            }
450        );
451    }
452
453    #[test]
454    fn test_inclusive_scopes_array() {
455        // Fields are scanned as long as they are a child of any `include` path
456        let ruleset = ScopedRuleSet::new(&[
457            Scope::include(vec![Path::from(vec![0.into()])]),
458            Scope::include(vec![Path::from(vec![1.into(), 0.into()])]),
459            Scope::include(vec![Path::from(vec![2.into(), 0.into()])]),
460        ]);
461
462        let mut event = SimpleEvent::List(vec![
463            SimpleEvent::String("value-0".into()),
464            SimpleEvent::String("value-1".into()),
465            SimpleEvent::List(vec![SimpleEvent::String("value-2-0".into())]),
466            SimpleEvent::String("value-3".into()),
467        ]);
468
469        let paths = visit_event(&mut event, &ruleset);
470
471        assert_eq!(
472            paths,
473            Visited {
474                paths: vec![
475                    VisitedPath {
476                        path: Path::from(vec![0.into()]),
477                        content: "value-0".into(),
478                        rules: vec![VisitedRule {
479                            rule_index: 0,
480                            is_excluded: false
481                        }]
482                    },
483                    VisitedPath {
484                        path: Path::from(vec![1.into()]),
485                        content: "value-1".into(),
486                        rules: vec![]
487                    },
488                    VisitedPath {
489                        path: Path::from(vec![2.into(), 0.into()]),
490                        content: "value-2-0".into(),
491                        rules: vec![VisitedRule {
492                            rule_index: 2,
493                            is_excluded: false
494                        }]
495                    },
496                    VisitedPath {
497                        path: Path::from(vec![3.into()]),
498                        content: "value-3".into(),
499                        rules: vec![]
500                    }
501                ],
502            }
503        );
504    }
505
506    #[test]
507    fn test_exclusive_scopes() {
508        // All fields are scanned, but fields that are children of any `exclude` path are marked as excluded
509        let ruleset = ScopedRuleSet::new(&[
510            Scope::exclude(vec![Path::from(vec!["a".into()])]),
511            Scope::exclude(vec![Path::from(vec!["a".into(), "b".into()])]),
512            // matches everything (it will always be excluded)
513            Scope::all(),
514        ]);
515
516        let mut event = SimpleEvent::Map(
517            [
518                (
519                    "a".into(),
520                    SimpleEvent::Map(
521                        [
522                            ("b".into(), SimpleEvent::String("value-ab".into())),
523                            ("c".into(), SimpleEvent::String("value-ac".into())),
524                        ]
525                        .into(),
526                    ),
527                ),
528                ("d".into(), SimpleEvent::String("value-d".into())),
529                ("e".into(), SimpleEvent::List(vec![])),
530            ]
531            .into(),
532        );
533
534        let paths = visit_event(&mut event, &ruleset);
535
536        assert_eq!(
537            paths,
538            Visited {
539                paths: vec![
540                    VisitedPath {
541                        path: Path::from(vec!["a".into(), "b".into()]),
542                        content: "value-ab".into(),
543                        rules: vec![
544                            VisitedRule {
545                                rule_index: 0,
546                                is_excluded: true
547                            },
548                            VisitedRule {
549                                rule_index: 1,
550                                is_excluded: true
551                            },
552                            VisitedRule {
553                                rule_index: 2,
554                                is_excluded: false
555                            }
556                        ]
557                    },
558                    VisitedPath {
559                        path: Path::from(vec!["a".into(), "c".into()]),
560                        content: "value-ac".into(),
561                        rules: vec![
562                            VisitedRule {
563                                rule_index: 0,
564                                is_excluded: true
565                            },
566                            VisitedRule {
567                                rule_index: 1,
568                                is_excluded: false
569                            },
570                            VisitedRule {
571                                rule_index: 2,
572                                is_excluded: false
573                            }
574                        ]
575                    },
576                    VisitedPath {
577                        path: Path::from(vec!["d".into()]),
578                        content: "value-d".into(),
579                        rules: vec![
580                            VisitedRule {
581                                rule_index: 0,
582                                is_excluded: false
583                            },
584                            VisitedRule {
585                                rule_index: 1,
586                                is_excluded: false
587                            },
588                            VisitedRule {
589                                rule_index: 2,
590                                is_excluded: false
591                            }
592                        ]
593                    }
594                ],
595            },
596        );
597    }
598
599    #[test]
600    fn test_exclusive_scopes_array() {
601        // All fields are scanned, but fields that are children of any `exclude` path are marked as excluded
602        let ruleset = ScopedRuleSet::new(&[
603            Scope::exclude(vec![Path::from(vec![0.into()])]),
604            Scope::exclude(vec![Path::from(vec![1.into(), 0.into()])]),
605            Scope::exclude(vec![Path::from(vec![2.into(), 0.into()])]),
606        ]);
607
608        let mut event = SimpleEvent::List(vec![
609            SimpleEvent::String("value-0".into()),
610            SimpleEvent::String("value-1".into()),
611            SimpleEvent::List(vec![SimpleEvent::String("value-2-0".into())]),
612            SimpleEvent::String("value-3".into()),
613        ]);
614
615        let paths = visit_event(&mut event, &ruleset);
616
617        assert_eq!(
618            paths,
619            Visited {
620                paths: vec![
621                    VisitedPath {
622                        path: Path::from(vec![0.into()]),
623                        content: "value-0".into(),
624                        rules: vec![
625                            VisitedRule {
626                                rule_index: 0,
627                                is_excluded: true
628                            },
629                            VisitedRule {
630                                rule_index: 1,
631                                is_excluded: false
632                            },
633                            VisitedRule {
634                                rule_index: 2,
635                                is_excluded: false
636                            }
637                        ]
638                    },
639                    VisitedPath {
640                        path: Path::from(vec![1.into()]),
641                        content: "value-1".into(),
642                        rules: vec![
643                            VisitedRule {
644                                rule_index: 0,
645                                is_excluded: false
646                            },
647                            VisitedRule {
648                                rule_index: 1,
649                                is_excluded: false
650                            },
651                            VisitedRule {
652                                rule_index: 2,
653                                is_excluded: false
654                            }
655                        ]
656                    },
657                    VisitedPath {
658                        path: Path::from(vec![2.into(), 0.into()]),
659                        content: "value-2-0".into(),
660                        rules: vec![
661                            VisitedRule {
662                                rule_index: 0,
663                                is_excluded: false
664                            },
665                            VisitedRule {
666                                rule_index: 1,
667                                is_excluded: false
668                            },
669                            VisitedRule {
670                                rule_index: 2,
671                                is_excluded: true
672                            }
673                        ]
674                    },
675                    VisitedPath {
676                        path: Path::from(vec![3.into()]),
677                        content: "value-3".into(),
678                        rules: vec![
679                            VisitedRule {
680                                rule_index: 0,
681                                is_excluded: false
682                            },
683                            VisitedRule {
684                                rule_index: 1,
685                                is_excluded: false
686                            },
687                            VisitedRule {
688                                rule_index: 2,
689                                is_excluded: false
690                            }
691                        ]
692                    }
693                ],
694            }
695        );
696    }
697
698    #[test]
699    fn test_include_and_exclude() {
700        // Fields that are children of any `include` path are scanned, but they are only marked as
701        // excluded if they are children of any `exclude` path
702        let ruleset = ScopedRuleSet::new(&[Scope::include_and_exclude(
703            vec![Path::from(vec![2.into()])],
704            vec![Path::from(vec![2.into(), 0.into()])],
705        )]);
706
707        let mut event = SimpleEvent::List(vec![
708            SimpleEvent::String("value-0".into()),
709            SimpleEvent::String("value-1".into()),
710            SimpleEvent::List(vec![
711                SimpleEvent::String("value-2-0".into()),
712                SimpleEvent::String("value-2-1".into()),
713            ]),
714            SimpleEvent::String("value-3".into()),
715        ]);
716
717        let paths = visit_event(&mut event, &ruleset);
718
719        assert_eq!(
720            paths,
721            Visited {
722                paths: vec![
723                    VisitedPath {
724                        path: Path::from(vec![0.into()]),
725                        content: "value-0".into(),
726                        rules: vec![]
727                    },
728                    VisitedPath {
729                        path: Path::from(vec![1.into()]),
730                        content: "value-1".into(),
731                        rules: vec![]
732                    },
733                    VisitedPath {
734                        path: Path::from(vec![2.into(), 0.into()]),
735                        content: "value-2-0".into(),
736                        rules: vec![VisitedRule {
737                            rule_index: 0,
738                            is_excluded: true
739                        }]
740                    },
741                    VisitedPath {
742                        path: Path::from(vec![2.into(), 1.into()]),
743                        content: "value-2-1".into(),
744                        rules: vec![VisitedRule {
745                            rule_index: 0,
746                            is_excluded: false
747                        }]
748                    },
749                    VisitedPath {
750                        path: Path::from(vec![3.into()]),
751                        content: "value-3".into(),
752                        rules: vec![]
753                    },
754                ]
755            }
756        );
757    }
758
759    #[test]
760    fn test_include_and_exclude_priority() {
761        // exclude paths have priority, and override any include path. (even if the include is more specific)
762
763        let ruleset = ScopedRuleSet::new(&[Scope::include_and_exclude(
764            // This include is ignored, since an exclude overrides it
765            vec![Path::from(vec![1.into(), 0.into()])],
766            vec![Path::from(vec![1.into()])],
767        )]);
768
769        let mut event = SimpleEvent::List(vec![
770            SimpleEvent::String("value-0".into()),
771            SimpleEvent::List(vec![
772                SimpleEvent::String("value-1-0".into()),
773                SimpleEvent::String("value-1-1".into()),
774            ]),
775        ]);
776
777        let paths = visit_event(&mut event, &ruleset);
778
779        assert_eq!(
780            paths,
781            Visited {
782                paths: vec![
783                    VisitedPath {
784                        path: Path::from(vec![0.into()]),
785                        content: "value-0".into(),
786                        rules: vec![]
787                    },
788                    VisitedPath {
789                        path: Path::from(vec![1.into(), 0.into()]),
790                        content: "value-1-0".into(),
791                        rules: vec![VisitedRule {
792                            rule_index: 0,
793                            is_excluded: true
794                        }]
795                    },
796                    VisitedPath {
797                        path: Path::from(vec![1.into(), 1.into()]),
798                        content: "value-1-1".into(),
799                        rules: vec![]
800                    }
801                ],
802            }
803        );
804    }
805
806    #[test]
807    fn test_include_same_change_multiple_times() {
808        // If multiple "include" scopes cover the same field, it should only be returned once.
809
810        let ruleset = ScopedRuleSet::new(&[Scope::include(vec![
811            Path::from(vec!["a".into()]),
812            Path::from(vec!["a".into(), "b".into()]),
813        ])]);
814
815        let mut event = SimpleEvent::Map(
816            [
817                (
818                    "a".into(),
819                    SimpleEvent::Map(
820                        [
821                            ("b".into(), SimpleEvent::String("value-ab".into())),
822                            ("c".into(), SimpleEvent::String("value-ac".into())),
823                        ]
824                        .into(),
825                    ),
826                ),
827                ("d".into(), SimpleEvent::String("value-d".into())),
828            ]
829            .into(),
830        );
831
832        let paths = visit_event(&mut event, &ruleset);
833
834        assert_eq!(
835            paths,
836            Visited {
837                paths: vec![
838                    VisitedPath {
839                        path: Path::from(vec!["a".into(), "b".into()]),
840                        content: "value-ab".into(),
841                        rules: vec![VisitedRule {
842                            rule_index: 0,
843                            is_excluded: false
844                        }]
845                    },
846                    VisitedPath {
847                        path: Path::from(vec!["a".into(), "c".into()]),
848                        content: "value-ac".into(),
849                        rules: vec![VisitedRule {
850                            rule_index: 0,
851                            is_excluded: false
852                        }]
853                    },
854                    VisitedPath {
855                        path: Path::from(vec!["d".into()]),
856                        content: "value-d".into(),
857                        rules: vec![]
858                    }
859                ],
860            }
861        );
862    }
863
864    #[test]
865    fn test_include_root_multiple_times() {
866        // This makes sure duplicate rule changes (at the root of the tree) are removed
867        let ruleset =
868            ScopedRuleSet::new(&[Scope::include(vec![Path::from(vec![]), Path::from(vec![])])]);
869
870        let mut event = SimpleEvent::Map(
871            [
872                (
873                    "a".into(),
874                    SimpleEvent::Map(
875                        [
876                            ("b".into(), SimpleEvent::String("value-ab".into())),
877                            ("c".into(), SimpleEvent::String("value-ac".into())),
878                        ]
879                        .into(),
880                    ),
881                ),
882                ("d".into(), SimpleEvent::String("value-d".into())),
883            ]
884            .into(),
885        );
886
887        let paths = visit_event(&mut event, &ruleset);
888
889        assert_eq!(
890            paths,
891            Visited {
892                paths: vec![
893                    VisitedPath {
894                        path: Path::from(vec!["a".into(), "b".into()]),
895                        content: "value-ab".into(),
896                        rules: vec![VisitedRule {
897                            rule_index: 0,
898                            is_excluded: false
899                        }]
900                    },
901                    VisitedPath {
902                        path: Path::from(vec!["a".into(), "c".into()]),
903                        content: "value-ac".into(),
904                        rules: vec![VisitedRule {
905                            rule_index: 0,
906                            is_excluded: false
907                        }]
908                    },
909                    VisitedPath {
910                        path: Path::from(vec!["d".into()]),
911                        content: "value-d".into(),
912                        rules: vec![VisitedRule {
913                            rule_index: 0,
914                            is_excluded: false
915                        }]
916                    }
917                ],
918            }
919        );
920    }
921
922    #[test]
923    fn test_include_same_change_multiple_times_reversed() {
924        // If multiple "include" scopes cover the same field, it should only be returned once.
925        // This one specifically tests when a more generic path is added _after_ a specific path.
926        // (The more specific one should be removed from the tree)
927
928        let ruleset = ScopedRuleSet::new(&[Scope::include(vec![
929            Path::from(vec!["a".into(), "b".into()]),
930            Path::from(vec!["a".into()]),
931        ])]);
932
933        let mut event = SimpleEvent::Map(
934            [
935                (
936                    "a".into(),
937                    SimpleEvent::Map(
938                        [
939                            ("b".into(), SimpleEvent::String("value-ab".into())),
940                            ("c".into(), SimpleEvent::String("value-ac".into())),
941                        ]
942                        .into(),
943                    ),
944                ),
945                ("d".into(), SimpleEvent::String("value-d".into())),
946            ]
947            .into(),
948        );
949
950        let paths = visit_event(&mut event, &ruleset);
951
952        assert_eq!(
953            paths,
954            Visited {
955                paths: vec![
956                    VisitedPath {
957                        path: Path::from(vec!["a".into(), "b".into()]),
958                        content: "value-ab".into(),
959                        rules: vec![VisitedRule {
960                            rule_index: 0,
961                            is_excluded: false
962                        }]
963                    },
964                    VisitedPath {
965                        path: Path::from(vec!["a".into(), "c".into()]),
966                        content: "value-ac".into(),
967                        rules: vec![VisitedRule {
968                            rule_index: 0,
969                            is_excluded: false
970                        }]
971                    },
972                    VisitedPath {
973                        path: Path::from(vec!["d".into()]),
974                        content: "value-d".into(),
975                        rules: vec![]
976                    }
977                ],
978            }
979        );
980    }
981
982    #[test]
983    fn test_fields_should_act_as_wildcard_on_lists() {
984        let ruleset = ScopedRuleSet::new(&[Scope::include(vec![Path::from(vec![
985            "a".into(),
986            "b".into(),
987        ])])])
988        .with_implicit_index_wildcards(true);
989
990        let mut event = SimpleEvent::Map(
991            [(
992                "a".into(),
993                SimpleEvent::List(vec![SimpleEvent::Map(
994                    [("b".into(), SimpleEvent::String("value-a-0-b".to_string()))].into(),
995                )]),
996            )]
997            .into(),
998        );
999
1000        let paths = visit_event(&mut event, &ruleset);
1001
1002        assert_eq!(
1003            paths,
1004            Visited {
1005                paths: vec![VisitedPath {
1006                    path: Path::from(vec!["a".into(), 0.into(), "b".into()]),
1007                    content: "value-a-0-b".into(),
1008                    // Rule 0 matches this path even though there is a "0" index between the "a" and "b" field
1009                    rules: vec![VisitedRule {
1010                        rule_index: 0,
1011                        is_excluded: false
1012                    }]
1013                },],
1014            }
1015        );
1016    }
1017
1018    #[test]
1019    fn test_exclude_implicit_wildcard_path() {
1020        // This makes sure that exclusions are applied even when a path matches due to an
1021        // implicit array wildcard.
1022
1023        let ab_path = Path::from(vec!["a".into(), "b".into()]);
1024        let ruleset = ScopedRuleSet::new(&[Scope::include_and_exclude(
1025            vec![ab_path.clone()],
1026            vec![ab_path],
1027        )])
1028        .with_implicit_index_wildcards(true);
1029
1030        let mut event = SimpleEvent::Map(
1031            [(
1032                "a".into(),
1033                SimpleEvent::List(vec![SimpleEvent::Map(
1034                    [("b".into(), SimpleEvent::String("value-a-0-b".to_string()))].into(),
1035                )]),
1036            )]
1037            .into(),
1038        );
1039
1040        let paths = visit_event(&mut event, &ruleset);
1041
1042        assert_eq!(
1043            paths,
1044            Visited {
1045                paths: vec![VisitedPath {
1046                    path: Path::from(vec!["a".into(), 0.into(), "b".into()]),
1047                    content: "value-a-0-b".into(),
1048                    // Rule 0 matches this path even though there is a "0" index between the "a" and "b" field
1049                    rules: vec![VisitedRule {
1050                        rule_index: 0,
1051                        is_excluded: true
1052                    }]
1053                },],
1054            }
1055        );
1056    }
1057
1058    #[test]
1059    fn test_included_scope_both_implicit_and_explicit_index() {
1060        let a_0_c_path = Path::from(vec!["a".into(), 0.into(), "c".into()]);
1061        let ab_path = Path::from(vec!["a".into(), "b".into()]);
1062        let a_1_d_path = Path::from(vec!["a".into(), 1.into(), "d".into()]);
1063
1064        let ruleset = ScopedRuleSet::new(&[Scope::include(vec![a_0_c_path, ab_path, a_1_d_path])])
1065            .with_implicit_index_wildcards(true);
1066
1067        let mut event = SimpleEvent::Map(
1068            [(
1069                "a".into(),
1070                SimpleEvent::List(vec![
1071                    SimpleEvent::Map(
1072                        [
1073                            ("b".into(), SimpleEvent::String("value-a-0-b".to_string())),
1074                            ("c".into(), SimpleEvent::String("value-a-0-c".to_string())),
1075                            ("d".into(), SimpleEvent::String("value-a-0-d".to_string())),
1076                        ]
1077                        .into(),
1078                    ),
1079                    SimpleEvent::Map(
1080                        [
1081                            ("b".into(), SimpleEvent::String("value-a-1-b".to_string())),
1082                            ("c".into(), SimpleEvent::String("value-a-1-c".to_string())),
1083                            ("d".into(), SimpleEvent::String("value-a-1-d".to_string())),
1084                        ]
1085                        .into(),
1086                    ),
1087                ]),
1088            )]
1089            .into(),
1090        );
1091
1092        let paths = visit_event(&mut event, &ruleset);
1093
1094        assert_eq!(
1095            paths,
1096            Visited {
1097                paths: vec![
1098                    VisitedPath {
1099                        path: Path::from(vec!["a".into(), 0.into(), "b".into()]),
1100                        content: "value-a-0-b".into(),
1101                        rules: vec![VisitedRule {
1102                            rule_index: 0,
1103                            is_excluded: false
1104                        }]
1105                    },
1106                    VisitedPath {
1107                        path: Path::from(vec!["a".into(), 0.into(), "c".into()]),
1108                        content: "value-a-0-c".into(),
1109                        rules: vec![VisitedRule {
1110                            rule_index: 0,
1111                            is_excluded: false
1112                        }]
1113                    },
1114                    VisitedPath {
1115                        path: Path::from(vec!["a".into(), 0.into(), "d".into()]),
1116                        content: "value-a-0-d".into(),
1117                        rules: vec![]
1118                    },
1119                    VisitedPath {
1120                        path: Path::from(vec!["a".into(), 1.into(), "b".into()]),
1121                        content: "value-a-1-b".into(),
1122                        rules: vec![VisitedRule {
1123                            rule_index: 0,
1124                            is_excluded: false
1125                        }]
1126                    },
1127                    VisitedPath {
1128                        path: Path::from(vec!["a".into(), 1.into(), "c".into()]),
1129                        content: "value-a-1-c".into(),
1130                        rules: vec![]
1131                    },
1132                    VisitedPath {
1133                        path: Path::from(vec!["a".into(), 1.into(), "d".into()]),
1134                        content: "value-a-1-d".into(),
1135                        rules: vec![VisitedRule {
1136                            rule_index: 0,
1137                            is_excluded: false
1138                        }]
1139                    },
1140                ],
1141            }
1142        );
1143    }
1144
1145    #[test]
1146    fn test_duplicate_rules_are_filtered_out() {
1147        // Internally there may be multiple "active" trees which could end up storing duplicate rules.
1148        // Those duplicates must be filtered out.
1149
1150        // Both of these scopes match the path "a[0].b" (one with an explicit index, one with an implicit wildcard index)
1151        let a_b_path = Path::from(vec!["a".into(), "b".into()]);
1152        let a_0_b_path = Path::from(vec!["a".into(), 0.into(), "b".into()]);
1153
1154        let ruleset = ScopedRuleSet::new(&[Scope::include(vec![a_b_path, a_0_b_path])])
1155            .with_implicit_index_wildcards(true);
1156
1157        let mut event = SimpleEvent::Map(
1158            [(
1159                "a".into(),
1160                SimpleEvent::List(vec![SimpleEvent::Map(
1161                    [("b".into(), SimpleEvent::String("value-a-0-b".to_string()))].into(),
1162                )]),
1163            )]
1164            .into(),
1165        );
1166
1167        let paths = visit_event(&mut event, &ruleset);
1168
1169        assert_eq!(
1170            paths,
1171            Visited {
1172                paths: vec![VisitedPath {
1173                    path: Path::from(vec!["a".into(), 0.into(), "b".into()]),
1174                    content: "value-a-0-b".into(),
1175                    rules: vec![VisitedRule {
1176                        rule_index: 0,
1177                        is_excluded: false
1178                    }]
1179                },],
1180            }
1181        );
1182    }
1183
1184    #[test]
1185    fn test_deeply_nested_implicit_wildcard_index() {
1186        // A wildcard index can skip multiple levels of indexing, not just 1
1187
1188        let a_b_path = Path::from(vec!["a".into(), "b".into()]);
1189        let ruleset = ScopedRuleSet::new(&[Scope::include(vec![a_b_path])])
1190            .with_implicit_index_wildcards(true);
1191
1192        let mut event = SimpleEvent::Map(
1193            [(
1194                "a".into(),
1195                SimpleEvent::List(vec![SimpleEvent::List(vec![SimpleEvent::Map(
1196                    [("b".into(), SimpleEvent::String("value-a-0-0-b".to_string()))].into(),
1197                )])]),
1198            )]
1199            .into(),
1200        );
1201
1202        let paths = visit_event(&mut event, &ruleset);
1203
1204        assert_eq!(
1205            paths,
1206            Visited {
1207                paths: vec![VisitedPath {
1208                    path: Path::from(vec!["a".into(), 0.into(), 0.into(), "b".into()]),
1209                    content: "value-a-0-0-b".into(),
1210                    rules: vec![VisitedRule {
1211                        rule_index: 0,
1212                        is_excluded: false
1213                    }]
1214                },],
1215            }
1216        );
1217    }
1218
1219    #[test]
1220    fn test_implicit_index_wildcard_is_disabled_by_default() {
1221        let ruleset = ScopedRuleSet::new(&[Scope::include(vec![Path::from(vec![
1222            "a".into(),
1223            "b".into(),
1224        ])])]);
1225
1226        let mut event = SimpleEvent::Map(
1227            [(
1228                "a".into(),
1229                SimpleEvent::List(vec![SimpleEvent::Map(
1230                    [("b".into(), SimpleEvent::String("value-a-0-b".to_string()))].into(),
1231                )]),
1232            )]
1233            .into(),
1234        );
1235
1236        let paths = visit_event(&mut event, &ruleset);
1237
1238        assert_eq!(
1239            paths,
1240            Visited {
1241                paths: vec![VisitedPath {
1242                    path: Path::from(vec!["a".into(), 0.into(), "b".into()]),
1243                    content: "value-a-0-b".into(),
1244                    // Rule 0 does NOT match, since the implicit index wildcard is disabled
1245                    rules: vec![]
1246                },],
1247            }
1248        );
1249    }
1250}