Skip to main content

hidane_rules/
paths.rs

1//! Which `match` statements cover a path, and what their wildcards hold (#59).
2//!
3//! As measured with `tools/oracle/rules_paths.py` (`tests/fixtures/rules_paths.json`):
4//!
5//! - A `match` covers a path when its segments, after those of every `match` around it
6//!   (outermost first, from the service), take the whole path: `databases`, the database ID,
7//!   `documents`, then the document's segments. Its `allow` statements apply to that path only;
8//!   a `match` that takes part of it only leads to the matches inside.
9//! - A literal segment takes itself; a wildcard (`{d}`, `{d=*}`) takes one segment and holds it
10//!   as a string.
11//! - A recursive wildcard (`{rest=**}`) holds a path: one or more segments in rules version 1,
12//!   where the compiler only allows it last, and zero or more in version 2, where it may come
13//!   anywhere, once per `match`. So in version 2 `/c/{d}/{rest=**}` covers `c/d` itself, and a
14//!   nested `/{rest=**}` covers the document of the `match` around it.
15//!
16//! A list is checked on its collection's path followed by an unknown document ID, and a
17//! collection group's path holds an unknown run of segments before its collection ID
18//! (`tools/oracle/rules_list.py`): only a wildcard takes the unknown ID, and leaves its variable
19//! unbound (using it is a `Null value error`); only a recursive wildcard takes an unknown run.
20
21use crate::ast::{Match, SegmentKind, Service, Version};
22
23/// The value of a path variable.
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum Binding {
26    /// A wildcard's segment.
27    Segment(String),
28    /// A recursive wildcard's segments; an unknown part is [`UNKNOWN_ID`] or [`UNKNOWN_RUN`].
29    Path(Vec<String>),
30    /// A wildcard that took an unknown document ID.
31    Unbound,
32}
33
34/// A segment of a path to cover.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum Part<'p> {
37    Segment(&'p str),
38    /// The document ID of a list: any one segment.
39    UnknownId,
40    /// The parents of a collection group's collections: any number of segments.
41    UnknownRun,
42}
43
44/// How an unknown document ID shows in a recursive wildcard's path (no segment holds it).
45pub const UNKNOWN_ID: &str = "\u{0}*";
46/// How an unknown run of segments shows in a recursive wildcard's path.
47pub const UNKNOWN_RUN: &str = "\u{0}**";
48
49/// A `match` that covers a path: the matches from the service down to it, and the variables
50/// they bound, outermost first.
51#[derive(Debug, Clone)]
52pub struct Covering<'a> {
53    pub chain: Vec<&'a Match>,
54    pub bindings: Vec<(String, Binding)>,
55}
56
57impl Covering<'_> {
58    /// The innermost `match`, whose `allow` statements apply.
59    pub fn target(&self) -> &Match {
60        self.chain
61            .last()
62            .expect("a covering has at least one match")
63    }
64}
65
66/// Every `match` of `service` that covers `path`, in source order (a `match` before the ones
67/// inside it). A `match` that covers it in several ways (two recursive wildcards splitting it
68/// differently) is listed once, with the first way found, which gives earlier recursive
69/// wildcards fewer segments.
70pub fn covering<'a>(version: Version, service: &'a Service, path: &[&str]) -> Vec<Covering<'a>> {
71    let parts: Vec<Part<'_>> = path.iter().map(|s| Part::Segment(s)).collect();
72    covering_parts(version, service, &parts)
73}
74
75/// [`covering`] for a path with unknown parts.
76pub fn covering_parts<'a>(
77    version: Version,
78    service: &'a Service,
79    path: &[Part<'_>],
80) -> Vec<Covering<'a>> {
81    let mut found = Vec::new();
82    for m in &service.body.matches {
83        visit(
84            version,
85            m,
86            path,
87            0,
88            &mut Vec::new(),
89            &mut Vec::new(),
90            &mut found,
91        );
92    }
93    found
94}
95
96fn visit<'a>(
97    version: Version,
98    m: &'a Match,
99    path: &[Part<'_>],
100    at: usize,
101    chain: &mut Vec<&'a Match>,
102    bindings: &mut Vec<(String, Binding)>,
103    found: &mut Vec<Covering<'a>>,
104) {
105    chain.push(m);
106    let mut covered = false;
107    for (end, taken) in prefixes(version, m, path, at) {
108        let depth = bindings.len();
109        bindings.extend(taken);
110        if end == path.len() && !covered {
111            covered = true;
112            found.push(Covering {
113                chain: chain.clone(),
114                bindings: bindings.clone(),
115            });
116        }
117        for inner in &m.body.matches {
118            visit(version, inner, path, end, chain, bindings, found);
119        }
120        bindings.truncate(depth);
121    }
122    chain.pop();
123}
124
125/// Every way the segments of `m` can take a prefix of `path[at..]`: where it ends, and what
126/// its variables hold.
127fn prefixes(
128    version: Version,
129    m: &Match,
130    path: &[Part<'_>],
131    at: usize,
132) -> Vec<(usize, Vec<(String, Binding)>)> {
133    let mut out = Vec::new();
134    take(version, &m.path, path, at, &mut Vec::new(), &mut out);
135    out
136}
137
138fn take(
139    version: Version,
140    segments: &[crate::ast::Segment],
141    path: &[Part<'_>],
142    at: usize,
143    bound: &mut Vec<(String, Binding)>,
144    out: &mut Vec<(usize, Vec<(String, Binding)>)>,
145) {
146    let Some((first, rest)) = segments.split_first() else {
147        out.push((at, bound.clone()));
148        return;
149    };
150    match &first.kind {
151        SegmentKind::Literal(literal) => {
152            if path.get(at) == Some(&Part::Segment(literal)) {
153                take(version, rest, path, at + 1, bound, out);
154            }
155        }
156        SegmentKind::Wildcard(name) => {
157            let binding = match path.get(at) {
158                Some(Part::Segment(segment)) => Binding::Segment((*segment).to_owned()),
159                Some(Part::UnknownId) => Binding::Unbound,
160                Some(Part::UnknownRun) | None => return,
161            };
162            bound.push((name.clone(), binding));
163            take(version, rest, path, at + 1, bound, out);
164            bound.pop();
165        }
166        SegmentKind::Recursive(name) => {
167            let fewest = match version {
168                Version::V1 => 1,
169                Version::V2 => 0,
170            };
171            for end in at + fewest..=path.len() {
172                let segments = path[at..end]
173                    .iter()
174                    .map(|part| match part {
175                        Part::Segment(s) => (*s).to_owned(),
176                        Part::UnknownId => UNKNOWN_ID.to_owned(),
177                        Part::UnknownRun => UNKNOWN_RUN.to_owned(),
178                    })
179                    .collect();
180                bound.push((name.clone(), Binding::Path(segments)));
181                take(version, rest, path, end, bound, out);
182                bound.pop();
183            }
184        }
185    }
186}