Skip to main content

guard_core_engine/
json_walk.rs

1//! Ordered JSON body walk, ported from the Go engine's `jsonwalk.go`, which
2//! ports `guard_core/_utils/body_json_scan.py` and `embedded_json_scan.py`.
3//!
4//! The reference engine parses with `json.loads` (dict insertion order) and
5//! walks the tree depth-first in insertion order: for each object entry the
6//! key is checked against the mongo-operator-key registry (a direct `nosql`
7//! hit reported without a pattern scan) and scanned as a name; then the entry
8//! value descends. Objects and arrays deeper than the JSON depth cap are
9//! serialized back to compact JSON and scanned as one text value. Scalar
10//! leaves scan `str(value)`.
11//!
12//! Values parsed out of a form or multipart field string walk with the field
13//! context plus the [`crate::detect::EMBEDDED_JSON_LEAF_CONTEXT_SUFFIX`]
14//! (`embedded_json_scan.py`), and such a leaf string that itself parses as a
15//! JSON object or array walks again with another suffix, exactly like
16//! `_check_embedded_json` short-circuiting the raw value scan. The top-level
17//! JSON body walk keeps the plain `request_body` context and never re-parses
18//! leaf strings (the reference skips the embedded check when the context is
19//! exactly `request_body`).
20//!
21//! Deviations: number leaves scan the literal JSON text instead of Python's
22//! float `repr` (Python's arbitrary-precision ints render identically); a
23//! lone surrogate escape becomes U+FFFD at the decode boundary (the engine
24//! string model) instead of Python's lone surrogate rune; parse recursion is
25//! bounded like Python's `json.loads` recursion limit, so deeper bodies fall
26//! back to the blob scan exactly as a `RecursionError` does in the reference.
27
28use std::sync::LazyLock;
29
30use regex::Regex;
31
32use crate::body_scan::{BodyScanValue, REQUEST_BODY_CONTEXT};
33use crate::detect::EMBEDDED_JSON_LEAF_CONTEXT_SUFFIX;
34
35/// JSON walk depth cap (`_json_depth_cap_value` default): containers at or
36/// beyond this walk depth serialize back to compact JSON and scan as text.
37pub const JSON_WALK_DEPTH_CAP: usize = 32;
38
39/// Parse recursion bound standing in for Python's `json.loads`
40/// `RecursionError`, which falls back to the blob scan.
41const PARSE_DEPTH_LIMIT: usize = 1000;
42
43/// One scalar leaf, storing the JSON literal where scanning needs it
44/// (see [`Scalar::scan_text`]).
45#[derive(Debug, Clone, PartialEq)]
46enum Scalar {
47    Str(String),
48    /// The raw JSON number literal.
49    Number(String),
50    Bool(bool),
51    Null,
52    Nan,
53    Infinity,
54    NegInfinity,
55}
56
57impl Scalar {
58    /// Python `str(value)` rendering, the text the reference scans.
59    fn scan_text(&self) -> String {
60        match self {
61            Self::Str(s) | Self::Number(s) => s.clone(),
62            Self::Bool(true) => "True".to_owned(),
63            Self::Bool(false) => "False".to_owned(),
64            Self::Null => "None".to_owned(),
65            Self::Nan => "nan".to_owned(),
66            Self::Infinity => "inf".to_owned(),
67            Self::NegInfinity => "-inf".to_owned(),
68        }
69    }
70}
71
72/// One node of the ordered parse tree. Duplicate object keys keep the first
73/// insertion position and the last value (Python dict semantics).
74#[derive(Debug, Clone, PartialEq)]
75pub struct JsonNode {
76    is_object: bool,
77    is_array: bool,
78    keys: Vec<String>,
79    values: Vec<Self>,
80    items: Vec<Self>,
81    scalar: Option<Scalar>,
82}
83
84impl JsonNode {
85    const fn container(is_object: bool) -> Self {
86        Self {
87            is_object,
88            is_array: !is_object,
89            keys: Vec::new(),
90            values: Vec::new(),
91            items: Vec::new(),
92            scalar: None,
93        }
94    }
95
96    const fn leaf(scalar: Scalar) -> Self {
97        Self {
98            is_object: false,
99            is_array: false,
100            keys: Vec::new(),
101            values: Vec::new(),
102            items: Vec::new(),
103            scalar: Some(scalar),
104        }
105    }
106
107    fn object(keys: Vec<String>, values: Vec<Self>) -> Self {
108        Self {
109            keys,
110            values,
111            ..Self::container(true)
112        }
113    }
114
115    fn array(items: Vec<Self>) -> Self {
116        Self {
117            items,
118            ..Self::container(false)
119        }
120    }
121}
122
123#[derive(Debug)]
124enum ParseError {
125    /// Malformed JSON (`json.JSONDecodeError`).
126    Invalid,
127    /// Beyond the recursion bound (`RecursionError` in the reference).
128    Depth,
129}
130
131/// `parseOrderedJSON`: parse `s` and return the root when it is a JSON
132/// object or array.
133///
134/// Trailing data or any parse failure returns `None`, like the reference
135/// falling through to the blob or raw-value scan (`_scan_json_content` and
136/// `_check_embedded_json` only walk dict/list).
137#[must_use]
138pub fn parse_ordered_json(s: &str) -> Option<JsonNode> {
139    let mut parser = Parser {
140        bytes: s.as_bytes(),
141        pos: 0,
142        stack: Vec::new(),
143        depth: 0,
144        pending: None,
145    };
146    let root = parser.parse_root().ok()?;
147    // _scan_json_content and _check_embedded_json only walk dict/list.
148    if !root.is_object && !root.is_array {
149        return None;
150    }
151    Some(root)
152}
153
154/// The flattened parser: `bytes`/`pos` carry the cursor, the frame stack the
155/// open containers, `depth` the container nesting, and `pending` the last
156/// completed node waiting to attach to its parent.
157struct Parser<'a> {
158    bytes: &'a [u8],
159    pos: usize,
160    stack: Vec<ParseFrame>,
161    depth: usize,
162    pending: Option<JsonNode>,
163}
164
165/// The next machine step: continue with a state, or finish with the root.
166enum Flow {
167    Next(ParseState),
168    Done(JsonNode),
169}
170
171/// Duplicate keys keep the first insertion position and the last value
172/// (Python dict semantics).
173fn upsert_object(keys: &mut Vec<String>, values: &mut Vec<JsonNode>, key: String, node: JsonNode) {
174    if let Some(at) = keys.iter().position(|existing| *existing == key) {
175        values[at] = node;
176    } else {
177        keys.push(key);
178        values.push(node);
179    }
180}
181
182/// Explicit parse-frame stack entry: the recursive descent of the reference
183/// `json.loads`, flattened so the depth bound is a semantic cap rather than a
184/// stack-overflow guard.
185enum ParseFrame {
186    Array(Vec<JsonNode>),
187    Object {
188        keys: Vec<String>,
189        values: Vec<JsonNode>,
190        current_key: Option<String>,
191    },
192}
193
194enum ParseState {
195    /// Parse one value (leaf, or open a container).
196    Value,
197    /// Parse an object key (or the closing brace in the caller).
198    Key,
199    /// A completed node is waiting to attach to its parent.
200    Attach,
201}
202
203fn frame_into_node(frame: ParseFrame) -> JsonNode {
204    match frame {
205        ParseFrame::Array(items) => JsonNode::array(items),
206        ParseFrame::Object { keys, values, .. } => JsonNode::object(keys, values),
207    }
208}
209
210impl Parser<'_> {
211    const fn peek(&self) -> Option<u8> {
212        if self.pos < self.bytes.len() {
213            Some(self.bytes[self.pos])
214        } else {
215            None
216        }
217    }
218
219    const fn bump(&mut self) {
220        self.pos += 1;
221    }
222
223    const fn skip_ws(&mut self) {
224        while matches!(self.peek(), Some(b' ' | b'\t' | b'\n' | b'\r')) {
225            self.pos += 1;
226        }
227    }
228
229    fn literal(&mut self, word: &str) -> bool {
230        if self.bytes[self.pos..].starts_with(word.as_bytes()) {
231            self.pos += word.len();
232            return true;
233        }
234        false
235    }
236
237    fn parse_root(&mut self) -> Result<JsonNode, ParseError> {
238        let mut flow = Flow::Next(ParseState::Value);
239        loop {
240            flow = match flow {
241                Flow::Next(ParseState::Value) => Flow::Next(self.step_value()?),
242                Flow::Next(ParseState::Key) => Flow::Next(self.step_key()?),
243                Flow::Next(ParseState::Attach) => self.step_attach()?,
244                Flow::Done(node) => return Ok(node),
245            };
246        }
247    }
248
249    /// One Value step: open a container or parse a leaf.
250    fn step_value(&mut self) -> Result<ParseState, ParseError> {
251        self.skip_ws();
252        match self.peek() {
253            Some(b'[') => {
254                self.bump();
255                self.open_frame()?;
256                self.skip_ws();
257                if self.peek() == Some(b']') {
258                    self.bump();
259                    self.close_frame();
260                    Ok(ParseState::Attach)
261                } else {
262                    Ok(ParseState::Value)
263                }
264            }
265            Some(b'{') => {
266                self.bump();
267                self.open_frame()?;
268                self.skip_ws();
269                if self.peek() == Some(b'}') {
270                    self.bump();
271                    self.close_frame();
272                    Ok(ParseState::Attach)
273                } else {
274                    Ok(ParseState::Key)
275                }
276            }
277            _ => {
278                self.pending = Some(self.parse_leaf()?);
279                Ok(ParseState::Attach)
280            }
281        }
282    }
283
284    /// Open the container whose opening bracket was just consumed.
285    fn open_frame(&mut self) -> Result<(), ParseError> {
286        self.depth += 1;
287        if self.depth > PARSE_DEPTH_LIMIT {
288            return Err(ParseError::Depth);
289        }
290        if self.bytes[self.pos - 1] == b'[' {
291            self.stack.push(ParseFrame::Array(Vec::new()));
292        } else {
293            self.stack.push(ParseFrame::Object {
294                keys: Vec::new(),
295                values: Vec::new(),
296                current_key: None,
297            });
298        }
299        Ok(())
300    }
301
302    /// Close the top frame after consuming its closing bracket: pop it, turn
303    /// it into a node, and queue it for attachment.
304    fn close_frame(&mut self) {
305        self.depth -= 1;
306        let frame = self.stack.pop().expect("frame stack not empty");
307        self.pending = Some(frame_into_node(frame));
308    }
309
310    /// One Key step: parse `"key":` and store it on the open object frame.
311    fn step_key(&mut self) -> Result<ParseState, ParseError> {
312        self.skip_ws();
313        if self.peek() != Some(b'"') {
314            return Err(ParseError::Invalid);
315        }
316        let key = self.parse_string()?;
317        self.skip_ws();
318        if self.peek() != Some(b':') {
319            return Err(ParseError::Invalid);
320        }
321        self.bump();
322        match self.stack.last_mut() {
323            Some(ParseFrame::Object { current_key, .. }) => *current_key = Some(key),
324            _ => return Err(ParseError::Invalid),
325        }
326        Ok(ParseState::Value)
327    }
328
329    /// One Attach step: attach the completed node to its parent, close the
330    /// parent on its closing bracket, or finish the root value.
331    fn step_attach(&mut self) -> Result<Flow, ParseError> {
332        let Some(node) = self.pending.take() else {
333            return Err(ParseError::Invalid);
334        };
335        self.skip_ws();
336        let delimiter = self.peek();
337        let Some(frame) = self.stack.pop() else {
338            if self.pos != self.bytes.len() {
339                // json.loads rejects trailing content after the value.
340                return Err(ParseError::Invalid);
341            }
342            return Ok(Flow::Done(node));
343        };
344        match frame {
345            ParseFrame::Array(mut items) => match delimiter {
346                Some(b',') => {
347                    self.bump();
348                    items.push(node);
349                    self.stack.push(ParseFrame::Array(items));
350                    Ok(Flow::Next(ParseState::Value))
351                }
352                Some(b']') => {
353                    self.bump();
354                    items.push(node);
355                    self.depth -= 1;
356                    self.pending = Some(JsonNode::array(items));
357                    Ok(Flow::Next(ParseState::Attach))
358                }
359                _ => Err(ParseError::Invalid),
360            },
361            ParseFrame::Object {
362                mut keys,
363                mut values,
364                mut current_key,
365            } => {
366                let key = current_key.take().ok_or(ParseError::Invalid)?;
367                match delimiter {
368                    Some(b',') => {
369                        self.bump();
370                        upsert_object(&mut keys, &mut values, key, node);
371                        self.stack.push(ParseFrame::Object {
372                            keys,
373                            values,
374                            current_key,
375                        });
376                        Ok(Flow::Next(ParseState::Key))
377                    }
378                    Some(b'}') => {
379                        self.bump();
380                        upsert_object(&mut keys, &mut values, key, node);
381                        self.depth -= 1;
382                        self.pending = Some(JsonNode::object(keys, values));
383                        Ok(Flow::Next(ParseState::Attach))
384                    }
385                    _ => Err(ParseError::Invalid),
386                }
387            }
388        }
389    }
390
391    /// A non-container value: string, number, boolean, null, or the Python
392    /// `json.loads` NaN/Infinity extensions.
393    fn parse_leaf(&mut self) -> Result<JsonNode, ParseError> {
394        match self.peek() {
395            Some(b'"') => Ok(JsonNode::leaf(Scalar::Str(self.parse_string()?))),
396            Some(b't') if self.literal("true") => Ok(JsonNode::leaf(Scalar::Bool(true))),
397            Some(b'f') if self.literal("false") => Ok(JsonNode::leaf(Scalar::Bool(false))),
398            Some(b'n') if self.literal("null") => Ok(JsonNode::leaf(Scalar::Null)),
399            Some(b'N') if self.literal("NaN") => Ok(JsonNode::leaf(Scalar::Nan)),
400            Some(b'I') if self.literal("Infinity") => Ok(JsonNode::leaf(Scalar::Infinity)),
401            Some(b'-') => {
402                if self.literal("-Infinity") {
403                    return Ok(JsonNode::leaf(Scalar::NegInfinity));
404                }
405                Ok(JsonNode::leaf(Scalar::Number(self.parse_number_literal()?)))
406            }
407            Some(c) if c.is_ascii_digit() => {
408                Ok(JsonNode::leaf(Scalar::Number(self.parse_number_literal()?)))
409            }
410            _ => Err(ParseError::Invalid),
411        }
412    }
413
414    /// A JSON string with escape and surrogate-pair processing; unescaped
415    /// control characters are rejected (Python's strict mode).
416    fn parse_string(&mut self) -> Result<String, ParseError> {
417        self.bump(); // opening '"'
418        let mut out = String::new();
419        loop {
420            match self.peek() {
421                None => return Err(ParseError::Invalid),
422                Some(b'"') => {
423                    self.bump();
424                    return Ok(out);
425                }
426                Some(b'\\') => {
427                    self.bump();
428                    let escaped = self.peek().ok_or(ParseError::Invalid)?;
429                    self.bump();
430                    match escaped {
431                        b'"' => out.push('"'),
432                        b'\\' => out.push('\\'),
433                        b'/' => out.push('/'),
434                        b'b' => out.push('\u{8}'),
435                        b'f' => out.push('\u{c}'),
436                        b'n' => out.push('\n'),
437                        b'r' => out.push('\r'),
438                        b't' => out.push('\t'),
439                        b'u' => {
440                            let first = self.parse_hex4()?;
441                            let ch = match first {
442                                (0xD800..=0xDBFF) => {
443                                    // High surrogate: a following low
444                                    // surrogate combines, anything else is a
445                                    // lone surrogate (U+FFFD at the engine's
446                                    // decode boundary).
447                                    if self.peek() == Some(b'\\')
448                                        && self.bytes.get(self.pos + 1) == Some(&b'u')
449                                    {
450                                        self.pos += 2;
451                                        let second = self.parse_hex4()?;
452                                        if (0xDC00..=0xDFFF).contains(&second) {
453                                            let combined = 0x1_0000
454                                                + ((u32::from(first) - 0xD800) << 10)
455                                                + (u32::from(second) - 0xDC00);
456                                            char::from_u32(combined).ok_or(ParseError::Invalid)?
457                                        } else {
458                                            self.pos -= 2;
459                                            '\u{FFFD}'
460                                        }
461                                    } else {
462                                        '\u{FFFD}'
463                                    }
464                                }
465                                (0xDC00..=0xDFFF) => '\u{FFFD}',
466                                _ => char::from_u32(u32::from(first)).ok_or(ParseError::Invalid)?,
467                            };
468                            out.push(ch);
469                        }
470                        _ => return Err(ParseError::Invalid),
471                    }
472                }
473                Some(c) if c < 0x20 => return Err(ParseError::Invalid),
474                Some(_) => {
475                    // Copy one full UTF-8 scalar; the input is a valid &str.
476                    let rest = &self.bytes[self.pos..];
477                    let s = std::str::from_utf8(rest).map_err(|_| ParseError::Invalid)?;
478                    let ch = s.chars().next().ok_or(ParseError::Invalid)?;
479                    out.push(ch);
480                    self.pos += ch.len_utf8();
481                }
482            }
483        }
484    }
485
486    fn parse_hex4(&mut self) -> Result<u16, ParseError> {
487        if self.pos + 4 > self.bytes.len() {
488            return Err(ParseError::Invalid);
489        }
490        let slice = &self.bytes[self.pos..self.pos + 4];
491        let text = std::str::from_utf8(slice).map_err(|_| ParseError::Invalid)?;
492        let value = u16::from_str_radix(text, 16).map_err(|_| ParseError::Invalid)?;
493        self.pos += 4;
494        Ok(value)
495    }
496
497    /// Strict JSON number grammar; leading zeros rejected like Python's
498    /// strict parser. The literal span is kept verbatim for scanning.
499    fn parse_number_literal(&mut self) -> Result<String, ParseError> {
500        let start = self.pos;
501        if self.peek() == Some(b'-') {
502            self.bump();
503        }
504        match self.peek() {
505            Some(b'0') => self.bump(),
506            Some(c) if c.is_ascii_digit() => {
507                while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
508                    self.bump();
509                }
510            }
511            _ => return Err(ParseError::Invalid),
512        }
513        if self.peek() == Some(b'.') {
514            self.bump();
515            if !matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
516                return Err(ParseError::Invalid);
517            }
518            while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
519                self.bump();
520            }
521        }
522        if matches!(self.peek(), Some(b'e' | b'E')) {
523            self.bump();
524            if matches!(self.peek(), Some(b'+' | b'-')) {
525                self.bump();
526            }
527            if !matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
528                return Err(ParseError::Invalid);
529            }
530            while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
531                self.bump();
532            }
533        }
534        Ok(std::str::from_utf8(&self.bytes[start..self.pos])
535            .map_err(|_| ParseError::Invalid)?
536            .to_owned())
537    }
538}
539
540/// `_MONGO_OPERATOR_KEY_RE` from `guard_core/_utils/body_json_scan.py`.
541fn mongo_operator_key_re() -> &'static Regex {
542    static RE: LazyLock<Regex> = LazyLock::new(|| {
543        Regex::new(r"^\$(?:ne|gt|gte|lt|lte|eq|in|nin|nor|and|or|not|all|size|exists|type|mod|options|where|regex|expr|function|elemMatch)$")
544            .expect("mongo operator key regex")
545    });
546    &RE
547}
548
549struct Frame<'a> {
550    is_entry: bool,
551    key: String,
552    node: &'a JsonNode,
553    depth: usize,
554}
555
556/// `appendJSONWalkEntries` emits the scan values of one JSON walk in the
557/// reference order. Entry keys carry the `request_body` context; leaves carry
558/// the walk context (`request_body` for a top-level body walk, or a field
559/// context with the `:embedded_json` suffix per embedded walk level).
560///
561/// `excluded` carries the excluded detection body fields (entries
562/// lowercased at resolution time): an entry key in the set skips its whole
563/// subtree, exactly the reference's excluded-field routing in the JSON walk
564/// (the mongo-operator direct hit sits behind the same check).
565pub(crate) fn append_json_walk_entries(
566    mut values: Vec<BodyScanValue>,
567    root: &JsonNode,
568    context: &str,
569    excluded: crate::body_scan::ExcludedBodyFields<'_>,
570) -> Vec<BodyScanValue> {
571    let allow_leaf_reparse = context != REQUEST_BODY_CONTEXT;
572    // Frames are consumed in order; the reference uses a LIFO stack and pushes
573    // children reversed, so children process in insertion order.
574    let mut stack = vec![Frame {
575        is_entry: false,
576        key: String::new(),
577        node: root,
578        depth: 1,
579    }];
580    while let Some(frame) = stack.pop() {
581        if frame.is_entry {
582            if excluded.contains(&frame.key) {
583                continue;
584            }
585            if mongo_operator_key_re().is_match(&frame.key) {
586                // body_json_scan._mongo_operator_key_hit: the reference
587                // reports this hit straight from the walk, unfiltered.
588                values.push(BodyScanValue::forced(
589                    frame.key,
590                    crate::body_scan::MONGO_OPERATOR_CATEGORY,
591                ));
592                continue;
593            }
594            values.push(BodyScanValue::plain(
595                frame.key.as_str(),
596                REQUEST_BODY_CONTEXT,
597            ));
598            stack.push(Frame {
599                is_entry: false,
600                key: frame.key.clone(),
601                node: frame.node,
602                depth: frame.depth + 1,
603            });
604            continue;
605        }
606        let node = frame.node;
607        if node.is_object {
608            if frame.depth >= JSON_WALK_DEPTH_CAP {
609                values.push(BodyScanValue::plain(serialize_compact_json(node), context));
610                continue;
611            }
612            for index in (0..node.keys.len()).rev() {
613                stack.push(Frame {
614                    is_entry: true,
615                    key: node.keys[index].clone(),
616                    node: &node.values[index],
617                    depth: frame.depth,
618                });
619            }
620        } else if node.is_array {
621            if frame.depth >= JSON_WALK_DEPTH_CAP {
622                values.push(BodyScanValue::plain(serialize_compact_json(node), context));
623                continue;
624            }
625            for item in node.items.iter().rev() {
626                // List items inherit the container's label.
627                stack.push(Frame {
628                    is_entry: false,
629                    key: frame.key.clone(),
630                    node: item,
631                    depth: frame.depth + 1,
632                });
633            }
634        } else {
635            let scalar = node.scalar.as_ref().expect("leaf node carries a scalar");
636            let text = scalar.scan_text();
637            if allow_leaf_reparse && let Some(inner) = parse_ordered_json(&text) {
638                values = append_json_walk_entries(
639                    values,
640                    &inner,
641                    &format!("{context}{EMBEDDED_JSON_LEAF_CONTEXT_SUFFIX}"),
642                    excluded,
643                );
644                continue;
645            }
646            values.push(BodyScanValue::plain(text, context));
647        }
648    }
649    values
650}
651
652/// Render a subtree the way
653/// `json.dumps(value, separators=(",", ":"), ensure_ascii=False)` does for
654/// the depth-capped subtree scan.
655///
656/// `NaN`/`Infinity`/`-Infinity` are emitted bare, like `json.dumps` with the
657/// default `allow_nan=True`.
658#[must_use]
659pub fn serialize_compact_json(node: &JsonNode) -> String {
660    let mut out = String::new();
661    write_compact_json(&mut out, node);
662    out
663}
664
665fn write_compact_json(out: &mut String, node: &JsonNode) {
666    if node.is_object {
667        out.push('{');
668        for (index, key) in node.keys.iter().enumerate() {
669            if index > 0 {
670                out.push(',');
671            }
672            out.push('"');
673            write_json_string_body(out, key);
674            out.push_str("\":");
675            write_compact_json(out, &node.values[index]);
676        }
677        out.push('}');
678        return;
679    }
680    if node.is_array {
681        out.push('[');
682        for (index, item) in node.items.iter().enumerate() {
683            if index > 0 {
684                out.push(',');
685            }
686            write_compact_json(out, item);
687        }
688        out.push(']');
689        return;
690    }
691    match node.scalar.as_ref().expect("leaf node carries a scalar") {
692        Scalar::Str(s) => {
693            out.push('"');
694            write_json_string_body(out, s);
695            out.push('"');
696        }
697        Scalar::Number(literal) => out.push_str(literal),
698        Scalar::Bool(true) => out.push_str("true"),
699        Scalar::Bool(false) => out.push_str("false"),
700        Scalar::Null => out.push_str("null"),
701        Scalar::Nan => out.push_str("NaN"),
702        Scalar::Infinity => out.push_str("Infinity"),
703        Scalar::NegInfinity => out.push_str("-Infinity"),
704    }
705}
706
707/// Escape `s` the way `json.dumps(ensure_ascii=False)` does: quote,
708/// backslash, and the C0 controls; everything else stays literal UTF-8.
709fn write_json_string_body(out: &mut String, s: &str) {
710    for r in s.chars() {
711        match r {
712            '"' => out.push_str("\\\""),
713            '\\' => out.push_str("\\\\"),
714            '\n' => out.push_str("\\n"),
715            '\r' => out.push_str("\\r"),
716            '\t' => out.push_str("\\t"),
717            '\u{8}' => out.push_str("\\b"),
718            '\u{c}' => out.push_str("\\f"),
719            _ => {
720                if r < ' ' {
721                    const HEX_DIGITS: &[u8; 16] = b"0123456789abcdef";
722                    let cp = r as u32;
723                    out.push_str("\\u00");
724                    out.push(HEX_DIGITS[((cp >> 4) & 0xf) as usize] as char);
725                    out.push(HEX_DIGITS[(cp & 0xf) as usize] as char);
726                } else {
727                    out.push(r);
728                }
729            }
730        }
731    }
732}
733
734#[cfg(test)]
735mod tests {
736    use super::*;
737
738    fn walk(context: &str, body: &str) -> Vec<(String, String, Option<&'static str>)> {
739        parse_ordered_json(body)
740            .map(|root| {
741                append_json_walk_entries(
742                    Vec::new(),
743                    &root,
744                    context,
745                    crate::body_scan::ExcludedBodyFields::default(),
746                )
747                .into_iter()
748                .map(|v| (v.context, v.content, v.forced_category))
749                .collect()
750            })
751            .unwrap_or_default()
752    }
753
754    #[test]
755    fn root_must_be_object_or_array() {
756        assert!(parse_ordered_json("123").is_none());
757        assert!(parse_ordered_json("\"str\"").is_none());
758        assert!(parse_ordered_json("true").is_none());
759        assert!(parse_ordered_json("null").is_none());
760        assert!(parse_ordered_json("").is_none());
761        assert!(parse_ordered_json("{\"a\":1}").is_some());
762        assert!(parse_ordered_json("[1,2]").is_some());
763    }
764
765    #[test]
766    fn trailing_data_rejected_like_json_loads() {
767        assert!(parse_ordered_json("{\"a\":1} trailing").is_none());
768        assert!(parse_ordered_json("[1] [2]").is_none());
769    }
770
771    #[test]
772    fn duplicate_keys_keep_first_position_and_last_value() {
773        let values = walk(REQUEST_BODY_CONTEXT, "{\"a\":1,\"b\":2,\"a\":3}");
774        let contents: Vec<&str> = values.iter().map(|(_, c, _)| c.as_str()).collect();
775        // Insertion order a, b with the last value winning per key.
776        assert_eq!(contents, vec!["a", "3", "b", "2"]);
777    }
778
779    #[test]
780    fn number_and_boolean_leaves_scan_python_str_renderings() {
781        let values = walk(REQUEST_BODY_CONTEXT, "[1, -2.5, 1e2, true, false, null]");
782        let contents: Vec<&str> = values.iter().map(|(_, c, _)| c.as_str()).collect();
783        // Number leaves scan the literal (documented deviation from Python's
784        // float repr for 1e2, which Python renders as "100.0").
785        assert_eq!(contents, vec!["1", "-2.5", "1e2", "True", "False", "None"]);
786    }
787
788    #[test]
789    fn nan_and_infinity_accepted_like_json_loads() {
790        let values = walk(REQUEST_BODY_CONTEXT, "[NaN, Infinity, -Infinity]");
791        let contents: Vec<&str> = values.iter().map(|(_, c, _)| c.as_str()).collect();
792        assert_eq!(contents, vec!["nan", "inf", "-inf"]);
793    }
794
795    #[test]
796    fn mongo_operator_keys_force_nosql_hits() {
797        for key in ["$where", "$ne", "$regex", "$elemMatch"] {
798            let body = format!("{{\"{key}\": 1}}");
799            let values = walk(REQUEST_BODY_CONTEXT, &body);
800            assert_eq!(values.len(), 1, "{key}");
801            assert_eq!(values[0].0, REQUEST_BODY_CONTEXT);
802            assert_eq!(values[0].1, key);
803            assert_eq!(values[0].2, Some(crate::body_scan::MONGO_OPERATOR_CATEGORY));
804        }
805    }
806
807    #[test]
808    fn dollar_word_that_is_not_an_operator_scans_plain() {
809        let values = walk(REQUEST_BODY_CONTEXT, "{\"$money\": 1}");
810        assert_eq!(values.len(), 2);
811        assert_eq!(values[0].2, None);
812        assert_eq!(values[0].1, "$money");
813    }
814
815    #[test]
816    fn embedded_walk_appends_the_suffix_per_level() {
817        // A form field walk whose leaf is a JSON string carrying JSON: the
818        // leaf re-parses once per level, so each level adds one suffix.
819        let single = r#"{"outer":"{\"a\":\"1 OR 1=1\"}"}"#;
820        let values = walk("request_body:form_field", single);
821        assert_eq!(
822            values,
823            vec![
824                (REQUEST_BODY_CONTEXT.to_owned(), "outer".to_owned(), None),
825                (REQUEST_BODY_CONTEXT.to_owned(), "a".to_owned(), None),
826                (
827                    "request_body:form_field:embedded_json".to_owned(),
828                    "1 OR 1=1".to_owned(),
829                    None
830                ),
831            ]
832        );
833
834        // A string leaf only re-walks when it parses to an object or array,
835        // so the second suffix needs another JSON object carried in the
836        // first one's leaf string.
837        let double = r#"{"outer":"{\"a\":\"{\\\"b\\\":\\\"1 OR 1=1\\\"}\"}"}"#;
838        let values = walk("request_body:form_field", double);
839        assert_eq!(
840            values,
841            vec![
842                (REQUEST_BODY_CONTEXT.to_owned(), "outer".to_owned(), None),
843                (REQUEST_BODY_CONTEXT.to_owned(), "a".to_owned(), None),
844                (REQUEST_BODY_CONTEXT.to_owned(), "b".to_owned(), None),
845                (
846                    "request_body:form_field:embedded_json:embedded_json".to_owned(),
847                    "1 OR 1=1".to_owned(),
848                    None
849                ),
850            ]
851        );
852    }
853
854    #[test]
855    fn top_level_walk_never_reparses_leaf_strings() {
856        let body = "{\"a\":\"{\\\"b\\\":\\\"c\\\"}\"}";
857        let values = walk(REQUEST_BODY_CONTEXT, body);
858        assert_eq!(
859            values,
860            vec![
861                (REQUEST_BODY_CONTEXT.to_owned(), "a".to_owned(), None),
862                (
863                    REQUEST_BODY_CONTEXT.to_owned(),
864                    "{\"b\":\"c\"}".to_owned(),
865                    None
866                ),
867            ]
868        );
869    }
870
871    #[test]
872    fn depth_cap_serializes_the_subtree_as_compact_json() {
873        let mut deep = String::new();
874        for _ in 0..40 {
875            deep.insert(0, '[');
876            deep.push(']');
877        }
878        let values = walk(REQUEST_BODY_CONTEXT, &format!("{{\"a\":{deep}}}"));
879        assert_eq!(values[0].1, "a");
880        let last = values.last().expect("capped subtree value");
881        // The 32nd-level container serializes back to compact JSON: 40
882        // nested arrays minus the 30 walked levels leave 10 in the text.
883        assert_eq!(last.1.matches('[').count(), 10);
884        assert_eq!(last.1.matches(']').count(), 10);
885    }
886
887    #[test]
888    fn compact_serialization_escapes_like_json_dumps() {
889        let values = walk(REQUEST_BODY_CONTEXT, "{\"a\":\"quote\\\" back\\\\ nl\\n\"}");
890        let last = values.last().expect("leaf");
891        assert_eq!(last.1, "quote\" back\\ nl\n");
892        // Serialize the same string from a capped container.
893        let node = parse_ordered_json("{\"a\":\"x\\u0001y\"}").expect("parses");
894        let text = serialize_compact_json(&node);
895        assert_eq!(text, "{\"a\":\"x\\u0001y\"}");
896    }
897
898    #[test]
899    fn parse_depth_limit_beyond_python_recursion_fails_to_blob() {
900        let mut deep = String::new();
901        for _ in 0..1500 {
902            deep.insert(0, '[');
903            deep.push(']');
904        }
905        assert!(parse_ordered_json(&deep).is_none());
906        // Just above the walk cap but far below the parse limit still walks,
907        // with the capped subtrees serialized.
908        let mut mid = String::new();
909        for _ in 0..40 {
910            mid.insert(0, '[');
911            mid.push(']');
912        }
913        assert!(parse_ordered_json(&mid).is_some());
914    }
915
916    #[test]
917    fn strict_grammar_rejects_loose_json() {
918        for body in [
919            "{\"a\":01}",
920            "[1,]",
921            "{\"a\":}",
922            "{\"a\" 1}",
923            "[unclosed",
924            "\"\\x41\"",
925            "[1 2]",
926        ] {
927            assert!(parse_ordered_json(body).is_none(), "{body:?} must fail");
928        }
929    }
930
931    #[test]
932    fn unicode_escapes_decode() {
933        let values = walk(
934            REQUEST_BODY_CONTEXT,
935            "[\"\\u00e9\\ud83d\\ude00\", \"\\ud800\"]",
936        );
937        assert_eq!(values[0].1, "\u{e9}\u{1F600}");
938        assert_eq!(values[1].1, "\u{FFFD}", "lone surrogate -> replacement");
939    }
940}