hidane_rules/eval.rs
1//! Evaluating a ruleset for one request: what the evaluator takes and what it decides.
2//!
3//! [`authorize`] finds the `allow` statements that apply to the request and evaluates their
4//! conditions. What the official emulator does, as measured with
5//! `tools/oracle/rules_conditions.py` (`results/rules-conditions-official-v1.22.0.json`) and
6//! `tools/oracle/rules_eval.py` (`tests/fixtures/rules_eval.json`):
7//!
8//! - Types are checked when a condition runs. Each operator, function and method takes the
9//! types its error lists (`Unsupported operation error. Received: list + list. Expected:
10//! int + int, …`); a method a type does not have is a `Function not found error`.
11//! - Errors are absorbed the CEL way: `error || true` and `true || error` are true, `error &&
12//! false` and `false && error` are false, anything else with an error is the error. An error
13//! that reaches the top of a condition denies, like `false`.
14//! - `==` between values of different types is false; an int equals a float of the same value,
15//! but not inside lists and maps. Sets and map diffs compare numbers by value at any depth.
16//! - Strings are Java strings: sizes, indexes and slices count UTF-16 code units, `lower()` and
17//! `upper()` change ASCII letters only, and regular expressions are RE2.
18//! - The operands of an operator, a call or a list or map literal are all evaluated, the last
19//! first (as `debug()` shows), and the first error in order wins (for `x in y`, `y`'s; for a
20//! method, a null receiver or a method it does not have comes before its arguments' errors).
21//! - The arguments of a function (the last first), then its `let`s in order, are evaluated
22//! before its result, but an error in one only matters if the function uses it.
23//! - A decision evaluates at most 1,000 expressions ([`MAX_EXPRESSIONS`], measured with
24//! `tools/oracle/rules_expressions.py`). Each expression evaluated counts once, parentheses
25//! not at all, and some count more: `&&` and `||` once more when they evaluate their right
26//! side, a conditional once more once its condition is evaluated (whatever it came to) and two
27//! more again when it is false, `is` and each `let` once more, a path once more for each
28//! literal segment. What a short circuit or a conditional skips does not count. An expression
29//! counts once what is in it is evaluated, a call before its function runs: `debug()` prints
30//! only within the limit (`tools/oracle/rules_expressions_report.py`). The expression that goes
31//! past the limit is an error ("Unable to evaluate the expression as the maximum of 1000
32//! expressions to evaluate has been reached."), whatever it came to, and nothing is evaluated
33//! after it, so nothing absorbs it and the coverage report holds no value of it or of what
34//! holds it. The official emulator's second evaluation of a condition (below) counts from
35//! zero, so evaluating once comes to the same decisions.
36//!
37//! Each condition is evaluated once, so a denial lists each statement once; the official
38//! emulator evaluates again after an error and lists everything twice
39//! (`docs/parity-exceptions.md`).
40//!
41//! [`authorize_traced`] also reports what the official emulator logs and counts for its
42//! coverage report (`tools/oracle/rules_coverage.py`, [`Trace`]): each `debug()` value, and
43//! what each expression evaluated to. Which statements it evaluates shows there: those of
44//! every `match` covering the path, each `match`'s until one allows.
45
46mod constraint;
47mod interp;
48mod library;
49mod list;
50mod time;
51mod trace;
52mod value;
53
54pub use constraint::Comparator;
55pub use trace::{Evaluation, KeyStatus, Observed, Outcome, Segment, Trace, Verdict};
56pub(crate) use trace::{NodeKey, Traced, hash_val, observe, shows};
57pub use value::java_double;
58
59use std::rc::Rc;
60
61use indexmap::IndexMap;
62
63use crate::{
64 Ruleset, ast,
65 paths::{Binding, Covering, Part},
66};
67use interp::{Frame, Interp, Level};
68pub(crate) use value::Val;
69
70/// A value as rules see it. Firestore values convert to these (`timestamp`, `bytes`, `latlng`,
71/// a reference as a `path`, maps and lists); the others only come from rules.
72#[derive(Debug, Clone, PartialEq)]
73pub enum Value {
74 Null,
75 Bool(bool),
76 Int(i64),
77 Float(f64),
78 String(String),
79 Bytes(Vec<u8>),
80 Timestamp(Timestamp),
81 /// Seconds and nanoseconds.
82 Duration(i64, i32),
83 /// Latitude and longitude, in degrees.
84 LatLng(f64, f64),
85 /// The segments of a path. A reference holds its document's path from `databases`, like
86 /// [`Request::path`] and [`Document::path`].
87 Path(Vec<String>),
88 List(Vec<Value>),
89 Map(Map),
90}
91
92/// A map's entries, in the order the document or the literal holds them. Equality ignores the
93/// order; where rules list keys in another order, the evaluator sorts.
94pub type Map = IndexMap<String, Value>;
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
97pub struct Timestamp {
98 pub seconds: i64,
99 pub nanos: i32,
100}
101
102/// The method a request is checked against. `read` covers `get` and `list`, `write` covers
103/// `create`, `update` and `delete`.
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub enum Method {
106 Get,
107 List,
108 Create,
109 Update,
110 Delete,
111}
112
113impl Method {
114 /// The name rules see in `request.method` and denials print.
115 pub fn name(self) -> &'static str {
116 match self {
117 Self::Get => "get",
118 Self::List => "list",
119 Self::Create => "create",
120 Self::Update => "update",
121 Self::Delete => "delete",
122 }
123 }
124
125 fn statement(self) -> ast::Method {
126 match self {
127 Self::Get => ast::Method::Get,
128 Self::List => ast::Method::List,
129 Self::Create => ast::Method::Create,
130 Self::Update => ast::Method::Update,
131 Self::Delete => ast::Method::Delete,
132 }
133 }
134}
135
136/// A document as `resource`, `request.resource` and `get()` give it.
137#[derive(Debug, Clone, PartialEq)]
138pub struct Document {
139 /// The path from `databases`: `databases`, the database ID, `documents`, then the
140 /// document's segments.
141 pub path: Vec<String>,
142 pub data: Map,
143}
144
145/// `request.auth` for a caller with a token.
146#[derive(Debug, Clone, PartialEq)]
147pub struct Auth {
148 /// The token's `sub`; without one, `request.auth.uid` is null.
149 pub uid: Option<String>,
150 /// Every claim of the token.
151 pub token: Map,
152}
153
154/// `request.query` for a list, and the filter that decides it.
155///
156/// `request.query` holds `limit`, `offset` and `orderBy` from here, and what the official
157/// emulator derives from the path (`tools/oracle/rules_request_fields.py`): `kind`, the
158/// collection ID (null for a query of every collection); `parent`, the document the collection
159/// is in (null at the root), named `ancestor` for a collection group; `allDescendants`; and
160/// `distinct` and `selectOnlyKeys`, always false, and `groupBy`, always empty, which Firestore
161/// queries cannot set.
162#[derive(Debug, Clone, PartialEq, Default)]
163pub struct Query {
164 pub limit: Option<i64>,
165 pub offset: Option<i64>,
166 /// The orderings the query gives (not those completing it), field path to `ASC` or
167 /// `DESC`, in the order `request.query.orderBy` lists them.
168 pub order_by: Vec<(String, String)>,
169 pub filter: Option<Filter>,
170 /// For a collection group, its collection ID: the list covers the collections with that ID
171 /// at any depth below [`Request::path`] (an empty ID: every collection below it).
172 pub group: Option<String>,
173 /// With an empty [`Query::group`]: only the collections directly below [`Request::path`]
174 /// (a query whose `from` has no collection ID and is not `allDescendants`).
175 pub children: bool,
176 /// `request.fields`: the fields the query selects, as the official emulator lists them
177 /// (`None`, null, without a projection).
178 pub fields: Option<Vec<String>>,
179}
180
181/// A query's filter.
182#[derive(Debug, Clone, PartialEq)]
183pub enum Filter {
184 And(Vec<Filter>),
185 Or(Vec<Filter>),
186 /// A field, by its segments (`a.b` is `["a", "b"]`), and the value it is compared with
187 /// (null for the unary operators; a list for `In`, `NotIn` and `ArrayContainsAny`).
188 Field {
189 field: Vec<String>,
190 op: FilterOp,
191 value: Value,
192 },
193 /// The document's name, and a reference ([`Value::Path`]) or a list of them.
194 Name {
195 op: FilterOp,
196 value: Value,
197 },
198}
199
200#[derive(Debug, Clone, Copy, PartialEq, Eq)]
201pub enum FilterOp {
202 Lt,
203 Le,
204 Gt,
205 Ge,
206 Eq,
207 Ne,
208 ArrayContains,
209 In,
210 ArrayContainsAny,
211 NotIn,
212 IsNan,
213 IsNull,
214 IsNotNan,
215 IsNotNull,
216}
217
218/// One request, as rules see it.
219#[derive(Debug, Clone, PartialEq)]
220pub struct Request {
221 pub method: Method,
222 /// A create or an update without an existence precondition (a set, a patch, a transform):
223 /// whether the document exists chose the method. A denial then also lists the statements of
224 /// the other of the two, which cannot allow the write (`tools/oracle/rules_writes.py`,
225 /// `rules_denials.py`).
226 pub upsert: bool,
227 /// The path checked, from `databases` (a document, or for a list its collection).
228 pub path: Vec<String>,
229 pub auth: Option<Auth>,
230 pub time: Timestamp,
231 /// The document as it would be after a create or an update, or for a verify the stored
232 /// one; `None` otherwise.
233 pub resource: Option<Document>,
234 pub query: Option<Query>,
235 /// `request.inTransaction`: true for every write, which the official emulator commits in
236 /// a transaction of its own, and for a read of documents in a transaction begun before it;
237 /// false for a read that begins one, and for every list, in a transaction or not
238 /// (`tools/oracle/rules_request_fields.py`).
239 pub in_transaction: bool,
240 /// For a write, `request.writeFields`: the paths of its transforms, then those of its
241 /// update mask (`None`, null, for a write with neither).
242 pub write_fields: Option<Vec<String>>,
243 /// For a write, `request.transforms`: the paths its field transforms change, in order
244 /// (`None`, null, without any).
245 pub transforms: Option<Vec<String>>,
246 /// A create whose document ID the server chooses (a `CreateDocument` without one). The
247 /// official emulator decides it without the ID: [`Request::path`] is the collection's,
248 /// the statements are those of the collection's documents, with the document's wildcard
249 /// unbound, and `request.resource` holds only `data`. `request.autoId` says so for a
250 /// create that is not an upsert; other writes do not have it.
251 pub auto_id: bool,
252 /// A verify of a commit (`Write.verify`), checked as a `get` of its document. `request` has
253 /// the keys of a write's, `request.resource` the stored document (null when there is none),
254 /// as in the official emulator's second pass, where its first has the keys of a read's
255 /// (`tools/oracle/rules_verify.py`, `docs/parity-exceptions.md`).
256 pub verify: bool,
257}
258
259/// The documents `get()` / `exists()` read (as they were before the request's writes) and
260/// `getAfter()` / `existsAfter()` read (as they would be after all of them).
261pub trait Documents {
262 fn get(&self, path: &[String]) -> Option<Document>;
263 fn get_after(&self, path: &[String]) -> Option<Document>;
264}
265
266/// The lookups a request may still make (`tools/oracle/rules_lookups.py`): a single read or
267/// write may look up 10 distinct documents, a query 20, and a batch 10 per write and 20 in all.
268/// A document looked up again costs nothing, across the writes of a batch too, and so does the
269/// document a request reads or writes and every document its batch writes
270/// (`tools/oracle/rules_eval.py`).
271///
272/// The verifies of a commit count as its writes (`tools/oracle/rules_verify.py`). The documents
273/// they verify cost nothing through `getAfter()` and `existsAfter()` only, and in a verify's
274/// condition so do the documents the batch writes.
275///
276/// A decision also evaluates at most [`MAX_EXPRESSIONS`] expressions
277/// (`tools/oracle/rules_expressions.py`): the statements of the method that applies, in order,
278/// count together; each write and verify of a batch, the other method of an upsert and each way
279/// of a list count from zero.
280#[derive(Debug, Clone)]
281pub struct Budget {
282 /// Distinct documents looked up so far, by the whole request or batch.
283 pub looked_up: Vec<Vec<String>>,
284 /// How many of them the current write or read looked up first.
285 pub this_part: usize,
286 pub part_limit: usize,
287 pub total_limit: usize,
288 /// The documents the batch writes, from `databases` (see [`Budget::add_written`]).
289 pub written: Vec<Vec<String>>,
290 /// The documents the batch verifies, from `databases` (see [`Budget::add_verified`]).
291 pub verified: Vec<Vec<String>>,
292 /// Whether a verify is being decided (see [`Budget::next_verify`]).
293 pub verifying: bool,
294 /// The expressions the decision has evaluated so far.
295 pub expressions: usize,
296}
297
298/// The most expressions a decision may evaluate.
299pub const MAX_EXPRESSIONS: usize = 1_000;
300
301impl Budget {
302 fn with(part_limit: usize, total_limit: usize) -> Self {
303 Self {
304 looked_up: Vec::new(),
305 this_part: 0,
306 part_limit,
307 total_limit,
308 written: Vec::new(),
309 verified: Vec::new(),
310 verifying: false,
311 expressions: 0,
312 }
313 }
314
315 /// The budget of another decision of the same request: its lookups so far, but none of
316 /// the expressions this one evaluated.
317 fn another(&self) -> Self {
318 Self {
319 expressions: 0,
320 ..self.clone()
321 }
322 }
323
324 /// Records a document the batch writes (or deletes): any write of the batch may look it up
325 /// for free. The document of the request being decided is free without this.
326 pub fn add_written(&mut self, path: Vec<String>) {
327 if !self.written.contains(&path) {
328 self.written.push(path);
329 }
330 }
331
332 /// Records a document the batch verifies: any write or verify of the batch may look it up
333 /// with `getAfter()` or `existsAfter()` for free.
334 pub fn add_verified(&mut self, path: Vec<String>) {
335 if !self.verified.contains(&path) {
336 self.verified.push(path);
337 }
338 }
339
340 /// Whether `path` costs nothing to look up, `after` the writes or before them, besides the
341 /// document of the request being decided and those looked up already.
342 pub fn free(&self, path: &[String], after: bool) -> bool {
343 let has = |list: &[Vec<String>]| list.iter().any(|p| p.as_slice() == path);
344 if after {
345 has(&self.written) || has(&self.verified)
346 } else {
347 !self.verifying && has(&self.written)
348 }
349 }
350
351 /// A single read or write.
352 pub fn single() -> Self {
353 Self::with(10, 10)
354 }
355
356 /// A query.
357 pub fn query() -> Self {
358 Self::with(20, 20)
359 }
360
361 /// A batch of writes; call [`Budget::next_write`] before each one.
362 pub fn batch() -> Self {
363 Self::with(10, 20)
364 }
365
366 pub fn next_write(&mut self) {
367 self.this_part = 0;
368 self.verifying = false;
369 self.expressions = 0;
370 }
371
372 /// Like [`Budget::next_write`], before a verify of the batch.
373 pub fn next_verify(&mut self) {
374 self.this_part = 0;
375 self.verifying = true;
376 self.expressions = 0;
377 }
378}
379
380/// What the rules decided.
381#[derive(Debug, Clone, PartialEq, Eq)]
382pub enum Decision {
383 Allow,
384 /// `PERMISSION_DENIED`, with the official emulator's message.
385 Deny(String),
386}
387
388/// Decides one request against `ruleset` (the `cloud.firestore` service). `resource` is the
389/// stored document (`None` if it does not exist, or for a list). The rules' version decides
390/// how recursive wildcards match.
391///
392/// The `allow` statements of every `match` that covers the path and names the method apply, in
393/// source order; one whose condition is true allows. A denial lists each one's result:
394/// `false for 'get' @ L5` or the error instead of `false`, with the line of the condition, or
395/// says `No matching allow statements` when none applies. For an upsert the statements of the
396/// other method are listed too, `create`'s first: each is `false`, or its condition's error.
397/// Their lookups do not count against `budget`.
398///
399/// A list is decided from its query's filter, not from the documents (`list`).
400pub fn authorize(
401 ruleset: &Ruleset,
402 request: &Request,
403 resource: Option<&Document>,
404 documents: &dyn Documents,
405 budget: &mut Budget,
406) -> Decision {
407 authorize_traced(
408 ruleset,
409 request,
410 resource,
411 documents,
412 budget,
413 &mut Trace::new(),
414 )
415}
416
417/// [`authorize`], reporting into `trace` what the conditions evaluated.
418///
419/// An upsert evaluates the statements of both methods, whichever applies, as the official
420/// emulator does: `create`'s, then `update`'s, as [`authorize`] evaluates those of a method
421/// (`tools/oracle/rules_coverage.py`). Only those of the method that applies decide.
422pub fn authorize_traced(
423 ruleset: &Ruleset,
424 request: &Request,
425 resource: Option<&Document>,
426 documents: &dyn Documents,
427 budget: &mut Budget,
428 trace: &mut Trace,
429) -> Decision {
430 let Some(service) = ruleset.service("cloud.firestore") else {
431 return unmatched(request, resource.map_or(Val::Null, value::resource), trace);
432 };
433 if request.method == Method::List {
434 return list::authorize(ruleset, service, request, documents, budget, trace);
435 }
436 let mut parts: Vec<Part<'_>> = request.path.iter().map(|s| Part::Segment(s)).collect();
437 if request.auto_id {
438 parts.push(Part::UnknownId);
439 }
440 let coverings = crate::paths::covering_parts(ruleset.version, service, &parts);
441 let other = match request.method {
442 Method::Create if request.upsert => Some(Method::Update),
443 Method::Update if request.upsert => Some(Method::Create),
444 _ => None,
445 };
446 let applying = statements(&coverings, request.method);
447 let others = other.map(|other| (other, statements(&coverings, other)));
448 if applying.is_empty() && others.as_ref().is_none_or(|(_, s)| s.is_empty()) {
449 return unmatched(request, resource.map_or(Val::Null, value::resource), trace);
450 }
451
452 // The statements of each method see `request.method` as theirs.
453 let context_for = |method| Context {
454 ruleset,
455 service,
456 request: request_value(request, method),
457 resource: resource.map_or(Val::Null, value::resource),
458 target: &request.path,
459 documents,
460 };
461 let context = context_for(request.method);
462 let Some((other, others)) = others else {
463 return match context.decide(&applying, request.method, budget, trace) {
464 Ok(()) => Decision::Allow,
465 Err(entries) => denied(&entries),
466 };
467 };
468 let other_context = context_for(other);
469 // `create`'s statements come first: before the decision when `update` applies. The other
470 // method's lookups count against a copy of the budget, and its expressions from zero.
471 if other == Method::Create {
472 other_context.each(&others, other, &mut budget.another(), trace);
473 }
474 let decided = context.decide(&applying, request.method, budget, trace);
475 let Err(entries) = decided else {
476 if other == Method::Update {
477 other_context.each(&others, other, &mut budget.another(), trace);
478 }
479 return Decision::Allow;
480 };
481 // A denial lists every statement of the other method; `create`'s, evaluated already, are
482 // evaluated again without being traced.
483 let listed = match other {
484 Method::Create => {
485 other_context.listed(&others, other, &mut budget.another(), &mut Trace::new())
486 }
487 _ => other_context.listed(&others, other, &mut budget.another(), trace),
488 };
489 let entries: Vec<String> = match other {
490 Method::Create => listed.into_iter().chain(entries).collect(),
491 _ => entries.into_iter().chain(listed).collect(),
492 };
493 denied(&entries)
494}
495
496fn no_match() -> Decision {
497 Decision::Deny("No matching allow statements".to_owned())
498}
499
500/// The denial of a request no statement applies to, reported as an evaluation of none.
501fn unmatched(request: &Request, resource: Val, trace: &mut Trace) -> Decision {
502 if trace.evaluations.is_some() {
503 trace.evaluating(
504 request.method,
505 &request_value(request, request.method),
506 &resource,
507 );
508 }
509 no_match()
510}
511
512/// A denial listing `entries`.
513fn denied(entries: &[String]) -> Decision {
514 Decision::Deny(format!("\n{}", entries.join(", ")))
515}
516
517/// The `allow` statements for `method` of every `match` in `coverings`, in source order.
518fn statements<'c, 'a>(
519 coverings: &'c [Covering<'a>],
520 method: Method,
521) -> Vec<(&'c ast::Allow, &'c Covering<'a>)> {
522 let method = method.statement();
523 let mut statements = Vec::new();
524 for covering in coverings {
525 for allow in &covering.target().body.allows {
526 if allow
527 .methods
528 .iter()
529 .any(|m| m.method.is_some_and(|m| m.covers(method)))
530 {
531 statements.push((allow, covering));
532 }
533 }
534 }
535 statements.sort_by_key(|(allow, _)| allow.span.start);
536 statements
537}
538
539/// What a statement's condition came to.
540fn verdict(result: &Result<bool, String>) -> Verdict {
541 match result {
542 Ok(true) => Verdict::Allow,
543 Ok(false) => Verdict::False,
544 Err(_) => Verdict::Error,
545 }
546}
547
548fn entry(text: &str, method: Method, line: u32) -> String {
549 format!("{text} for '{}' @ L{line}", method.name())
550}
551
552/// What every condition of one request is evaluated with.
553struct Context<'a> {
554 ruleset: &'a Ruleset,
555 service: &'a ast::Service,
556 request: Val,
557 resource: Val,
558 /// The path whose document is free to look up, in the database `get()` may read.
559 target: &'a [String],
560 documents: &'a dyn Documents,
561}
562
563impl Context<'_> {
564 /// Evaluates `statements` in order; if none allows, each one's entry. As the official
565 /// emulator does, the statements of a `match` stop at the first that allows, but those of
566 /// every other `match` are evaluated too (`tools/oracle/rules_coverage.py`).
567 fn decide(
568 &self,
569 statements: &[(&ast::Allow, &Covering<'_>)],
570 method: Method,
571 budget: &mut Budget,
572 trace: &mut Trace,
573 ) -> Result<(), Vec<String>> {
574 let (entries, allowed) = self.each(statements, method, budget, trace);
575 if allowed { Ok(()) } else { Err(entries) }
576 }
577
578 /// The statements of an upsert's method that does not apply, for a denial: each one's
579 /// entry, `false` or its condition's error.
580 fn listed(
581 &self,
582 statements: &[(&ast::Allow, &Covering<'_>)],
583 method: Method,
584 budget: &mut Budget,
585 trace: &mut Trace,
586 ) -> Vec<String> {
587 let mut entries = Vec::with_capacity(statements.len());
588 let mut marks = Vec::with_capacity(statements.len());
589 let at = trace.evaluating(method, &self.request, &self.resource);
590 for (allow, covering) in statements {
591 marks.push(trace.mark());
592 let (result, line) = match &allow.condition {
593 Some(condition) => (
594 self.evaluate(covering, condition, budget, trace),
595 condition.span.line,
596 ),
597 None => (Ok(true), allow.span.line),
598 };
599 trace.verdict(at, line, verdict(&result));
600 let text = result.err();
601 entries.push(entry(text.as_deref().unwrap_or("false"), method, line));
602 }
603 trace.repeat(&marks);
604 entries
605 }
606
607 /// Evaluates `statements` but those after one that allowed in the same `match`. Returns the
608 /// entries of those that did not allow, and whether one did.
609 fn each(
610 &self,
611 statements: &[(&ast::Allow, &Covering<'_>)],
612 method: Method,
613 budget: &mut Budget,
614 trace: &mut Trace,
615 ) -> (Vec<String>, bool) {
616 let mut entries = Vec::with_capacity(statements.len());
617 let mut marks = Vec::with_capacity(statements.len());
618 let mut allowed: Vec<*const ast::Match> = Vec::new();
619 let at = trace.evaluating(method, &self.request, &self.resource);
620 for (allow, covering) in statements {
621 let target = std::ptr::from_ref(covering.target());
622 if allowed.contains(&target) {
623 continue;
624 }
625 marks.push(trace.mark());
626 let Some(condition) = &allow.condition else {
627 trace.verdict(at, allow.span.line, Verdict::Allow);
628 allowed.push(target);
629 continue;
630 };
631 let result = self.evaluate(covering, condition, budget, trace);
632 trace.verdict(at, condition.span.line, verdict(&result));
633 let text = match result {
634 Ok(true) => {
635 allowed.push(target);
636 continue;
637 }
638 Ok(false) => "false".to_owned(),
639 Err(e) => e,
640 };
641 entries.push(entry(&text, method, condition.span.line));
642 }
643 trace.repeat(&marks);
644 (entries, !allowed.is_empty())
645 }
646
647 /// The condition of a statement at the end of `covering`: true, false or the error's text.
648 fn evaluate(
649 &self,
650 covering: &Covering<'_>,
651 condition: &ast::Expr,
652 budget: &mut Budget,
653 trace: &mut Trace,
654 ) -> Result<bool, String> {
655 let mut interp = Interp::new(
656 levels(self.ruleset, self.service, covering),
657 self.request.clone(),
658 self.resource.clone(),
659 self.target,
660 self.documents,
661 budget,
662 trace,
663 &self.ruleset.parens,
664 );
665 let frame = Frame::condition(covering.chain.len() + 1);
666 interp
667 .eval(condition, frame.scope())
668 .and_then(interp::boolean)
669 .map_err(|e| {
670 if e.message.is_empty() {
671 // Undecided: the official emulator names the condition's start.
672 format!(
673 "evaluation error at L{}:{}",
674 condition.span.line, condition.span.column
675 )
676 } else {
677 e.message
678 }
679 })
680 }
681}
682
683/// The names a condition at the end of `covering` sees: the functions of the file, of the
684/// service and of each `match`, and the variables each `match` binds.
685fn levels<'a>(
686 ruleset: &'a Ruleset,
687 service: &'a ast::Service,
688 covering: &'a Covering<'a>,
689) -> Vec<Level<'a>> {
690 let mut levels = vec![
691 Level {
692 functions: &ruleset.functions,
693 variables: Vec::new(),
694 },
695 Level {
696 functions: &service.body.functions,
697 variables: Vec::new(),
698 },
699 ];
700 let mut bindings = covering.bindings.iter();
701 for m in &covering.chain {
702 let count = m
703 .path
704 .iter()
705 .filter(|s| s.kind.variable().is_some())
706 .count();
707 let variables = bindings
708 .by_ref()
709 .take(count)
710 // A wildcard that took a list's unknown document ID is not bound: using it is a
711 // `Null value error`, as for any unknown name.
712 .filter_map(|(name, binding)| {
713 let value = match binding {
714 Binding::Segment(s) => Val::str(s),
715 Binding::Path(p) => Val::path(p.clone()),
716 Binding::Unbound => return None,
717 };
718 Some((name.as_str(), value))
719 })
720 .collect();
721 levels.push(Level {
722 functions: &m.body.functions,
723 variables,
724 });
725 }
726 levels
727}
728
729/// `request` for the statements of `method`, with the keys the official emulator gives it, in
730/// the order its coverage report lists them (`tools/oracle/rules_request_fields.py`):
731///
732/// - for a `get`: `time`, `auth`, `inTransaction`, `path`, `fields` (null), `method`;
733/// - for a `list`: `time`, `auth`, `inTransaction`, `fields`, `query`, `path`, `method`;
734/// - for a write: `time`, `auth`, `inTransaction`, `path`, `transforms`, `writeFields`,
735/// `readFields` (null), `autoId` (for a create that is not an upsert), `resource` (null for a
736/// delete; only `data` when the ID is the server's), `method`; for a verify, a `get`, the
737/// same but `autoId` (`tools/oracle/rules_verify.py`).
738///
739/// `auth` is null without a token, else `uid` and `token`.
740fn request_value(request: &Request, method: Method) -> Val {
741 let strings = |list: &Option<Vec<String>>| {
742 list.as_ref().map_or(Val::Null, |items| {
743 Val::list(items.iter().map(|s| Val::str(s)).collect())
744 })
745 };
746 let mut map = IndexMap::new();
747 map.insert(
748 "time".to_owned(),
749 Val::from(&Value::Timestamp(request.time)),
750 );
751 map.insert("auth".to_owned(), auth_value(request));
752 map.insert(
753 "inTransaction".to_owned(),
754 Val::Bool(request.in_transaction),
755 );
756 let path = Val::path(request.path.clone());
757 match method {
758 Method::Get if !request.verify => {
759 map.insert("path".to_owned(), path);
760 map.insert("fields".to_owned(), Val::Null);
761 }
762 Method::List => {
763 let query = request.query.clone().unwrap_or_default();
764 map.insert("fields".to_owned(), strings(&query.fields));
765 map.insert("query".to_owned(), query_value(request, &query));
766 map.insert("path".to_owned(), path);
767 }
768 Method::Get | Method::Create | Method::Update | Method::Delete => {
769 map.insert("path".to_owned(), path);
770 map.insert("transforms".to_owned(), strings(&request.transforms));
771 map.insert("writeFields".to_owned(), strings(&request.write_fields));
772 map.insert("readFields".to_owned(), Val::Null);
773 if method == Method::Create && !request.upsert {
774 map.insert("autoId".to_owned(), Val::Bool(request.auto_id));
775 }
776 let resource = match &request.resource {
777 None => Val::Null,
778 // The document has no name yet.
779 Some(doc) if request.auto_id => match value::resource(doc) {
780 Val::Map(map) => {
781 let data = map.get("data").cloned().unwrap_or(Val::Null);
782 Val::Map(Rc::new(IndexMap::from([("data".to_owned(), data)])))
783 }
784 other => other,
785 },
786 Some(doc) => value::resource(doc),
787 };
788 map.insert("resource".to_owned(), resource);
789 }
790 }
791 map.insert("method".to_owned(), Val::str(method.name()));
792 Val::Map(Rc::new(map))
793}
794
795/// `request.query`, its keys in the order of the official emulator's coverage report.
796fn query_value(request: &Request, query: &Query) -> Val {
797 let all_descendants = query.group.is_some() && !query.children;
798 // The collection ID, and the document holding the collection (or the collections).
799 let (kind, parent) = match &query.group {
800 None => match request.path.split_last() {
801 Some((id, parent)) => (Val::str(id), parent),
802 None => (Val::Null, &[][..]),
803 },
804 Some(id) if id.is_empty() => (Val::Null, request.path.as_slice()),
805 Some(id) => (Val::str(id), request.path.as_slice()),
806 };
807 // `databases/{id}/documents`, the root, is no document.
808 let parent = if parent.len() > 3 {
809 Val::path(parent.to_vec())
810 } else {
811 Val::Null
812 };
813 let order_by = query
814 .order_by
815 .iter()
816 .map(|(field, direction)| (field.clone(), Val::str(direction)))
817 .collect();
818 let mut q = IndexMap::new();
819 q.insert("distinct".to_owned(), Val::Bool(false));
820 q.insert("selectOnlyKeys".to_owned(), Val::Bool(false));
821 q.insert("groupBy".to_owned(), Val::Map(Rc::default()));
822 q.insert("kind".to_owned(), kind);
823 let parent_key = if all_descendants {
824 "ancestor"
825 } else {
826 "parent"
827 };
828 q.insert(parent_key.to_owned(), parent);
829 q.insert("orderBy".to_owned(), Val::Map(Rc::new(order_by)));
830 q.insert("limit".to_owned(), query.limit.map_or(Val::Null, Val::Int));
831 q.insert("offset".to_owned(), Val::Int(query.offset.unwrap_or(0)));
832 q.insert("allDescendants".to_owned(), Val::Bool(all_descendants));
833 Val::Map(Rc::new(q))
834}
835
836/// `request.auth`: null without a token, else `uid` and `token`.
837fn auth_value(request: &Request) -> Val {
838 match &request.auth {
839 None => Val::Null,
840 Some(auth) => {
841 let mut a = IndexMap::new();
842 a.insert(
843 "uid".to_owned(),
844 auth.uid.as_deref().map_or(Val::Null, Val::str),
845 );
846 a.insert(
847 "token".to_owned(),
848 Val::from(&Value::Map(auth.token.clone())),
849 );
850 Val::Map(Rc::new(a))
851 }
852 }
853}