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
165impl ParseFrame {
166    /// The object frame's pending-key slot; `None` for array frames (a Key
167    /// step only ever follows an object frame, so callers treat `None` as
168    /// an unreachable invariant violation).
169    const fn object_key_slot(&mut self) -> Option<&mut Option<String>> {
170        match self {
171            Self::Object { current_key, .. } => Some(current_key),
172            Self::Array(_) => None,
173        }
174    }
175}
176
177/// The next machine step: continue with a state, or finish with the root.
178enum Flow {
179    Next(ParseState),
180    Done(JsonNode),
181}
182
183/// Duplicate keys keep the first insertion position and the last value
184/// (Python dict semantics).
185fn upsert_object(keys: &mut Vec<String>, values: &mut Vec<JsonNode>, key: String, node: JsonNode) {
186    if let Some(at) = keys.iter().position(|existing| *existing == key) {
187        values[at] = node;
188    } else {
189        keys.push(key);
190        values.push(node);
191    }
192}
193
194/// Explicit parse-frame stack entry: the recursive descent of the reference
195/// `json.loads`, flattened so the depth bound is a semantic cap rather than a
196/// stack-overflow guard.
197#[derive(Debug)]
198enum ParseFrame {
199    Array(Vec<JsonNode>),
200    Object {
201        keys: Vec<String>,
202        values: Vec<JsonNode>,
203        current_key: Option<String>,
204    },
205}
206
207enum ParseState {
208    /// Parse one value (leaf, or open a container).
209    Value,
210    /// Parse an object key (or the closing brace in the caller).
211    Key,
212    /// A completed node is waiting to attach to its parent.
213    Attach,
214}
215
216fn frame_into_node(frame: ParseFrame) -> JsonNode {
217    match frame {
218        ParseFrame::Array(items) => JsonNode::array(items),
219        ParseFrame::Object { keys, values, .. } => JsonNode::object(keys, values),
220    }
221}
222
223impl Parser<'_> {
224    const fn peek(&self) -> Option<u8> {
225        if self.pos < self.bytes.len() {
226            Some(self.bytes[self.pos])
227        } else {
228            None
229        }
230    }
231
232    const fn bump(&mut self) {
233        self.pos += 1;
234    }
235
236    const fn skip_ws(&mut self) {
237        while matches!(self.peek(), Some(b' ' | b'\t' | b'\n' | b'\r')) {
238            self.pos += 1;
239        }
240    }
241
242    fn literal(&mut self, word: &str) -> bool {
243        if self.bytes[self.pos..].starts_with(word.as_bytes()) {
244            self.pos += word.len();
245            return true;
246        }
247        false
248    }
249
250    fn parse_root(&mut self) -> Result<JsonNode, ParseError> {
251        let mut flow = Flow::Next(ParseState::Value);
252        loop {
253            flow = match flow {
254                Flow::Next(ParseState::Value) => Flow::Next(self.step_value()?),
255                Flow::Next(ParseState::Key) => Flow::Next(self.step_key()?),
256                Flow::Next(ParseState::Attach) => self.step_attach()?,
257                Flow::Done(node) => return Ok(node),
258            };
259        }
260    }
261
262    /// One Value step: open a container or parse a leaf.
263    fn step_value(&mut self) -> Result<ParseState, ParseError> {
264        self.skip_ws();
265        match self.peek() {
266            Some(b'[') => {
267                self.bump();
268                self.open_frame()?;
269                self.skip_ws();
270                if self.peek() == Some(b']') {
271                    self.bump();
272                    self.close_frame();
273                    Ok(ParseState::Attach)
274                } else {
275                    Ok(ParseState::Value)
276                }
277            }
278            Some(b'{') => {
279                self.bump();
280                self.open_frame()?;
281                self.skip_ws();
282                if self.peek() == Some(b'}') {
283                    self.bump();
284                    self.close_frame();
285                    Ok(ParseState::Attach)
286                } else {
287                    Ok(ParseState::Key)
288                }
289            }
290            _ => {
291                self.pending = Some(self.parse_leaf()?);
292                Ok(ParseState::Attach)
293            }
294        }
295    }
296
297    /// Open the container whose opening bracket was just consumed.
298    fn open_frame(&mut self) -> Result<(), ParseError> {
299        self.depth += 1;
300        if self.depth > PARSE_DEPTH_LIMIT {
301            return Err(ParseError::Depth);
302        }
303        if self.bytes[self.pos - 1] == b'[' {
304            self.stack.push(ParseFrame::Array(Vec::new()));
305        } else {
306            self.stack.push(ParseFrame::Object {
307                keys: Vec::new(),
308                values: Vec::new(),
309                current_key: None,
310            });
311        }
312        Ok(())
313    }
314
315    /// Close the top frame after consuming its closing bracket: pop it, turn
316    /// it into a node, and queue it for attachment.
317    fn close_frame(&mut self) {
318        self.depth -= 1;
319        let frame = self.stack.pop().expect("frame stack not empty");
320        self.pending = Some(frame_into_node(frame));
321    }
322
323    /// One Key step: parse `"key":` and store it on the open object frame.
324    fn step_key(&mut self) -> Result<ParseState, ParseError> {
325        self.skip_ws();
326        if self.peek() != Some(b'"') {
327            return Err(ParseError::Invalid);
328        }
329        let key = self.parse_string()?;
330        self.skip_ws();
331        if self.peek() != Some(b':') {
332            return Err(ParseError::Invalid);
333        }
334        self.bump();
335        // a Key step only follows an opened object frame, so the frame under
336        // the key is always the object being filled
337        let mut frame = self.stack.last_mut();
338        #[cfg(not(coverage))] // unreachable: the machine only enters a Key
339        // step right after an object frame opened, so the slot is always
340        // present
341        if let Some(slot) = frame.as_mut().and_then(|frame| frame.object_key_slot()) {
342            *slot = Some(key);
343        }
344        #[cfg(coverage)]
345        {
346            let slot = frame
347                .as_mut()
348                .and_then(|frame| frame.object_key_slot())
349                .expect("a Key step always follows an object frame");
350            *slot = Some(key);
351        }
352        Ok(ParseState::Value)
353    }
354
355    /// One Attach step: attach the completed node to its parent, close the
356    /// parent on its closing bracket, or finish the root value.
357    fn step_attach(&mut self) -> Result<Flow, ParseError> {
358        #[cfg(not(coverage))] // unreachable: every Attach entry queues a node
359        let Some(node) = self.pending.take() else {
360            return Err(ParseError::Invalid);
361        };
362        #[cfg(coverage)]
363        let node = self
364            .pending
365            .take()
366            .expect("every Attach entry queues a node");
367        self.skip_ws();
368        let delimiter = self.peek();
369        let Some(frame) = self.stack.pop() else {
370            if self.pos != self.bytes.len() {
371                // json.loads rejects trailing content after the value.
372                return Err(ParseError::Invalid);
373            }
374            return Ok(Flow::Done(node));
375        };
376        match frame {
377            ParseFrame::Array(mut items) => match delimiter {
378                Some(b',') => {
379                    self.bump();
380                    items.push(node);
381                    self.stack.push(ParseFrame::Array(items));
382                    Ok(Flow::Next(ParseState::Value))
383                }
384                Some(b']') => {
385                    self.bump();
386                    items.push(node);
387                    self.depth -= 1;
388                    self.pending = Some(JsonNode::array(items));
389                    Ok(Flow::Next(ParseState::Attach))
390                }
391                _ => Err(ParseError::Invalid),
392            },
393            ParseFrame::Object {
394                mut keys,
395                mut values,
396                mut current_key,
397            } => {
398                #[cfg(not(coverage))] // unreachable: the machine re-arms the
399                // frame's key slot on every comma and closer, and a Key step
400                // rejects non-string input before any Attach can run
401                let key = current_key.take().ok_or(ParseError::Invalid)?;
402                #[cfg(coverage)]
403                let key = current_key
404                    .take()
405                    .expect("the key slot is re-armed before every Attach");
406                match delimiter {
407                    Some(b',') => {
408                        self.bump();
409                        upsert_object(&mut keys, &mut values, key, node);
410                        self.stack.push(ParseFrame::Object {
411                            keys,
412                            values,
413                            current_key,
414                        });
415                        Ok(Flow::Next(ParseState::Key))
416                    }
417                    Some(b'}') => {
418                        self.bump();
419                        upsert_object(&mut keys, &mut values, key, node);
420                        self.depth -= 1;
421                        self.pending = Some(JsonNode::object(keys, values));
422                        Ok(Flow::Next(ParseState::Attach))
423                    }
424                    _ => Err(ParseError::Invalid),
425                }
426            }
427        }
428    }
429
430    /// A non-container value: string, number, boolean, null, or the Python
431    /// `json.loads` NaN/Infinity extensions.
432    fn parse_leaf(&mut self) -> Result<JsonNode, ParseError> {
433        match self.peek() {
434            Some(b'"') => Ok(JsonNode::leaf(Scalar::Str(self.parse_string()?))),
435            Some(b't') if self.literal("true") => Ok(JsonNode::leaf(Scalar::Bool(true))),
436            Some(b'f') if self.literal("false") => Ok(JsonNode::leaf(Scalar::Bool(false))),
437            Some(b'n') if self.literal("null") => Ok(JsonNode::leaf(Scalar::Null)),
438            Some(b'N') if self.literal("NaN") => Ok(JsonNode::leaf(Scalar::Nan)),
439            Some(b'I') if self.literal("Infinity") => Ok(JsonNode::leaf(Scalar::Infinity)),
440            Some(b'-') => {
441                if self.literal("-Infinity") {
442                    return Ok(JsonNode::leaf(Scalar::NegInfinity));
443                }
444                Ok(JsonNode::leaf(Scalar::Number(self.parse_number_literal()?)))
445            }
446            Some(c) if c.is_ascii_digit() => {
447                Ok(JsonNode::leaf(Scalar::Number(self.parse_number_literal()?)))
448            }
449            _ => Err(ParseError::Invalid),
450        }
451    }
452
453    /// A JSON string with escape and surrogate-pair processing; unescaped
454    /// control characters are rejected (Python's strict mode).
455    fn parse_string(&mut self) -> Result<String, ParseError> {
456        self.bump(); // opening '"'
457        let mut out = String::new();
458        loop {
459            match self.peek() {
460                None => return Err(ParseError::Invalid),
461                Some(b'"') => {
462                    self.bump();
463                    return Ok(out);
464                }
465                Some(b'\\') => {
466                    self.bump();
467                    let escaped = self.peek().ok_or(ParseError::Invalid)?;
468                    self.bump();
469                    match escaped {
470                        b'"' => out.push('"'),
471                        b'\\' => out.push('\\'),
472                        b'/' => out.push('/'),
473                        b'b' => out.push('\u{8}'),
474                        b'f' => out.push('\u{c}'),
475                        b'n' => out.push('\n'),
476                        b'r' => out.push('\r'),
477                        b't' => out.push('\t'),
478                        b'u' => {
479                            let first = self.parse_hex4()?;
480                            let ch = match first {
481                                (0xD800..=0xDBFF) => {
482                                    // High surrogate: a following low
483                                    // surrogate combines, anything else is a
484                                    // lone surrogate (U+FFFD at the engine's
485                                    // decode boundary).
486                                    if self.peek() == Some(b'\\')
487                                        && self.bytes.get(self.pos + 1) == Some(&b'u')
488                                    {
489                                        self.pos += 2;
490                                        let second = self.parse_hex4()?;
491                                        if (0xDC00..=0xDFFF).contains(&second) {
492                                            let combined = 0x1_0000
493                                                + ((u32::from(first) - 0xD800) << 10)
494                                                + (u32::from(second) - 0xDC00);
495                                            #[cfg(not(coverage))] // unreachable:
496                                            // the surrogate-pair arithmetic
497                                            // always lands in the astral range
498                                            let ch = char::from_u32(combined)
499                                                .ok_or(ParseError::Invalid)?;
500                                            #[cfg(coverage)]
501                                            let ch = char::from_u32(combined).expect(
502                                                "the pair arithmetic yields a valid scalar",
503                                            );
504                                            ch
505                                        } else {
506                                            self.pos -= 2;
507                                            '\u{FFFD}'
508                                        }
509                                    } else {
510                                        '\u{FFFD}'
511                                    }
512                                }
513                                (0xDC00..=0xDFFF) => '\u{FFFD}',
514                                #[cfg(not(coverage))] // unreachable: every
515                                // non-surrogate u16 is a valid scalar
516                                _ => char::from_u32(u32::from(first)).ok_or(ParseError::Invalid)?,
517                                #[cfg(coverage)]
518                                _ => char::from_u32(u32::from(first))
519                                    .expect("a non-surrogate u16 is a valid scalar"),
520                            };
521                            out.push(ch);
522                        }
523                        _ => return Err(ParseError::Invalid),
524                    }
525                }
526                Some(c) if c < 0x20 => return Err(ParseError::Invalid),
527                Some(_) => {
528                    // Copy one full UTF-8 scalar; the input is a valid &str.
529                    let rest = &self.bytes[self.pos..];
530                    #[cfg(not(coverage))] // unreachable: the input is a &str,
531                    // so every slice of it is valid UTF-8...
532                    let s = std::str::from_utf8(rest).map_err(|_| ParseError::Invalid)?;
533                    #[cfg(coverage)]
534                    let s = std::str::from_utf8(rest).expect("a &str slice is valid UTF-8");
535                    #[cfg(not(coverage))] // unreachable: ...and non-empty (a
536                    // byte was peeked above), so a scalar always follows
537                    let ch = s.chars().next().ok_or(ParseError::Invalid)?;
538                    #[cfg(coverage)]
539                    let ch = s.chars().next().expect("the remainder is non-empty");
540                    out.push(ch);
541                    self.pos += ch.len_utf8();
542                }
543            }
544        }
545    }
546
547    fn parse_hex4(&mut self) -> Result<u16, ParseError> {
548        if self.pos + 4 > self.bytes.len() {
549            return Err(ParseError::Invalid);
550        }
551        let slice = &self.bytes[self.pos..self.pos + 4];
552        // A scalar can straddle the 4-byte window and break UTF-8 validity
553        // (a `\u` escape followed by a multi-byte character), so the
554        // conversion failure stays a real arm, not a defensive one.
555        let text = std::str::from_utf8(slice).map_err(|_| ParseError::Invalid)?;
556        let value = u16::from_str_radix(text, 16).map_err(|_| ParseError::Invalid)?;
557        self.pos += 4;
558        Ok(value)
559    }
560
561    /// Strict JSON number grammar; leading zeros rejected like Python's
562    /// strict parser. The literal span is kept verbatim for scanning.
563    fn parse_number_literal(&mut self) -> Result<String, ParseError> {
564        let start = self.pos;
565        if self.peek() == Some(b'-') {
566            self.bump();
567        }
568        match self.peek() {
569            Some(b'0') => self.bump(),
570            Some(c) if c.is_ascii_digit() => {
571                while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
572                    self.bump();
573                }
574            }
575            _ => return Err(ParseError::Invalid),
576        }
577        if self.peek() == Some(b'.') {
578            self.bump();
579            if !matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
580                return Err(ParseError::Invalid);
581            }
582            while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
583                self.bump();
584            }
585        }
586        if matches!(self.peek(), Some(b'e' | b'E')) {
587            self.bump();
588            if matches!(self.peek(), Some(b'+' | b'-')) {
589                self.bump();
590            }
591            if !matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
592                return Err(ParseError::Invalid);
593            }
594            while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
595                self.bump();
596            }
597        }
598        #[cfg(not(coverage))] // unreachable: the number span is a slice of
599        // a &str, so it is valid UTF-8
600        let text =
601            std::str::from_utf8(&self.bytes[start..self.pos]).map_err(|_| ParseError::Invalid)?;
602        #[cfg(coverage)]
603        let text = std::str::from_utf8(&self.bytes[start..self.pos])
604            .expect("the number span is valid UTF-8");
605        Ok(text.to_owned())
606    }
607}
608
609/// `_MONGO_OPERATOR_KEY_RE` from `guard_core/_utils/body_json_scan.py`.
610fn mongo_operator_key_re() -> &'static Regex {
611    static RE: LazyLock<Regex> = LazyLock::new(|| {
612        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)$")
613            .expect("mongo operator key regex")
614    });
615    &RE
616}
617
618struct Frame<'a> {
619    is_entry: bool,
620    key: String,
621    node: &'a JsonNode,
622    depth: usize,
623}
624
625/// `appendJSONWalkEntries` emits the scan values of one JSON walk in the
626/// reference order. Entry keys carry the `request_body` context; leaves carry
627/// the walk context (`request_body` for a top-level body walk, or a field
628/// context with the `:embedded_json` suffix per embedded walk level).
629///
630/// `excluded` carries the excluded detection body fields (entries
631/// lowercased at resolution time): an entry key in the set skips its whole
632/// subtree, exactly the reference's excluded-field routing in the JSON walk
633/// (the mongo-operator direct hit sits behind the same check).
634pub(crate) fn append_json_walk_entries(
635    mut values: Vec<BodyScanValue>,
636    root: &JsonNode,
637    context: &str,
638    excluded: crate::body_scan::ExcludedBodyFields<'_>,
639    depth_cap: usize,
640) -> Vec<BodyScanValue> {
641    let allow_leaf_reparse = context != REQUEST_BODY_CONTEXT;
642    // Frames are consumed in order; the reference uses a LIFO stack and pushes
643    // children reversed, so children process in insertion order.
644    let mut stack = vec![Frame {
645        is_entry: false,
646        key: String::new(),
647        node: root,
648        depth: 1,
649    }];
650    while let Some(frame) = stack.pop() {
651        if frame.is_entry {
652            if excluded.contains(&frame.key) {
653                continue;
654            }
655            if mongo_operator_key_re().is_match(&frame.key) {
656                // body_json_scan._mongo_operator_key_hit: the reference
657                // reports this hit straight from the walk, unfiltered.
658                values.push(BodyScanValue::forced(
659                    frame.key,
660                    crate::body_scan::MONGO_OPERATOR_CATEGORY,
661                ));
662                continue;
663            }
664            values.push(BodyScanValue::plain(
665                frame.key.as_str(),
666                REQUEST_BODY_CONTEXT,
667            ));
668            stack.push(Frame {
669                is_entry: false,
670                key: frame.key.clone(),
671                node: frame.node,
672                depth: frame.depth + 1,
673            });
674            continue;
675        }
676        let node = frame.node;
677        if node.is_object {
678            if frame.depth >= depth_cap {
679                values.push(BodyScanValue::plain(serialize_compact_json(node), context));
680                continue;
681            }
682            for index in (0..node.keys.len()).rev() {
683                stack.push(Frame {
684                    is_entry: true,
685                    key: node.keys[index].clone(),
686                    node: &node.values[index],
687                    depth: frame.depth,
688                });
689            }
690        } else if node.is_array {
691            if frame.depth >= depth_cap {
692                values.push(BodyScanValue::plain(serialize_compact_json(node), context));
693                continue;
694            }
695            for item in node.items.iter().rev() {
696                // List items inherit the container's label.
697                stack.push(Frame {
698                    is_entry: false,
699                    key: frame.key.clone(),
700                    node: item,
701                    depth: frame.depth + 1,
702                });
703            }
704        } else {
705            let scalar = node.scalar.as_ref().expect("leaf node carries a scalar");
706            let text = scalar.scan_text();
707            if allow_leaf_reparse && let Some(inner) = parse_ordered_json(&text) {
708                values = append_json_walk_entries(
709                    values,
710                    &inner,
711                    &format!("{context}{EMBEDDED_JSON_LEAF_CONTEXT_SUFFIX}"),
712                    excluded,
713                    depth_cap,
714                );
715                continue;
716            }
717            values.push(BodyScanValue::plain(text, context));
718        }
719    }
720    values
721}
722
723/// Render a subtree the way
724/// `json.dumps(value, separators=(",", ":"), ensure_ascii=False)` does for
725/// the depth-capped subtree scan.
726///
727/// `NaN`/`Infinity`/`-Infinity` are emitted bare, like `json.dumps` with the
728/// default `allow_nan=True`.
729#[must_use]
730pub fn serialize_compact_json(node: &JsonNode) -> String {
731    let mut out = String::new();
732    write_compact_json(&mut out, node);
733    out
734}
735
736fn write_compact_json(out: &mut String, node: &JsonNode) {
737    if node.is_object {
738        out.push('{');
739        for (index, key) in node.keys.iter().enumerate() {
740            if index > 0 {
741                out.push(',');
742            }
743            out.push('"');
744            write_json_string_body(out, key);
745            out.push_str("\":");
746            write_compact_json(out, &node.values[index]);
747        }
748        out.push('}');
749        return;
750    }
751    if node.is_array {
752        out.push('[');
753        for (index, item) in node.items.iter().enumerate() {
754            if index > 0 {
755                out.push(',');
756            }
757            write_compact_json(out, item);
758        }
759        out.push(']');
760        return;
761    }
762    match node.scalar.as_ref().expect("leaf node carries a scalar") {
763        Scalar::Str(s) => {
764            out.push('"');
765            write_json_string_body(out, s);
766            out.push('"');
767        }
768        Scalar::Number(literal) => out.push_str(literal),
769        Scalar::Bool(true) => out.push_str("true"),
770        Scalar::Bool(false) => out.push_str("false"),
771        Scalar::Null => out.push_str("null"),
772        Scalar::Nan => out.push_str("NaN"),
773        Scalar::Infinity => out.push_str("Infinity"),
774        Scalar::NegInfinity => out.push_str("-Infinity"),
775    }
776}
777
778/// Escape `s` the way `json.dumps(ensure_ascii=False)` does: quote,
779/// backslash, and the C0 controls; everything else stays literal UTF-8.
780fn write_json_string_body(out: &mut String, s: &str) {
781    for r in s.chars() {
782        match r {
783            '"' => out.push_str("\\\""),
784            '\\' => out.push_str("\\\\"),
785            '\n' => out.push_str("\\n"),
786            '\r' => out.push_str("\\r"),
787            '\t' => out.push_str("\\t"),
788            '\u{8}' => out.push_str("\\b"),
789            '\u{c}' => out.push_str("\\f"),
790            _ => {
791                if r < ' ' {
792                    const HEX_DIGITS: &[u8; 16] = b"0123456789abcdef";
793                    let cp = r as u32;
794                    out.push_str("\\u00");
795                    out.push(HEX_DIGITS[((cp >> 4) & 0xf) as usize] as char);
796                    out.push(HEX_DIGITS[(cp & 0xf) as usize] as char);
797                } else {
798                    out.push(r);
799                }
800            }
801        }
802    }
803}
804
805#[cfg(test)]
806mod tests {
807    use super::*;
808
809    fn walk(context: &str, body: &str) -> Vec<(String, String, Option<&'static str>)> {
810        parse_ordered_json(body)
811            .map(|root| {
812                append_json_walk_entries(
813                    Vec::new(),
814                    &root,
815                    context,
816                    crate::body_scan::ExcludedBodyFields::default(),
817                    JSON_WALK_DEPTH_CAP,
818                )
819                .into_iter()
820                .map(|v| (v.context, v.content, v.forced_category))
821                .collect()
822            })
823            .unwrap_or_default()
824    }
825
826    #[test]
827    fn root_must_be_object_or_array() {
828        assert!(parse_ordered_json("123").is_none());
829        assert!(parse_ordered_json("\"str\"").is_none());
830        assert!(parse_ordered_json("true").is_none());
831        assert!(parse_ordered_json("null").is_none());
832        assert!(parse_ordered_json("").is_none());
833        assert!(parse_ordered_json("{\"a\":1}").is_some());
834        assert!(parse_ordered_json("[1,2]").is_some());
835    }
836
837    #[test]
838    fn object_key_slot_answers_none_for_array_frames() {
839        let mut array = ParseFrame::Array(Vec::new());
840        assert!(array.object_key_slot().is_none());
841        let mut object = ParseFrame::Object {
842            keys: Vec::new(),
843            values: Vec::new(),
844            current_key: None,
845        };
846        assert!(object.object_key_slot().is_some());
847    }
848
849    #[test]
850    fn parse_error_arms_reject_the_reference_failures() {
851        // The recursion bound standing in for RecursionError.
852        let deep = format!("{}1{}", "[".repeat(1001), "]".repeat(1001));
853        assert!(parse_ordered_json(&deep).is_none(), "depth bound fires");
854        // the object arm of the frame bound: nested objects, not arrays
855        let deep_objects = format!("{}1{}", "{\"a\":".repeat(1001), "1".repeat(1001));
856        assert!(parse_ordered_json(&deep_objects).is_none(), "object depth");
857        // a comma followed by a value re-opens the object frame keyless,
858        // and the stray value cannot attach
859        assert!(parse_ordered_json("{\"a\":1,2}").is_none());
860        // a trailing comma before the closer is rejected like json.loads
861        assert!(parse_ordered_json("{\"a\":1,}").is_none());
862        // a malformed second \\u escape after a high surrogate
863        assert!(parse_ordered_json("{\"k\":\"\\ud83d\\uZZZZ\"}").is_none());
864        // An invalid escape inside an object key fails the key step.
865        assert!(parse_ordered_json("{\"\\q\":1}").is_none());
866        // A trailing comma leaves the object frame without a pending key.
867        assert!(parse_ordered_json("{\"a\":1,}").is_none());
868        // Control characters inside strings are rejected.
869        assert!(parse_ordered_json("{\"k\":\"a\u{1}b\"}").is_none());
870        // A non-hex \\u escape is rejected.
871        assert!(parse_ordered_json("{\"k\":\"\\uZZZZ\"}").is_none());
872        // A multi-byte scalar straddling the \\u window breaks the read.
873        assert!(parse_ordered_json("{\"k\":\"\\u\u{e9}\u{20ac}\"}").is_none());
874    }
875
876    #[test]
877    fn surrogate_pair_arms_follow_the_reference() {
878        // A valid high/low pair combines into an astral scalar...
879        let entries = walk("body", "{\"k\":\"\\ud83d\\ude00\"}");
880        let joined: String = entries.iter().map(|e| e.1.as_str()).collect();
881        assert!(
882            joined.contains('\u{1F600}'),
883            "the surrogate pair combines: {entries:?}"
884        );
885        // ...a lone high surrogate degrades to U+FFFD, and the parser
886        // rewinds before the unconsumed \\u.
887        let entries = walk("body", "{\"k\":\"\\ud83dx\"}");
888        let joined: String = entries.iter().map(|e| e.1.as_str()).collect();
889        assert!(
890            joined.contains('\u{FFFD}'),
891            "the lone high surrogate degrades: {entries:?}"
892        );
893        // A lone low surrogate degrades to U+FFFD too.
894        let entries = walk("body", "{\"k\":\"\\udc00\"}");
895        let joined: String = entries.iter().map(|e| e.1.as_str()).collect();
896        assert!(
897            joined.contains('\u{FFFD}'),
898            "the lone low surrogate degrades: {entries:?}"
899        );
900    }
901
902    #[test]
903    fn trailing_data_rejected_like_json_loads() {
904        assert!(parse_ordered_json("{\"a\":1} trailing").is_none());
905        assert!(parse_ordered_json("[1] [2]").is_none());
906    }
907
908    #[test]
909    fn duplicate_keys_keep_first_position_and_last_value() {
910        let values = walk(REQUEST_BODY_CONTEXT, "{\"a\":1,\"b\":2,\"a\":3}");
911        let contents: Vec<&str> = values.iter().map(|(_, c, _)| c.as_str()).collect();
912        // Insertion order a, b with the last value winning per key.
913        assert_eq!(contents, vec!["a", "3", "b", "2"]);
914    }
915
916    #[test]
917    fn number_and_boolean_leaves_scan_python_str_renderings() {
918        let values = walk(REQUEST_BODY_CONTEXT, "[1, -2.5, 1e2, true, false, null]");
919        let contents: Vec<&str> = values.iter().map(|(_, c, _)| c.as_str()).collect();
920        // Number leaves scan the literal (documented deviation from Python's
921        // float repr for 1e2, which Python renders as "100.0").
922        assert_eq!(contents, vec!["1", "-2.5", "1e2", "True", "False", "None"]);
923    }
924
925    #[test]
926    fn nan_and_infinity_accepted_like_json_loads() {
927        let values = walk(REQUEST_BODY_CONTEXT, "[NaN, Infinity, -Infinity]");
928        let contents: Vec<&str> = values.iter().map(|(_, c, _)| c.as_str()).collect();
929        assert_eq!(contents, vec!["nan", "inf", "-inf"]);
930    }
931
932    #[test]
933    fn mongo_operator_keys_force_nosql_hits() {
934        for key in ["$where", "$ne", "$regex", "$elemMatch"] {
935            let body = format!("{{\"{key}\": 1}}");
936            let values = walk(REQUEST_BODY_CONTEXT, &body);
937            assert_eq!(values.len(), 1, "{key}");
938            assert_eq!(values[0].0, REQUEST_BODY_CONTEXT);
939            assert_eq!(values[0].1, key);
940            assert_eq!(values[0].2, Some(crate::body_scan::MONGO_OPERATOR_CATEGORY));
941        }
942    }
943
944    #[test]
945    fn dollar_word_that_is_not_an_operator_scans_plain() {
946        let values = walk(REQUEST_BODY_CONTEXT, "{\"$money\": 1}");
947        assert_eq!(values.len(), 2);
948        assert_eq!(values[0].2, None);
949        assert_eq!(values[0].1, "$money");
950    }
951
952    #[test]
953    fn embedded_walk_appends_the_suffix_per_level() {
954        // A form field walk whose leaf is a JSON string carrying JSON: the
955        // leaf re-parses once per level, so each level adds one suffix.
956        let single = r#"{"outer":"{\"a\":\"1 OR 1=1\"}"}"#;
957        let values = walk("request_body:form_field", single);
958        assert_eq!(
959            values,
960            vec![
961                (REQUEST_BODY_CONTEXT.to_owned(), "outer".to_owned(), None),
962                (REQUEST_BODY_CONTEXT.to_owned(), "a".to_owned(), None),
963                (
964                    "request_body:form_field:embedded_json".to_owned(),
965                    "1 OR 1=1".to_owned(),
966                    None
967                ),
968            ]
969        );
970
971        // A string leaf only re-walks when it parses to an object or array,
972        // so the second suffix needs another JSON object carried in the
973        // first one's leaf string.
974        let double = r#"{"outer":"{\"a\":\"{\\\"b\\\":\\\"1 OR 1=1\\\"}\"}"}"#;
975        let values = walk("request_body:form_field", double);
976        assert_eq!(
977            values,
978            vec![
979                (REQUEST_BODY_CONTEXT.to_owned(), "outer".to_owned(), None),
980                (REQUEST_BODY_CONTEXT.to_owned(), "a".to_owned(), None),
981                (REQUEST_BODY_CONTEXT.to_owned(), "b".to_owned(), None),
982                (
983                    "request_body:form_field:embedded_json:embedded_json".to_owned(),
984                    "1 OR 1=1".to_owned(),
985                    None
986                ),
987            ]
988        );
989    }
990
991    #[test]
992    fn top_level_walk_never_reparses_leaf_strings() {
993        let body = "{\"a\":\"{\\\"b\\\":\\\"c\\\"}\"}";
994        let values = walk(REQUEST_BODY_CONTEXT, body);
995        assert_eq!(
996            values,
997            vec![
998                (REQUEST_BODY_CONTEXT.to_owned(), "a".to_owned(), None),
999                (
1000                    REQUEST_BODY_CONTEXT.to_owned(),
1001                    "{\"b\":\"c\"}".to_owned(),
1002                    None
1003                ),
1004            ]
1005        );
1006    }
1007
1008    #[test]
1009    fn depth_cap_serializes_the_subtree_as_compact_json() {
1010        let mut deep = String::new();
1011        for _ in 0..40 {
1012            deep.insert(0, '[');
1013            deep.push(']');
1014        }
1015        let values = walk(REQUEST_BODY_CONTEXT, &format!("{{\"a\":{deep}}}"));
1016        assert_eq!(values[0].1, "a");
1017        let last = values.last().expect("capped subtree value");
1018        // The 32nd-level container serializes back to compact JSON: 40
1019        // nested arrays minus the 30 walked levels leave 10 in the text.
1020        assert_eq!(last.1.matches('[').count(), 10);
1021        assert_eq!(last.1.matches(']').count(), 10);
1022    }
1023
1024    #[test]
1025    fn compact_serialization_escapes_like_json_dumps() {
1026        let values = walk(REQUEST_BODY_CONTEXT, "{\"a\":\"quote\\\" back\\\\ nl\\n\"}");
1027        let last = values.last().expect("leaf");
1028        assert_eq!(last.1, "quote\" back\\ nl\n");
1029        // Serialize the same string from a capped container.
1030        let node = parse_ordered_json("{\"a\":\"x\\u0001y\"}").expect("parses");
1031        let text = serialize_compact_json(&node);
1032        assert_eq!(text, "{\"a\":\"x\\u0001y\"}");
1033    }
1034
1035    #[test]
1036    fn parse_depth_limit_beyond_python_recursion_fails_to_blob() {
1037        let mut deep = String::new();
1038        for _ in 0..1500 {
1039            deep.insert(0, '[');
1040            deep.push(']');
1041        }
1042        assert!(parse_ordered_json(&deep).is_none());
1043        // Just above the walk cap but far below the parse limit still walks,
1044        // with the capped subtrees serialized.
1045        let mut mid = String::new();
1046        for _ in 0..40 {
1047            mid.insert(0, '[');
1048            mid.push(']');
1049        }
1050        assert!(parse_ordered_json(&mid).is_some());
1051    }
1052
1053    #[test]
1054    fn strict_grammar_rejects_loose_json() {
1055        for body in [
1056            "{\"a\":01}",
1057            "[1,]",
1058            "{\"a\":}",
1059            "{\"a\" 1}",
1060            "[unclosed",
1061            "\"\\x41\"",
1062            "[1 2]",
1063        ] {
1064            assert!(parse_ordered_json(body).is_none(), "{body:?} must fail");
1065        }
1066    }
1067
1068    #[test]
1069    fn unicode_escapes_decode() {
1070        let values = walk(
1071            REQUEST_BODY_CONTEXT,
1072            "[\"\\u00e9\\ud83d\\ude00\", \"\\ud800\"]",
1073        );
1074        assert_eq!(values[0].1, "\u{e9}\u{1F600}");
1075        assert_eq!(values[1].1, "\u{FFFD}", "lone surrogate -> replacement");
1076    }
1077}
1078
1079#[cfg(test)]
1080mod coverage_tests {
1081    use super::*;
1082
1083    #[test]
1084    fn strict_parser_rejects_malformed_documents() {
1085        for bad in [
1086            "{'1':2}",     // the key is not a string
1087            "[\"k\":1]",   // a key step on an array frame
1088            "\"abc",       // unterminated string
1089            "\"abc\\",     // a trailing backslash
1090            "{\"a\":01}",  // a leading zero number
1091            "{\"a\":-}",   // a bare minus
1092            "{\"a\":tru}", // a truncated literal
1093            "+1",          // a bad value start
1094            "{\"a\":1,}",  // a trailing comma
1095            "{} extra",    // trailing content after the value
1096        ] {
1097            assert!(parse_ordered_json(bad).is_none(), "accepted {bad:?}");
1098        }
1099    }
1100
1101    #[test]
1102    fn string_escapes_decode_including_surrogate_pairs() {
1103        // scalar roots are not walked (the corpus only carries objects and
1104        // arrays), so the escape probes ride inside an object
1105        let node = parse_ordered_json(r#"{"e":"\/\b\f\r\tA"}"#).expect("escapes");
1106        assert_eq!(serialize_compact_json(&node), r#"{"e":"/\b\f\r\tA"}"#);
1107
1108        // a valid surrogate pair assembles the astral char
1109        let node = parse_ordered_json(r#"{"p":"\ud83d\ude00"}"#).expect("pair");
1110        assert_eq!(serialize_compact_json(&node), "{\"p\":\"\u{1F600}\"}");
1111
1112        // lone surrogates decode to the replacement character
1113        let node = parse_ordered_json(r#"{"h":"\ud83d"}"#).expect("lone high");
1114        assert_eq!(serialize_compact_json(&node), "{\"h\":\"\u{FFFD}\"}");
1115        let node = parse_ordered_json(r#"{"l":"\udc00"}"#).expect("lone low");
1116        assert_eq!(serialize_compact_json(&node), "{\"l\":\"\u{FFFD}\"}");
1117    }
1118
1119    #[test]
1120    fn serializer_renders_every_scalar_and_separator() {
1121        let cases = [
1122            (
1123                r#"{"n":1,"t":true,"f":false,"z":null}"#,
1124                "{\"n\":1,\"t\":true,\"f\":false,\"z\":null}",
1125            ),
1126            ("[1,2]", "[1,2]"),
1127            (
1128                r#"{"x":NaN,"y":Infinity,"z":-Infinity}"#,
1129                "{\"x\":NaN,\"y\":Infinity,\"z\":-Infinity}",
1130            ),
1131            (
1132                r#"{"s":"quote\" back\\ nl\ncr\rtab\bbc\ffc"}"#,
1133                "{\"s\":\"quote\\\" back\\\\ nl\\ncr\\rtab\\bbc\\ffc\"}",
1134            ),
1135            (r#"{"o":{"k":[{},[]]}}"#, "{\"o\":{\"k\":[{},[]]}}"),
1136        ];
1137        for (src, want) in cases {
1138            let node = parse_ordered_json(src).expect(src);
1139            assert_eq!(serialize_compact_json(&node), want, "serializing {src}");
1140        }
1141    }
1142}
1143
1144#[cfg(test)]
1145mod gap_tests {
1146    use super::*;
1147    use crate::body_scan::ExcludedBodyFields;
1148
1149    #[test]
1150    fn a_high_surrogate_followed_by_a_non_low_escape_becomes_fffd() {
1151        // the second escape is re-scanned after the rewind, so only the lone
1152        // high surrogate degrades to U+FFFD
1153        let node = parse_ordered_json(r#"{"k":"\ud800\u0041"}"#).expect("parses");
1154        let values = append_json_walk_entries(
1155            Vec::new(),
1156            &node,
1157            REQUEST_BODY_CONTEXT,
1158            ExcludedBodyFields::default(),
1159            JSON_WALK_DEPTH_CAP,
1160        );
1161        // the rewind lands two chars into the failed second escape, so the
1162        // trailing digits rescan as literal text
1163        assert!(values.iter().any(|value| value.content == "\u{FFFD}41"));
1164    }
1165
1166    #[test]
1167    fn a_truncated_unicode_escape_is_malformed() {
1168        // fewer than four hex digits before the end of input
1169        assert!(parse_ordered_json("{\"k\":\"\\u1\"}").is_none());
1170    }
1171
1172    #[test]
1173    fn malformed_number_grammars_are_rejected() {
1174        // a fraction dot without digits
1175        assert!(parse_ordered_json("{\"k\":1.}").is_none());
1176        // an exponent sign without digits
1177        assert!(parse_ordered_json("{\"k\":1e+}").is_none());
1178        // a signed exponent walks
1179        let node = parse_ordered_json("{\"k\":1e+5}").expect("parses");
1180        let values = append_json_walk_entries(
1181            Vec::new(),
1182            &node,
1183            REQUEST_BODY_CONTEXT,
1184            ExcludedBodyFields::default(),
1185            JSON_WALK_DEPTH_CAP,
1186        );
1187        assert!(values.iter().any(|value| value.content == "1e+5"));
1188    }
1189
1190    #[test]
1191    fn an_excluded_entry_key_skips_its_subtree() {
1192        let excluded = std::iter::once("password".to_owned()).collect();
1193        let node = parse_ordered_json(r#"{"password":"secret","keep":"v"}"#).expect("parses");
1194        let values = append_json_walk_entries(
1195            Vec::new(),
1196            &node,
1197            REQUEST_BODY_CONTEXT,
1198            ExcludedBodyFields::new(&excluded),
1199            JSON_WALK_DEPTH_CAP,
1200        );
1201        let keys: Vec<&str> = values.iter().map(|value| value.content.as_str()).collect();
1202        assert!(!keys.contains(&"password"));
1203        assert!(!keys.contains(&"secret"));
1204        assert!(keys.contains(&"keep"));
1205    }
1206
1207    #[test]
1208    fn containers_at_the_depth_cap_serialize_back_to_json() {
1209        // 31 nested arrays put the inner object at walk depth 32
1210        let body = format!("{}{{\"k\":1}}{}", "[".repeat(31), "]".repeat(31));
1211        let node = parse_ordered_json(&body).expect("parses");
1212        let values = append_json_walk_entries(
1213            Vec::new(),
1214            &node,
1215            REQUEST_BODY_CONTEXT,
1216            ExcludedBodyFields::default(),
1217            JSON_WALK_DEPTH_CAP,
1218        );
1219        assert!(values.iter().any(|value| value.content == "{\"k\":1}"));
1220    }
1221}