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}