Skip to main content

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