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 or collection 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, or the collection ID of one without (`from` with an empty
39    /// collection ID): any one segment.
40    UnknownId,
41    /// The parents of a collection group's collections: any number of segments.
42    UnknownRun,
43}
44
45/// How an unknown document ID shows in a recursive wildcard's path (no segment holds it).
46pub const UNKNOWN_ID: &str = "\u{0}*";
47/// How an unknown run of segments shows in a recursive wildcard's path.
48pub const UNKNOWN_RUN: &str = "\u{0}**";
49
50/// A `match` that covers a path: the matches from the service down to it, and the variables
51/// they bound, outermost first.
52#[derive(Debug, Clone)]
53pub struct Covering<'a> {
54    pub chain: Vec<&'a Match>,
55    pub bindings: Vec<(String, Binding)>,
56}
57
58impl Covering<'_> {
59    /// The innermost `match`, whose `allow` statements apply.
60    pub fn target(&self) -> &Match {
61        self.chain
62            .last()
63            .expect("a covering has at least one match")
64    }
65}
66
67/// Every `match` of `service` that covers `path`, in source order (a `match` before the ones
68/// inside it). A `match` that covers it in several ways (two recursive wildcards splitting it
69/// differently) is listed once, with the first way found, which gives earlier recursive
70/// wildcards fewer segments.
71pub fn covering<'a>(version: Version, service: &'a Service, path: &[&str]) -> Vec<Covering<'a>> {
72    let parts: Vec<Part<'_>> = path.iter().map(|s| Part::Segment(s)).collect();
73    covering_parts(version, service, &parts)
74}
75
76/// [`covering`] for a path with unknown parts.
77pub fn covering_parts<'a>(
78    version: Version,
79    service: &'a Service,
80    path: &[Part<'_>],
81) -> Vec<Covering<'a>> {
82    let mut found = Vec::new();
83    for m in &service.body.matches {
84        visit(
85            version,
86            m,
87            path,
88            0,
89            &mut Vec::new(),
90            &mut Vec::new(),
91            &mut found,
92        );
93    }
94    found
95}
96
97fn visit<'a>(
98    version: Version,
99    m: &'a Match,
100    path: &[Part<'_>],
101    at: usize,
102    chain: &mut Vec<&'a Match>,
103    bindings: &mut Vec<(String, Binding)>,
104    found: &mut Vec<Covering<'a>>,
105) {
106    chain.push(m);
107    let mut covered = false;
108    for (end, taken) in prefixes(version, m, path, at) {
109        let depth = bindings.len();
110        bindings.extend(taken);
111        if end == path.len() && !covered {
112            covered = true;
113            found.push(Covering {
114                chain: chain.clone(),
115                bindings: bindings.clone(),
116            });
117        }
118        for inner in &m.body.matches {
119            visit(version, inner, path, end, chain, bindings, found);
120        }
121        bindings.truncate(depth);
122    }
123    chain.pop();
124}
125
126/// Every way the segments of `m` can take a prefix of `path[at..]`: where it ends, and what
127/// its variables hold.
128fn prefixes(
129    version: Version,
130    m: &Match,
131    path: &[Part<'_>],
132    at: usize,
133) -> Vec<(usize, Vec<(String, Binding)>)> {
134    let mut out = Vec::new();
135    take(version, &m.path, path, at, &mut Vec::new(), &mut out);
136    out
137}
138
139fn take(
140    version: Version,
141    segments: &[crate::ast::Segment],
142    path: &[Part<'_>],
143    at: usize,
144    bound: &mut Vec<(String, Binding)>,
145    out: &mut Vec<(usize, Vec<(String, Binding)>)>,
146) {
147    let Some((first, rest)) = segments.split_first() else {
148        out.push((at, bound.clone()));
149        return;
150    };
151    match &first.kind {
152        SegmentKind::Literal(literal) => {
153            if path.get(at) == Some(&Part::Segment(literal)) {
154                take(version, rest, path, at + 1, bound, out);
155            }
156        }
157        SegmentKind::Wildcard(name) => {
158            let binding = match path.get(at) {
159                Some(Part::Segment(segment)) => Binding::Segment((*segment).to_owned()),
160                Some(Part::UnknownId) => Binding::Unbound,
161                Some(Part::UnknownRun) | None => return,
162            };
163            bound.push((name.clone(), binding));
164            take(version, rest, path, at + 1, bound, out);
165            bound.pop();
166        }
167        SegmentKind::Recursive(name) => {
168            let fewest = match version {
169                Version::V1 => 1,
170                Version::V2 => 0,
171            };
172            for end in at + fewest..=path.len() {
173                let segments = path[at..end]
174                    .iter()
175                    .map(|part| match part {
176                        Part::Segment(s) => (*s).to_owned(),
177                        Part::UnknownId => UNKNOWN_ID.to_owned(),
178                        Part::UnknownRun => UNKNOWN_RUN.to_owned(),
179                    })
180                    .collect();
181                bound.push((name.clone(), Binding::Path(segments)));
182                take(version, rest, path, end, bound, out);
183                bound.pop();
184            }
185        }
186    }
187}