Skip to main content

parse_rust_rest/
query_parse.rs

1//! Parsing the `where` parameter.
2//!
3//! A `where` value is `{"field": <literal>}` for equality, or `{"field": {"$op": <value>}}` for
4//! everything else. An operator document may carry several operators, which is how a range is
5//! expressed. `$or`, `$and`, `$nor` and `$relatedTo` are query-level keys rather than field names.
6//!
7//! **Anything unsupported is an error.** Silently ignoring an operator returns more rows than the
8//! caller asked for, which is an authorization failure rather than a missing feature, and it is
9//! the failure mode this rule exists to prevent. The vocabulary grew at 0.2.0; the rule did not
10//! relax.
11//!
12//! The output is a [`ParsedWhere`] rather than a `Query`, because two constructs cannot be
13//! lowered without reading the database: `$relatedTo` and a constraint on a `Relation`-typed
14//! field are both join-table reads. They stay as nodes here and are resolved by
15//! [`crate::relations`] once a schema and a caller are known.
16
17use std::collections::HashSet;
18
19use parse_rust_core::{classify, ErrorCode, ParseError, ParseValue};
20use parse_rust_storage::{Comparison, Constraint};
21use serde_json::Value as Json;
22
23/// A parsed `where` document: a conjunction of clauses.
24#[derive(Debug, Clone, Default)]
25pub struct ParsedWhere {
26    pub clauses: Vec<ParsedClause>,
27}
28
29/// One element of a parsed `where`.
30#[derive(Debug, Clone)]
31pub enum ParsedClause {
32    Field(Constraint),
33    /// `{"$relatedTo": {"object": <pointer>, "key": <field>}}`. Resolved against the join table
34    /// of the **owning** class, and only after the caller has been authorized to read the owning
35    /// object.
36    RelatedTo {
37        class_name: String,
38        object_id: String,
39        key: String,
40    },
41    Or(Vec<ParsedWhere>),
42    And(Vec<ParsedWhere>),
43    Nor(Vec<ParsedWhere>),
44}
45
46impl ParsedWhere {
47    pub fn is_empty(&self) -> bool {
48        self.clauses.is_empty()
49    }
50
51    /// Every field key named anywhere in the tree, including inside logical clauses.
52    ///
53    /// Used by `denyProtectedFields`, which recurses into `$or`/`$and`/`$nor`
54    /// (`RestQuery.js:956-967`).
55    pub fn field_keys(&self) -> Vec<String> {
56        let mut out = Vec::new();
57        self.collect_field_keys(&mut out);
58        out
59    }
60
61    fn collect_field_keys(&self, out: &mut Vec<String>) {
62        for clause in &self.clauses {
63            match clause {
64                ParsedClause::Field(c) => out.push(c.field.clone()),
65                ParsedClause::RelatedTo { .. } => {}
66                ParsedClause::Or(branches)
67                | ParsedClause::And(branches)
68                | ParsedClause::Nor(branches) => {
69                    for branch in branches {
70                        branch.collect_field_keys(out);
71                    }
72                }
73            }
74        }
75    }
76
77    /// The objectId this query is pinned to, if it is pinned by a top-level equality.
78    ///
79    /// Upstream reads `query.objectId` directly (`DatabaseController.js:1838`), which is a string
80    /// only for the shorthand equality form. Deliberately does not look inside a logical clause:
81    /// inside an `$or` no such pinning exists.
82    pub fn pinned_object_id(&self) -> Option<&str> {
83        self.clauses.iter().find_map(|c| match c {
84            ParsedClause::Field(Constraint {
85                field,
86                comparison: Comparison::Equal(ParseValue::String(id)),
87            }) if field == "objectId" => Some(id.as_str()),
88            _ => None,
89        })
90    }
91
92    pub fn push(&mut self, clause: ParsedClause) {
93        self.clauses.push(clause);
94    }
95}
96
97/// Parse a decoded `where` object.
98pub fn parse_where(where_json: &Json) -> Result<ParsedWhere, ParseError> {
99    parse_where_at(where_json, true)
100}
101
102/// `top_level` carries whether `replaceEquality` applies, which is not a detail worth hiding.
103///
104/// Upstream runs that rewrite as the last stage of `buildRestWhere`, over the keys of `restWhere`
105/// itself (`RestQuery.js:852-858`). A branch inside `$or` is reached through an array, and
106/// `replaceEqualityConstraint` iterates an array's indices, which are never `$`-prefixed, so it
107/// finds no operator keys and returns the array untouched. The objects inside are therefore never
108/// visited. Recursing with this flag unset is what reproduces that: the same mixed constraint
109/// means one thing at the top level and another inside `$or`.
110fn parse_where_at(where_json: &Json, top_level: bool) -> Result<ParsedWhere, ParseError> {
111    let Json::Object(map) = where_json else {
112        return Err(ParseError::invalid_query(
113            "where must be an object".to_string(),
114        ));
115    };
116
117    let mut out = ParsedWhere::default();
118    for (field, value) in map {
119        // `validateQuery` refuses a query on `ACL` outright (`DatabaseController.js:130-132`).
120        // The ACL is stored as `_rperm`/`_wperm` and there is no column to match, so accepting
121        // this would match nothing and look like a legitimate empty result.
122        if field == "ACL" {
123            return Err(ParseError::invalid_query(
124                "Cannot query on ACL.".to_string(),
125            ));
126        }
127
128        if let Some(clause) = parse_query_level_key(field, value)? {
129            out.push(clause);
130            continue;
131        }
132
133        match value {
134            // An operator document, unless it is a tagged Parse value like a Pointer or Date.
135            Json::Object(inner) if is_operator_document(inner) => {
136                for constraint in parse_operators(field, inner)? {
137                    out.push(ParsedClause::Field(constraint));
138                }
139            }
140            // Mixed: some `$` keys and some ordinary ones. Upstream's `replaceEquality` folds the
141            // ordinary keys into a single `$eq` whose value is an object of just those keys, and
142            // leaves the operators alone (`RestQuery.js:828-849`).
143            //
144            // Treating the whole object as a literal instead is the reading that looks obvious and
145            // it is wrong in the direction that matters: `{"foo": 1, "$gt": 0}` would ask for a
146            // column exactly equal to that two-key object, which matches nothing, so the query
147            // returns an empty result rather than an error and nothing says a constraint was
148            // dropped.
149            Json::Object(inner) if top_level && is_mixed_document(inner) => {
150                let mut rewritten = serde_json::Map::new();
151                let mut equal_to = serde_json::Map::new();
152                for (key, v) in inner {
153                    if key.starts_with('$') {
154                        rewritten.insert(key.clone(), v.clone());
155                    } else {
156                        equal_to.insert(key.clone(), v.clone());
157                    }
158                }
159                rewritten.insert("$eq".to_string(), Json::Object(equal_to));
160                for constraint in parse_operators(field, &rewritten)? {
161                    out.push(ParsedClause::Field(constraint));
162                }
163            }
164            // Shorthand equality. Kept **raw**, like every other operand: see the note in
165            // `parse_operators`.
166            literal => out.push(ParsedClause::Field(Constraint {
167                field: field.clone(),
168                comparison: Comparison::Equal(parse_rust_core::classify_raw(literal.clone())?),
169            })),
170        }
171    }
172    Ok(out)
173}
174
175/// `$or`, `$and`, `$nor` and `$relatedTo` at the top level of a where document.
176///
177/// Returns `Ok(None)` when the key is an ordinary field name, and an error for a `$`-prefixed key
178/// that is not one of the four. Treating an unknown one as a field name would match nothing and
179/// look like an empty result rather than an unsupported query.
180fn parse_query_level_key(field: &str, value: &Json) -> Result<Option<ParsedClause>, ParseError> {
181    if !field.starts_with('$') {
182        return Ok(None);
183    }
184    let clause = match field {
185        "$or" | "$and" | "$nor" => {
186            let branches = match value {
187                Json::Array(items) => items
188                    .iter()
189                    .map(|item| parse_where_at(item, false))
190                    .collect::<Result<Vec<_>, _>>()?,
191                // Upstream's messages, `Bad $or format - use an array value.` and the `$nor`
192                // variant naming a minimum of one element (`DatabaseController.js:134-159`).
193                _ => {
194                    return Err(ParseError::invalid_query(if field == "$nor" {
195                        "Bad $nor format - use an array of at least 1 value.".to_string()
196                    } else {
197                        format!("Bad {field} format - use an array value.")
198                    }))
199                }
200            };
201            if field == "$nor" && branches.is_empty() {
202                return Err(ParseError::invalid_query(
203                    "Bad $nor format - use an array of at least 1 value.".to_string(),
204                ));
205            }
206            match field {
207                "$or" => ParsedClause::Or(branches),
208                "$and" => ParsedClause::And(branches),
209                _ => ParsedClause::Nor(branches),
210            }
211        }
212        "$relatedTo" => parse_related_to(value)?,
213        other => {
214            return Err(ParseError::invalid_query(format!(
215                "unsupported query operator: {other}"
216            )))
217        }
218    };
219    Ok(Some(clause))
220}
221
222fn parse_related_to(value: &Json) -> Result<ParsedClause, ParseError> {
223    let bad = || ParseError::invalid_query("improper usage of $relatedTo".to_string());
224    let Json::Object(map) = value else {
225        return Err(bad());
226    };
227    let key = match map.get("key") {
228        Some(Json::String(k)) => k.clone(),
229        _ => return Err(bad()),
230    };
231    let object = map.get("object").ok_or_else(bad)?;
232    match classify(object.clone())? {
233        ParseValue::Pointer {
234            class_name,
235            object_id,
236        } => Ok(ParsedClause::RelatedTo {
237            class_name,
238            object_id,
239            key,
240        }),
241        _ => Err(bad()),
242    }
243}
244
245/// Turn one operator document into constraints.
246///
247/// `$regex` and `$options` are two keys producing one comparison, which is why this is not a
248/// straight map over the entries. Upstream keeps them separate all the way down and relies on
249/// reverse-alphabetical key iteration so `$regex` is handled before `$options`
250/// (`MongoTransform.js:670-675`); folding them here removes the ordering dependency.
251fn parse_operators(
252    field: &str,
253    inner: &serde_json::Map<String, Json>,
254) -> Result<Vec<Constraint>, ParseError> {
255    let mut out = Vec::new();
256
257    let regex = inner.get("$regex");
258    let options = inner.get("$options");
259    if regex.is_none() && options.is_some() {
260        // A lone `$options` is meaningless. Accepting it would drop the caller's intent
261        // silently.
262        return Err(ParseError::invalid_query(
263            "$options is only valid with $regex".to_string(),
264        ));
265    }
266    if let Some(regex) = regex {
267        let Json::String(pattern) = regex else {
268            return Err(ParseError::invalid_query(
269                "$regex value must be a string".to_string(),
270            ));
271        };
272        let options = match options {
273            None => None,
274            Some(Json::String(o)) => {
275                if !o.chars().all(|c| matches!(c, 'i' | 'm' | 'x' | 's' | 'u')) || o.is_empty() {
276                    return Err(ParseError::invalid_query(format!(
277                        "Bad $options value for query: {o}"
278                    )));
279                }
280                Some(o.clone())
281            }
282            Some(_) => {
283                return Err(ParseError::invalid_query(
284                    "$options value must be a string".to_string(),
285                ))
286            }
287        };
288        out.push(Constraint {
289            field: field.to_string(),
290            comparison: Comparison::Regex {
291                pattern: pattern.clone(),
292                options,
293            },
294        });
295    }
296
297    for (op, operand) in inner {
298        if op == "$regex" || op == "$options" {
299            continue;
300        }
301        // **A query operand is compared, not stored, so it keeps what the client sent, and the
302        // parser does not interpret a single `__type` envelope.**
303        //
304        // Two separate reasons, and the second is the one that decides where the work happens.
305        //
306        // Decoding an operand all the way down drops an unknown key inside a nested envelope, so
307        // the operand compares equal to a row upstream would not return: upstream reconstructs a
308        // recognized atom and leaves a plain object alone, and the nested case is the plain-object
309        // one.
310        //
311        // Recognizing only the top would fix that, and it still cannot be done here, because
312        // **which envelopes count is a property of the field** (`MongoTransform.js:655-662`) and
313        // this function has no schema. A constraint operand on an `Array` field takes the interior
314        // list of three tags; the same operand on a `GeoPoint` field takes the top-level list of
315        // all of them. Choosing either one here is wrong for the other, in opposite directions.
316        // So the operand stays raw and `parse-rust-mongo` recognizes it against the field, which is
317        // where upstream decides too.
318        let operand = parse_rust_core::classify_raw(operand.clone())?;
319        out.push(Constraint {
320            field: field.to_string(),
321            comparison: Comparison::from_operator(op, operand)?,
322        });
323    }
324    Ok(out)
325}
326
327/// Is this object a set of `$` operators rather than a literal value?
328///
329/// The distinction matters because `{"__type":"Pointer",...}` is a literal and `{"$gt":3}` is
330/// not. Upstream decides the same way: it looks for `$`-prefixed keys.
331fn is_operator_document(map: &serde_json::Map<String, Json>) -> bool {
332    !map.is_empty() && map.keys().all(|k| k.starts_with('$'))
333}
334
335/// Both kinds of key present, which is what `replaceEquality` acts on.
336///
337/// Neither `{"$gt": 0}` nor `{"__type": "Pointer", ...}` qualifies: the rewrite needs one of each,
338/// which is exactly upstream's `hasDirectConstraint && hasOperatorConstraint`.
339fn is_mixed_document(map: &serde_json::Map<String, Json>) -> bool {
340    map.keys().any(|k| k.starts_with('$')) && map.keys().any(|k| !k.starts_with('$'))
341}
342
343/// Internal columns a **client** may name in a query (`clientRead`,
344/// `DatabaseController.js:31-44`).
345///
346/// Only these two, and they are readable because a query on `_rperm` is how a client asks "which
347/// rows can I see". Everything else internal is refused, and the refusal is load bearing: without
348/// it a client can name `_hashed_password` in a `$regex` and recover a bcrypt hash one character
349/// at a time.
350pub const CLIENT_QUERYABLE_INTERNAL_FIELDS: [&str; 2] = ["_rperm", "_wperm"];
351
352/// Internal columns the **master key** may additionally name (`masterRead`).
353///
354/// Note what is absent from both lists: `_hashed_password` is `masterRead: false`, so not even
355/// master may query it.
356pub const MASTER_QUERYABLE_INTERNAL_FIELDS: [&str; 10] = [
357    "_email_verify_token",
358    "_perishable_token",
359    "_perishable_token_expires_at",
360    "_email_verify_token_expires_at",
361    "_failed_login_count",
362    "_account_lockout_expires_at",
363    "_password_changed_at",
364    "_password_history",
365    "_tombstone",
366    "_session_token",
367];
368
369/// The key-name half of `validateQuery` (`DatabaseController.js:161-188`).
370///
371/// A key must match `^[a-zA-Z][a-zA-Z0-9_\.]*$` or be one of the internal columns the caller's
372/// authority may name. `$relatedTo` is deliberately not in either list, and does not need to be:
373/// upstream deletes it from the query before this runs (`DatabaseController.js:1208`), and here it
374/// has already been resolved into an `objectId` constraint by the time the check happens.
375pub fn validate_query_keys(where_: &ParsedWhere, is_master: bool) -> Result<(), ParseError> {
376    for key in where_.field_keys() {
377        if matches_query_key_regex(&key)
378            || CLIENT_QUERYABLE_INTERNAL_FIELDS.contains(&key.as_str())
379            || (is_master && MASTER_QUERYABLE_INTERNAL_FIELDS.contains(&key.as_str()))
380        {
381            continue;
382        }
383        return Err(ParseError::invalid_key_name(format!(
384            "Invalid key name: {key}"
385        )));
386    }
387    Ok(())
388}
389
390/// `^[a-zA-Z][a-zA-Z0-9_\.]*$`.
391fn matches_query_key_regex(key: &str) -> bool {
392    let mut chars = key.chars();
393    match chars.next() {
394        Some(c) if c.is_ascii_alphabetic() => {}
395        _ => return false,
396    }
397    chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '.')
398}
399
400/// The deepest pointer chain an `include` may name. See [`parse_include`].
401pub const MAX_INCLUDE_DEPTH: usize = 20;
402
403/// The most distinct include paths one request may produce, counting expanded prefixes.
404/// See [`parse_include`].
405pub const MAX_INCLUDE_PATHS: usize = 500;
406
407/// Parse the `include` parameter into paths, every prefix materialized and sorted by depth.
408///
409/// `a.b.c` yields `a`, `a.b`, `a.b.c`, sorted so a parent resolves before its child
410/// (`RestQuery.js:241-257`).
411///
412/// **`include=*` is out of scope for 0.2.0 and is an explicit error**, not a silently
413/// unexpanded response. `includeAll` requires walking every Pointer and Array field of every
414/// result class, and returning bare pointers where a client asked for objects is the kind of
415/// difference an SDK turns into a null dereference rather than an error.
416///
417/// **The two limits are a deliberate difference from upstream's defaults, not from upstream.**
418/// The pin has `requestComplexity.includeDepth` and `requestComplexity.includeCount`
419/// (`Options/Definitions.js:751-762`), and **both default to `-1`, meaning unbounded**, with master
420/// and maintenance exempt. So upstream ships the unbounded configuration, which is the denial of
421/// service described below; these limits are fixed and always on instead. Tier 2 under the security
422/// carve-out, recorded with its blast radius.
423///
424/// Expanding every prefix means an `include` of *n* components produces *n* paths whose combined
425/// component count is n(n+1)/2, and every one of those paths becomes at least one further query in
426/// `expand_includes`. That is why the parameter is bounded before it is expanded rather than after,
427/// and why the dedupe borrows from the original string: both keep the work linear in the length of
428/// the input.
429///
430/// The values are not upstream's, which has no non-negative default to copy. They are set where a
431/// real `include` stops and a hostile one begins: a pointer chain deeper than [`MAX_INCLUDE_DEPTH`] is already
432/// beyond anything an SDK generates, and [`MAX_INCLUDE_PATHS`] distinct paths is more than a wide
433/// class has fields. A client over either limit is refused by name rather than truncated, because
434/// silently dropping an include returns bare pointers where objects were asked for.
435pub fn parse_include(include: &str) -> Result<Vec<Vec<String>>, ParseError> {
436    let mut paths: Vec<Vec<String>> = Vec::new();
437    let mut seen: HashSet<&str> = HashSet::new();
438    for raw in include.split(',').map(str::trim).filter(|s| !s.is_empty()) {
439        if raw == "*" {
440            return Err(ParseError::new(
441                ErrorCode::CommandUnavailable,
442                "include=* is not supported yet.",
443            ));
444        }
445        let parts: Vec<&str> = raw.split('.').collect();
446        if parts.len() > MAX_INCLUDE_DEPTH {
447            return Err(ParseError::invalid_query(format!(
448                "include path is too deep: at most {MAX_INCLUDE_DEPTH} components."
449            )));
450        }
451        for depth in 1..=parts.len() {
452            // Dedupe on a borrowed slice of the original string rather than on an owned
453            // `Vec<String>`. The prefix `a.b` is already a substring of `a.b.c`, so its end offset
454            // is the start of the next separator and no allocation is needed to recognise it.
455            let end = parts[..depth].iter().map(|p| p.len()).sum::<usize>() + depth - 1;
456            if !seen.insert(&raw[..end]) {
457                continue;
458            }
459            if paths.len() == MAX_INCLUDE_PATHS {
460                return Err(ParseError::invalid_query(format!(
461                    "too many include paths: at most {MAX_INCLUDE_PATHS}."
462                )));
463            }
464            paths.push(parts[..depth].iter().map(|s| s.to_string()).collect());
465        }
466    }
467    // Stable, so paths of one depth keep the order the client asked for them in.
468    paths.sort_by_key(Vec::len);
469    Ok(paths)
470}
471
472#[cfg(test)]
473mod tests {
474    use super::*;
475
476    fn j(s: &str) -> Json {
477        serde_json::from_str(s).expect("test literal")
478    }
479
480    fn fields(w: &ParsedWhere) -> Vec<&Constraint> {
481        w.clauses
482            .iter()
483            .filter_map(|c| match c {
484                ParsedClause::Field(f) => Some(f),
485                _ => None,
486            })
487            .collect()
488    }
489
490    #[test]
491    fn a_bare_value_is_equality() {
492        let w = parse_where(&j(r#"{"title":"hello"}"#)).expect("parse");
493        let c = fields(&w);
494        assert_eq!(c.len(), 1);
495        assert_eq!(c[0].field, "title");
496        assert!(matches!(
497            &c[0].comparison,
498            Comparison::Equal(ParseValue::String(s)) if s == "hello"
499        ));
500    }
501
502    #[test]
503    fn several_operators_on_one_field_become_several_constraints() {
504        let w = parse_where(&j(r#"{"views":{"$gt":1,"$lt":9}}"#)).expect("parse");
505        assert_eq!(fields(&w).len(), 2);
506        assert!(fields(&w).iter().all(|x| x.field == "views"));
507    }
508
509    /// A tagged value is a literal rather than an operator document, and it reaches the backend
510    /// **undecoded**.
511    ///
512    /// Both halves are the assertion. The parser must not read `__type` as a constraint, and it
513    /// must not read it as an envelope either: which envelopes count depends on the field's type,
514    /// which this layer does not know. Recognition belongs to the backend and is asserted there
515    /// (`parse-rust-mongo`'s `the_atom_list_is_chosen_by_the_field_not_by_the_parser`).
516    #[test]
517    fn a_tagged_value_is_a_literal_and_reaches_the_backend_undecoded() {
518        let w = parse_where(&j(
519            r#"{"author":{"__type":"Pointer","className":"_User","objectId":"u1","extra":7}}"#,
520        ))
521        .expect("parse");
522        let Comparison::Equal(ParseValue::Object(map)) = &fields(&w)[0].comparison else {
523            panic!("expected a raw object operand, got {:?}", fields(&w)[0]);
524        };
525        // Every key the client sent, including the one no envelope declares. Decoding here would
526        // drop `extra`, and for a field type that takes the interior transform upstream compares
527        // it.
528        assert_eq!(map.len(), 4, "{map:?}");
529        assert!(matches!(map.get("objectId"), Some(ParseValue::String(s)) if s == "u1"));
530        assert!(matches!(map.get("extra"), Some(ParseValue::Number(n)) if *n == 7.0));
531    }
532
533    /// The rule that stops a dropped constraint from broadening a result set. The list shrank at
534    /// 0.2.0; what must not change is that the remainder error and that the message names the
535    /// operator.
536    #[test]
537    fn an_unsupported_operator_is_refused() {
538        for op in [
539            "$inQuery",
540            "$notInQuery",
541            "$select",
542            "$dontSelect",
543            "$text",
544            "$nearSphere",
545            "$containedBy",
546            "$geoWithin",
547        ] {
548            let src = format!(r#"{{"title":{{"{op}":1}}}}"#);
549            let e = parse_where(&j(&src)).unwrap_err();
550            assert_eq!(e.code, ErrorCode::InvalidQuery, "{op}");
551            assert!(e.message.contains(op), "{op}: {}", e.message);
552        }
553    }
554
555    #[test]
556    fn an_unknown_query_level_operator_is_refused() {
557        let e = parse_where(&j(r#"{"$nope":[]}"#)).unwrap_err();
558        assert_eq!(e.code, ErrorCode::InvalidQuery);
559        assert!(e.message.contains("$nope"));
560    }
561
562    #[test]
563    fn logical_operators_parse_recursively() {
564        let w = parse_where(&j(
565            r#"{"$or":[{"a":1},{"$and":[{"b":2},{"c":3}]}],"$nor":[{"d":4}]}"#,
566        ))
567        .expect("parse");
568        assert_eq!(w.clauses.len(), 2);
569        match &w.clauses[0] {
570            ParsedClause::Or(branches) => {
571                assert_eq!(branches.len(), 2);
572                assert!(matches!(branches[1].clauses[0], ParsedClause::And(_)));
573            }
574            other => panic!("expected Or, got {other:?}"),
575        }
576        assert!(matches!(w.clauses[1], ParsedClause::Nor(_)));
577    }
578
579    #[test]
580    fn a_non_array_logical_operator_is_invalid() {
581        for src in [r#"{"$or":{"a":1}}"#, r#"{"$and":3}"#, r#"{"$nor":[]}"#] {
582            let e = parse_where(&j(src)).unwrap_err();
583            assert_eq!(e.code, ErrorCode::InvalidQuery, "{src}");
584        }
585    }
586
587    #[test]
588    fn regex_folds_its_options_in() {
589        let w = parse_where(&j(r#"{"title":{"$regex":"^a","$options":"im"}}"#)).expect("parse");
590        let c = fields(&w);
591        assert_eq!(c.len(), 1, "two keys make one comparison");
592        assert!(matches!(
593            &c[0].comparison,
594            Comparison::Regex { pattern, options } if pattern == "^a" && options.as_deref() == Some("im")
595        ));
596    }
597
598    #[test]
599    fn regex_rejects_a_non_string_pattern_and_bad_options() {
600        assert!(parse_where(&j(r#"{"title":{"$regex":3}}"#)).is_err());
601        let e = parse_where(&j(r#"{"title":{"$regex":"a","$options":"z"}}"#)).unwrap_err();
602        assert_eq!(e.code, ErrorCode::InvalidQuery);
603        assert!(e.message.contains("Bad $options value for query: z"));
604        assert!(parse_where(&j(r#"{"title":{"$options":"i"}}"#)).is_err());
605    }
606
607    #[test]
608    fn all_parses() {
609        let w = parse_where(&j(r#"{"tags":{"$all":["a","b"]}}"#)).expect("parse");
610        assert!(matches!(&fields(&w)[0].comparison, Comparison::All(v) if v.len() == 2));
611    }
612
613    #[test]
614    fn related_to_parses_into_its_own_clause() {
615        let w = parse_where(&j(
616            r#"{"$relatedTo":{"object":{"__type":"Pointer","className":"_Role","objectId":"r1"},"key":"users"}}"#,
617        ))
618        .expect("parse");
619        match &w.clauses[0] {
620            ParsedClause::RelatedTo {
621                class_name,
622                object_id,
623                key,
624            } => {
625                assert_eq!(class_name, "_Role");
626                assert_eq!(object_id, "r1");
627                assert_eq!(key, "users");
628            }
629            other => panic!("expected RelatedTo, got {other:?}"),
630        }
631        assert!(parse_where(&j(r#"{"$relatedTo":{"key":"users"}}"#)).is_err());
632        assert!(parse_where(&j(r#"{"$relatedTo":{"object":3,"key":"u"}}"#)).is_err());
633    }
634
635    #[test]
636    fn querying_on_acl_is_refused() {
637        let e = parse_where(&j(r#"{"ACL":{"*":{"read":true}}}"#)).unwrap_err();
638        assert_eq!(e.code, ErrorCode::InvalidQuery);
639        assert_eq!(e.message, "Cannot query on ACL.");
640    }
641
642    #[test]
643    fn in_and_exists_parse() {
644        let w =
645            parse_where(&j(r#"{"tag":{"$in":["a","b"]},"x":{"$exists":true}}"#)).expect("parse");
646        assert_eq!(fields(&w).len(), 2);
647    }
648
649    #[test]
650    fn an_empty_object_is_an_empty_query_not_an_operator_document() {
651        assert!(parse_where(&j("{}")).expect("parse").is_empty());
652        let w = parse_where(&j(r#"{"meta":{}}"#)).expect("parse");
653        assert!(matches!(
654            &fields(&w)[0].comparison,
655            Comparison::Equal(ParseValue::Object(_))
656        ));
657    }
658
659    #[test]
660    fn where_must_be_an_object() {
661        assert!(parse_where(&j("[]")).is_err());
662        assert!(parse_where(&j("3")).is_err());
663    }
664
665    #[test]
666    fn field_keys_reach_into_logical_clauses() {
667        let w = parse_where(&j(r#"{"a":1,"$or":[{"b":2},{"$and":[{"c":3}]}]}"#)).expect("parse");
668        let mut keys = w.field_keys();
669        keys.sort();
670        assert_eq!(keys, vec!["a", "b", "c"]);
671    }
672
673    #[test]
674    fn pinned_object_id_only_reads_a_top_level_equality() {
675        let w = parse_where(&j(r#"{"objectId":"abc"}"#)).expect("parse");
676        assert_eq!(w.pinned_object_id(), Some("abc"));
677        let w = parse_where(&j(r#"{"$or":[{"objectId":"abc"}]}"#)).expect("parse");
678        assert_eq!(w.pinned_object_id(), None);
679    }
680
681    #[test]
682    fn include_paths_materialize_prefixes_and_sort_by_depth() {
683        assert_eq!(
684            parse_include("a.b.c,d").expect("parse"),
685            vec![
686                vec!["a".to_string()],
687                vec!["d".to_string()],
688                vec!["a".to_string(), "b".to_string()],
689                vec!["a".to_string(), "b".to_string(), "c".to_string()],
690            ]
691        );
692        assert!(parse_include("").expect("parse").is_empty());
693    }
694
695    /// The bound holds, and holds cheaply.
696    ///
697    /// Two assertions in one: an over-limit input is *refused* rather than truncated, and it is
698    /// refused **before** expansion. The second is what the running time proves: this test finishes
699    /// in the time a short string takes, which it could not if a 24,000-component path were
700    /// expanded first.
701    #[test]
702    fn a_hostile_include_is_refused_rather_than_expanded() {
703        let deep = vec!["a"; MAX_INCLUDE_DEPTH + 1].join(".");
704        let err = parse_include(&deep).expect_err("over the depth limit");
705        assert_eq!(err.code, ErrorCode::InvalidQuery);
706
707        // What the measured 2.7 GB allocation came from: thousands of components in one path.
708        let huge = vec!["a"; 24_000].join(".");
709        assert_eq!(
710            parse_include(&huge).expect_err("over the depth limit").code,
711            ErrorCode::InvalidQuery
712        );
713
714        // Many shallow paths hit the path cap instead of the depth cap.
715        let wide = (0..MAX_INCLUDE_PATHS + 1)
716            .map(|i| format!("f{i}"))
717            .collect::<Vec<_>>()
718            .join(",");
719        assert_eq!(
720            parse_include(&wide).expect_err("over the path limit").code,
721            ErrorCode::InvalidQuery
722        );
723
724        // The control: something a real client sends is still accepted, so the limits refuse
725        // hostile input rather than ordinary input.
726        assert_eq!(
727            parse_include("author.company.owner").expect("parse").len(),
728            3
729        );
730        let at_depth = vec!["a"; MAX_INCLUDE_DEPTH].join(".");
731        assert_eq!(
732            parse_include(&at_depth)
733                .expect("exactly at the limit")
734                .len(),
735            MAX_INCLUDE_DEPTH
736        );
737    }
738
739    #[test]
740    fn a_client_cannot_name_an_internal_column_in_a_query() {
741        // The one that matters: a `$regex` on the password hash would recover it a character at a
742        // time.
743        let w = parse_where(&j(r#"{"_hashed_password":{"$regex":"^a"}}"#)).expect("parse");
744        for is_master in [false, true] {
745            let e = validate_query_keys(&w, is_master).unwrap_err();
746            assert_eq!(e.code, ErrorCode::InvalidKeyName);
747            assert_eq!(e.message, "Invalid key name: _hashed_password");
748        }
749
750        // `_rperm` is queryable by anyone, which is how a client asks what it can see.
751        let w = parse_where(&j(r#"{"_rperm":"u1"}"#)).expect("parse");
752        assert!(validate_query_keys(&w, false).is_ok());
753
754        // A session token is master-only.
755        let w = parse_where(&j(r#"{"_session_token":"r:t"}"#)).expect("parse");
756        assert!(validate_query_keys(&w, false).is_err());
757        assert!(validate_query_keys(&w, true).is_ok());
758
759        // Nested inside a logical clause, where it would otherwise slip past.
760        let w = parse_where(&j(r#"{"$or":[{"_session_token":"r:t"}]}"#)).expect("parse");
761        assert!(validate_query_keys(&w, false).is_err());
762    }
763
764    #[test]
765    fn ordinary_and_dotted_field_names_pass() {
766        for src in [r#"{"title":"x"}"#, r#"{"meta.a_b":1}"#] {
767            let w = parse_where(&j(src)).expect("parse");
768            assert!(validate_query_keys(&w, false).is_ok(), "{src}");
769        }
770        // A leading digit is not a legal field name.
771        let w = parse_where(&j(r#"{"1bad":1}"#)).expect("parse");
772        assert!(validate_query_keys(&w, false).is_err());
773    }
774
775    #[test]
776    fn include_all_is_an_explicit_error() {
777        let e = parse_include("*").unwrap_err();
778        assert_eq!(e.code, ErrorCode::CommandUnavailable);
779        assert!(e.message.contains("include=*"));
780    }
781}
782
783#[cfg(test)]
784mod mixed_constraint_tests {
785    use super::*;
786
787    fn parse(json: &str) -> Result<ParsedWhere, ParseError> {
788        parse_where(&serde_json::from_str(json).expect("test literal"))
789    }
790
791    /// `replaceEquality`: the ordinary keys become one `$eq` object, the operators survive.
792    #[test]
793    fn a_mixed_constraint_becomes_an_eq_plus_the_operators() {
794        let parsed = parse(r#"{"meta": {"foo": 1, "$gt": 0}}"#).expect("parses");
795        let mut equals = 0;
796        let mut greater = 0;
797        for clause in &parsed.clauses {
798            let ParsedClause::Field(c) = clause else {
799                panic!("expected field clauses")
800            };
801            assert_eq!(c.field, "meta");
802            match &c.comparison {
803                // The `$eq` value is an object of just the non-operator keys, not the whole
804                // submitted document.
805                Comparison::EqualOperator(ParseValue::Object(map)) => {
806                    assert_eq!(map.len(), 1, "only the direct keys: {map:?}");
807                    assert!(map.contains_key("foo"));
808                    equals += 1;
809                }
810                Comparison::GreaterThan(_) => greater += 1,
811                other => panic!("unexpected comparison: {other:?}"),
812            }
813        }
814        assert_eq!((equals, greater), (1, 1));
815    }
816
817    /// The whole point: the constraint must not collapse into equality against the submitted
818    /// object, which is what matches nothing while returning 200.
819    #[test]
820    fn a_mixed_constraint_is_not_one_literal_equality() {
821        let parsed = parse(r#"{"meta": {"foo": 1, "$gt": 0}}"#).expect("parses");
822        assert_eq!(
823            parsed.clauses.len(),
824            2,
825            "one clause means it was read as a literal"
826        );
827    }
828
829    /// All-operator and all-literal documents are untouched by the rewrite.
830    #[test]
831    fn unmixed_documents_are_unchanged() {
832        let ops = parse(r#"{"n": {"$gt": 0, "$lt": 9}}"#).expect("parses");
833        assert_eq!(ops.clauses.len(), 2);
834
835        // A tagged Parse value has no `$` key, so it stays a single literal equality.
836        let literal = parse(r#"{"p": {"__type": "Pointer", "className": "C", "objectId": "x"}}"#)
837            .expect("parses");
838        assert_eq!(literal.clauses.len(), 1);
839    }
840
841    /// Upstream applies the rewrite to the keys of `restWhere` itself. A branch of `$or` is
842    /// reached through an array, whose indices are never `$`-prefixed, so
843    /// `replaceEqualityConstraint` finds no operator key and returns it untouched. The same
844    /// constraint therefore means different things at the two depths, and reproducing that is the
845    /// whole reason the recursion carries a flag.
846    #[test]
847    fn the_rewrite_does_not_reach_inside_or() {
848        let parsed = parse(r#"{"$or": [{"meta": {"foo": 1, "$gt": 0}}]}"#).expect("parses");
849        let [ParsedClause::Or(branches)] = parsed.clauses.as_slice() else {
850            panic!("expected one $or clause")
851        };
852        assert_eq!(branches.len(), 1);
853        assert_eq!(
854            branches[0].clauses.len(),
855            1,
856            "inside $or the mixed document stays one literal equality"
857        );
858    }
859
860    /// An explicit `$eq` from a client, which upstream accepts and parse-rust used to refuse.
861    #[test]
862    fn an_explicit_eq_operator_is_accepted() {
863        let parsed = parse(r#"{"n": {"$eq": 5}}"#).expect("parses");
864        assert_eq!(parsed.clauses.len(), 1);
865        let ParsedClause::Field(c) = &parsed.clauses[0] else {
866            panic!("expected a field clause")
867        };
868        assert!(matches!(c.comparison, Comparison::EqualOperator(_)));
869    }
870}