Skip to main content

reddb_types/utils/
json.rs

1//! Minimal JSON parser and serializer with zero dependencies.
2//! Implements a subset of JSON sufficient for MCP message handling.
3
4use std::fmt;
5
6/// Simplified JSON value representation.
7#[derive(Clone, Debug, PartialEq)]
8pub enum JsonValue {
9    Null,
10    Bool(bool),
11    Integer(i64),
12    Number(f64),
13    Decimal(String),
14    String(String),
15    Array(Vec<JsonValue>),
16    Object(Vec<(String, JsonValue)>),
17}
18
19impl JsonValue {
20    /// Returns the value as string reference if it is a string.
21    pub fn as_str(&self) -> Option<&str> {
22        match self {
23            JsonValue::String(s) => Some(s.as_str()),
24            _ => None,
25        }
26    }
27
28    /// Returns the value as f64 if it is a number.
29    pub fn as_f64(&self) -> Option<f64> {
30        match self {
31            JsonValue::Number(n) => Some(*n),
32            JsonValue::Integer(n) => Some(*n as f64),
33            JsonValue::Decimal(n) => n.parse::<f64>().ok(),
34            _ => None,
35        }
36    }
37
38    /// Returns the value as i64 if it is an exact integer.
39    pub fn as_i64(&self) -> Option<i64> {
40        match self {
41            JsonValue::Integer(n) => Some(*n),
42            _ => None,
43        }
44    }
45
46    /// Returns the value as boolean.
47    pub fn as_bool(&self) -> Option<bool> {
48        match self {
49            JsonValue::Bool(b) => Some(*b),
50            _ => None,
51        }
52    }
53
54    /// Returns the value as array.
55    pub fn as_array(&self) -> Option<&[JsonValue]> {
56        match self {
57            JsonValue::Array(items) => Some(items.as_slice()),
58            _ => None,
59        }
60    }
61
62    /// Returns the value as mutable array.
63    pub fn as_array_mut(&mut self) -> Option<&mut Vec<JsonValue>> {
64        match self {
65            JsonValue::Array(items) => Some(items),
66            _ => None,
67        }
68    }
69
70    /// Returns the object entries if the value is an object.
71    pub fn as_object(&self) -> Option<&[(String, JsonValue)]> {
72        match self {
73            JsonValue::Object(entries) => Some(entries.as_slice()),
74            _ => None,
75        }
76    }
77
78    /// Returns a mutable reference to the object entries.
79    pub fn as_object_mut(&mut self) -> Option<&mut Vec<(String, JsonValue)>> {
80        match self {
81            JsonValue::Object(entries) => Some(entries),
82            _ => None,
83        }
84    }
85
86    /// Retrieves field value from object by key.
87    pub fn get(&self, key: &str) -> Option<&JsonValue> {
88        if let JsonValue::Object(entries) = self {
89            for (k, v) in entries {
90                if k == key {
91                    return Some(v);
92                }
93            }
94        }
95        None
96    }
97
98    /// Retrieves mutable field value from object by key.
99    pub fn get_mut(&mut self, key: &str) -> Option<&mut JsonValue> {
100        if let JsonValue::Object(entries) = self {
101            for (k, v) in entries.iter_mut() {
102                if k == key {
103                    return Some(v);
104                }
105            }
106        }
107        None
108    }
109
110    /// Convenience constructor for JSON objects.
111    pub fn object(entries: Vec<(String, JsonValue)>) -> JsonValue {
112        JsonValue::Object(entries)
113    }
114
115    /// Convenience constructor for JSON arrays.
116    pub fn array(items: Vec<JsonValue>) -> JsonValue {
117        JsonValue::Array(items)
118    }
119
120    /// Serializes the value into a compact JSON string.
121    ///
122    /// **Deprecation note (ADR 0010 / issue #177):** the canonical
123    /// JSON encoder for serialization-boundary-sensitive paths
124    /// (audit log, HelloAck, PayloadReply, anything reaching a
125    /// downstream parser) is `crate::serde_json::Value::escape_string`
126    /// using `to_string_compact`. This local encoder is correct after
127    /// the F-01 hotfix (#181) but is not the canonical owner; new
128    /// audit / wire emission code should not call it. Existing MCP
129    /// JSON-RPC callers may keep using it pending a follow-up
130    /// retirement slice.
131    #[deprecated(
132        note = "Use crate::serde_json::Value::to_string_compact for boundary emission; see ADR 0010 / issue #177"
133    )]
134    pub fn to_json_string(&self) -> String {
135        let mut out = String::new();
136        self.write_json(&mut out);
137        out
138    }
139
140    fn write_json(&self, out: &mut String) {
141        match self {
142            JsonValue::Null => out.push_str("null"),
143            JsonValue::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
144            JsonValue::Integer(n) => out.push_str(&format!("{n}")),
145            JsonValue::Number(n) => {
146                if n.fract() == 0.0 {
147                    out.push_str(&format!("{}", *n as i64));
148                } else {
149                    out.push_str(&format!("{}", n));
150                }
151            }
152            JsonValue::Decimal(n) => out.push_str(n),
153            JsonValue::String(s) => {
154                out.push('"');
155                for ch in s.chars() {
156                    match ch {
157                        '"' => out.push_str("\\\""),
158                        '\\' => out.push_str("\\\\"),
159                        '\n' => out.push_str("\\n"),
160                        '\r' => out.push_str("\\r"),
161                        '\t' => out.push_str("\\t"),
162                        c if c.is_control() => {
163                            out.push_str(&format!("\\u{:04x}", c as u32));
164                        }
165                        c => out.push(c),
166                    }
167                }
168                out.push('"');
169            }
170            JsonValue::Array(items) => {
171                out.push('[');
172                for (idx, item) in items.iter().enumerate() {
173                    if idx > 0 {
174                        out.push(',');
175                    }
176                    item.write_json(out);
177                }
178                out.push(']');
179            }
180            JsonValue::Object(entries) => {
181                out.push('{');
182                for (idx, (key, value)) in entries.iter().enumerate() {
183                    if idx > 0 {
184                        out.push(',');
185                    }
186                    JsonValue::String(key.clone()).write_json(out);
187                    out.push(':');
188                    value.write_json(out);
189                }
190                out.push('}');
191            }
192        }
193    }
194}
195
196impl From<&str> for JsonValue {
197    fn from(value: &str) -> JsonValue {
198        JsonValue::String(value.to_string())
199    }
200}
201
202impl From<String> for JsonValue {
203    fn from(value: String) -> JsonValue {
204        JsonValue::String(value)
205    }
206}
207
208impl From<bool> for JsonValue {
209    fn from(value: bool) -> JsonValue {
210        JsonValue::Bool(value)
211    }
212}
213
214impl From<f64> for JsonValue {
215    fn from(value: f64) -> JsonValue {
216        JsonValue::Number(value)
217    }
218}
219
220impl From<i64> for JsonValue {
221    fn from(value: i64) -> JsonValue {
222        JsonValue::Integer(value)
223    }
224}
225
226impl From<usize> for JsonValue {
227    fn from(value: usize) -> JsonValue {
228        i64::try_from(value)
229            .map(JsonValue::Integer)
230            .unwrap_or_else(|_| JsonValue::Number(value as f64))
231    }
232}
233
234impl fmt::Display for JsonValue {
235    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
236        // Internal route — Display is the legacy entry point and
237        // routes through the (now deprecated for boundary use)
238        // `to_json_string`. Silence the warning here so the lint
239        // surfaces only at external call sites.
240        #[allow(deprecated)]
241        let s = self.to_json_string();
242        write!(f, "{s}")
243    }
244}
245
246/// Parser for JSON strings into [`JsonValue`].
247pub struct JsonParser<'a> {
248    input: &'a [u8],
249    pos: usize,
250    /// Current array/object nesting depth. Bounded by
251    /// [`JSON_MAX_NESTING_DEPTH`] so a hostile body such as `[[[[…` cannot
252    /// overflow the stack of the (recursive) parser before authentication.
253    depth: u32,
254}
255
256/// Maximum array/object nesting the parser accepts. Mirrors
257/// `reddb_rql::limits::JSON_LITERAL_MAX_DEPTH` so JSON reaching the engine
258/// through HTTP bodies and through SQL literals is bounded identically.
259pub const JSON_MAX_NESTING_DEPTH: u32 = 128;
260
261impl<'a> JsonParser<'a> {
262    /// Creates a new parser from input slice.
263    pub fn new(input: &'a str) -> Self {
264        Self {
265            input: input.as_bytes(),
266            pos: 0,
267            depth: 0,
268        }
269    }
270
271    /// Enter one nesting level; errors once the depth cap is reached.
272    fn push_depth(&mut self) -> Result<(), String> {
273        if self.depth >= JSON_MAX_NESTING_DEPTH {
274            return Err(format!(
275                "json nesting depth exceeds {JSON_MAX_NESTING_DEPTH}"
276            ));
277        }
278        self.depth += 1;
279        Ok(())
280    }
281
282    /// Parses a JSON value from the current position.
283    pub fn parse_value(&mut self) -> Result<JsonValue, String> {
284        self.skip_whitespace();
285        if self.eof() {
286            return Err("unexpected end of input".to_string());
287        }
288        let ch = self.current_char();
289        match ch {
290            b'n' => self.parse_null(),
291            b't' | b'f' => self.parse_bool(),
292            b'-' | b'0'..=b'9' => self.parse_number(),
293            b'"' => self.parse_string().map(JsonValue::String),
294            b'[' => {
295                self.push_depth()?;
296                let value = self.parse_array();
297                self.depth -= 1;
298                value
299            }
300            b'{' => {
301                self.push_depth()?;
302                let value = self.parse_object();
303                self.depth -= 1;
304                value
305            }
306            _ => Err(format!("unexpected character '{}'", ch as char)),
307        }
308    }
309
310    fn parse_null(&mut self) -> Result<JsonValue, String> {
311        self.expect_bytes(b"null")?;
312        Ok(JsonValue::Null)
313    }
314
315    fn parse_bool(&mut self) -> Result<JsonValue, String> {
316        if self.matches_bytes(b"true") {
317            self.pos += 4;
318            Ok(JsonValue::Bool(true))
319        } else if self.matches_bytes(b"false") {
320            self.pos += 5;
321            Ok(JsonValue::Bool(false))
322        } else {
323            Err("invalid boolean literal".to_string())
324        }
325    }
326
327    fn parse_number(&mut self) -> Result<JsonValue, String> {
328        let start = self.pos;
329        if self.current_char() == b'-' {
330            self.pos += 1;
331        }
332        if self.eof() {
333            return Err("invalid number literal".to_string());
334        }
335
336        match self.current_char() {
337            b'0' => {
338                self.pos += 1;
339            }
340            b'1'..=b'9' => {
341                self.pos += 1;
342                while !self.eof() && self.current_char().is_ascii_digit() {
343                    self.pos += 1;
344                }
345            }
346            _ => return Err("invalid number literal".to_string()),
347        }
348
349        let mut is_float = false;
350
351        if !self.eof() && self.current_char() == b'.' {
352            is_float = true;
353            self.pos += 1;
354            if self.eof() || !self.current_char().is_ascii_digit() {
355                return Err("invalid number literal".to_string());
356            }
357            while !self.eof() && self.current_char().is_ascii_digit() {
358                self.pos += 1;
359            }
360        }
361
362        if !self.eof() && (self.current_char() == b'e' || self.current_char() == b'E') {
363            is_float = true;
364            self.pos += 1;
365            if !self.eof() && (self.current_char() == b'+' || self.current_char() == b'-') {
366                self.pos += 1;
367            }
368            if self.eof() || !self.current_char().is_ascii_digit() {
369                return Err("invalid number literal".to_string());
370            }
371            while !self.eof() && self.current_char().is_ascii_digit() {
372                self.pos += 1;
373            }
374        }
375
376        let slice = &self.input[start..self.pos];
377        let s = std::str::from_utf8(slice).map_err(|_| "invalid UTF-8 in number".to_string())?;
378        if !is_float {
379            if let Ok(value) = s.parse::<i64>() {
380                return Ok(JsonValue::Integer(value));
381            }
382            return Ok(JsonValue::Decimal(s.to_string()));
383        }
384        if should_preserve_decimal_text(s) {
385            return Ok(JsonValue::Decimal(s.to_string()));
386        }
387        let value = s
388            .parse::<f64>()
389            .map_err(|_| "failed to parse number".to_string())?;
390        if !value.is_finite() {
391            return Ok(JsonValue::Decimal(s.to_string()));
392        }
393        Ok(JsonValue::Number(value))
394    }
395
396    fn parse_string(&mut self) -> Result<String, String> {
397        self.expect_char(b'"')?;
398        let mut result = String::new();
399        while !self.eof() {
400            let ch = self.current_char();
401            match ch {
402                b'"' => {
403                    self.pos += 1;
404                    return Ok(result);
405                }
406                b'\\' => {
407                    self.pos += 1;
408                    if self.eof() {
409                        return Err("unexpected end of input in escape".to_string());
410                    }
411                    let esc = self.current_char();
412                    self.pos += 1;
413                    match esc {
414                        b'"' => result.push('"'),
415                        b'\\' => result.push('\\'),
416                        b'/' => result.push('/'),
417                        b'b' => result.push('\x08'),
418                        b'f' => result.push('\x0c'),
419                        b'n' => result.push('\n'),
420                        b'r' => result.push('\r'),
421                        b't' => result.push('\t'),
422                        b'u' => {
423                            // RFC 8259 §7: `\uXXXX` covers code points in
424                            // the BMP. Code points above U+FFFF (e.g. emoji,
425                            // U+1F4A9 PILE OF POO) are represented in JSON
426                            // as a UTF-16 surrogate pair `💩` and
427                            // MUST be decoded as one Unicode scalar.
428                            let code = self.parse_unicode_escape()?;
429                            if (0xD800..=0xDBFF).contains(&code) {
430                                // High surrogate — must be followed by a
431                                // `\uXXXX` low surrogate.
432                                if self.pos + 2 > self.input.len()
433                                    || self.input[self.pos] != b'\\'
434                                    || self.input[self.pos + 1] != b'u'
435                                {
436                                    return Err(
437                                        "expected low surrogate after high surrogate".to_string()
438                                    );
439                                }
440                                self.pos += 2;
441                                let low = self.parse_unicode_escape()?;
442                                if !(0xDC00..=0xDFFF).contains(&low) {
443                                    return Err("invalid low surrogate".to_string());
444                                }
445                                let scalar = 0x10000 + (((code - 0xD800) << 10) | (low - 0xDC00));
446                                if let Some(chr) = char::from_u32(scalar) {
447                                    result.push(chr);
448                                } else {
449                                    return Err("invalid unicode escape".to_string());
450                                }
451                            } else if (0xDC00..=0xDFFF).contains(&code) {
452                                return Err(
453                                    "unexpected low surrogate without preceding high surrogate"
454                                        .to_string(),
455                                );
456                            } else if let Some(chr) = char::from_u32(code) {
457                                result.push(chr);
458                            } else {
459                                return Err("invalid unicode escape".to_string());
460                            }
461                        }
462                        _ => return Err("invalid escape sequence".to_string()),
463                    }
464                }
465                _ if ch < 0x80 => {
466                    // ASCII fast path: byte == codepoint.
467                    self.pos += 1;
468                    result.push(ch as char);
469                }
470                _ => {
471                    // Multi-byte UTF-8 sequence. The parser's input came
472                    // from `&str` (validated UTF-8 at JsonParser::new), so
473                    // a complete codepoint starts here. Decoding byte-by-
474                    // byte via `ch as char` would map each continuation
475                    // byte to a Latin-1 codepoint, producing mojibake (the
476                    // root cause of issue #191 — `é` → `é`, `🦀` → four
477                    // garbled chars). Instead, lift a full codepoint out
478                    // of the underlying UTF-8 stream.
479                    let next = std::str::from_utf8(&self.input[self.pos..])
480                        .map_err(|_| "invalid utf-8 in string body".to_string())?
481                        .chars()
482                        .next()
483                        .ok_or_else(|| "unterminated string literal".to_string())?;
484                    self.pos += next.len_utf8();
485                    result.push(next);
486                }
487            }
488        }
489        Err("unterminated string literal".to_string())
490    }
491
492    fn parse_unicode_escape(&mut self) -> Result<u32, String> {
493        if self.pos + 4 > self.input.len() {
494            return Err("invalid unicode escape".to_string());
495        }
496        let mut value = 0u32;
497        for _ in 0..4 {
498            let ch = self.current_char();
499            self.pos += 1;
500            value <<= 4;
501            value |= match ch {
502                b'0'..=b'9' => (ch - b'0') as u32,
503                b'a'..=b'f' => (ch - b'a' + 10) as u32,
504                b'A'..=b'F' => (ch - b'A' + 10) as u32,
505                _ => return Err("invalid unicode escape".to_string()),
506            };
507        }
508        Ok(value)
509    }
510
511    fn parse_array(&mut self) -> Result<JsonValue, String> {
512        self.expect_char(b'[')?;
513        let mut items = Vec::new();
514        self.skip_whitespace();
515        if self.peek_char() == Some(b']') {
516            self.pos += 1;
517            return Ok(JsonValue::Array(items));
518        }
519        loop {
520            let value = self.parse_value()?;
521            items.push(value);
522            self.skip_whitespace();
523            match self.peek_char() {
524                Some(b',') => {
525                    self.pos += 1;
526                }
527                Some(b']') => {
528                    self.pos += 1;
529                    break;
530                }
531                _ => return Err("expected ',' or ']' in array".to_string()),
532            }
533        }
534        Ok(JsonValue::Array(items))
535    }
536
537    fn parse_object(&mut self) -> Result<JsonValue, String> {
538        self.expect_char(b'{')?;
539        let mut entries = Vec::new();
540        self.skip_whitespace();
541        if self.peek_char() == Some(b'}') {
542            self.pos += 1;
543            return Ok(JsonValue::Object(entries));
544        }
545        loop {
546            self.skip_whitespace();
547            let key = self.parse_string()?;
548            self.skip_whitespace();
549            self.expect_char(b':')?;
550            let value = self.parse_value()?;
551            entries.push((key, value));
552            self.skip_whitespace();
553            match self.peek_char() {
554                Some(b',') => {
555                    self.pos += 1;
556                }
557                Some(b'}') => {
558                    self.pos += 1;
559                    break;
560                }
561                _ => return Err("expected ',' or '}' in object".to_string()),
562            }
563        }
564        Ok(JsonValue::Object(entries))
565    }
566
567    fn skip_whitespace(&mut self) {
568        while !self.eof() {
569            match self.current_char() {
570                b' ' | b'\n' | b'\r' | b'\t' => self.pos += 1,
571                _ => break,
572            }
573        }
574    }
575
576    fn expect_bytes(&mut self, expected: &[u8]) -> Result<(), String> {
577        if self.remaining().starts_with(expected) {
578            self.pos += expected.len();
579            Ok(())
580        } else {
581            Err("unexpected token".to_string())
582        }
583    }
584
585    fn matches_bytes(&self, expected: &[u8]) -> bool {
586        self.remaining().starts_with(expected)
587    }
588
589    fn expect_char(&mut self, expected: u8) -> Result<(), String> {
590        if self.peek_char() == Some(expected) {
591            self.pos += 1;
592            Ok(())
593        } else {
594            Err(format!("expected '{}'", expected as char))
595        }
596    }
597
598    fn peek_char(&self) -> Option<u8> {
599        if self.pos >= self.input.len() {
600            None
601        } else {
602            Some(self.input[self.pos])
603        }
604    }
605
606    fn current_char(&self) -> u8 {
607        self.input[self.pos]
608    }
609
610    fn remaining(&self) -> &[u8] {
611        &self.input[self.pos..]
612    }
613
614    fn eof(&self) -> bool {
615        self.pos >= self.input.len()
616    }
617}
618
619fn should_preserve_decimal_text(input: &str) -> bool {
620    let significant_digits = input.bytes().filter(u8::is_ascii_digit).count();
621    significant_digits > 15
622}
623
624/// Parses the provided JSON string into a [`JsonValue`].
625pub fn parse_json(input: &str) -> Result<JsonValue, String> {
626    let mut parser = JsonParser::new(input);
627    let value = parser.parse_value()?;
628    parser.skip_whitespace();
629    if parser.eof() {
630        Ok(value)
631    } else {
632        Err("unexpected trailing data".to_string())
633    }
634}
635
636#[cfg(test)]
637mod tests {
638    use super::*;
639    use proptest::prelude::*;
640
641    #[test]
642    fn parse_simple_object() {
643        let json = r#"{"name":"reddb","active":true,"count":3}"#;
644        let value = parse_json(json).unwrap();
645        let obj = value.as_object().unwrap();
646        assert_eq!(obj.len(), 3);
647    }
648
649    #[test]
650    fn parse_nested_array() {
651        let json = r#"{"items":[1,2,{"flag":false}]}"#;
652        let value = parse_json(json).unwrap();
653        assert!(value.get("items").unwrap().as_array().is_some());
654    }
655
656    #[test]
657    fn stringify_roundtrip() {
658        let json = r#"{"message":"hello","value":42}"#;
659        let value = parse_json(json).unwrap();
660        #[allow(deprecated)]
661        let output = value.to_json_string();
662        assert!(output.contains("hello"));
663    }
664
665    #[test]
666    fn constructors_accessors_mutators_and_display_cover_all_shapes() {
667        let mut value = JsonValue::object(vec![
668            ("name".to_string(), JsonValue::from("reddb")),
669            ("active".to_string(), JsonValue::from(true)),
670            ("count".to_string(), JsonValue::from(3usize)),
671            ("ratio".to_string(), JsonValue::from(1.5f64)),
672            (
673                "items".to_string(),
674                JsonValue::array(vec![JsonValue::Null, JsonValue::from(2i64)]),
675            ),
676        ]);
677
678        assert_eq!(value.get("name").and_then(JsonValue::as_str), Some("reddb"));
679        assert_eq!(value.get("active").and_then(JsonValue::as_bool), Some(true));
680        assert_eq!(value.get("count").and_then(JsonValue::as_f64), Some(3.0));
681        assert_eq!(
682            value
683                .get("items")
684                .and_then(JsonValue::as_array)
685                .map(<[_]>::len),
686            Some(2)
687        );
688        assert_eq!(value.as_object().map(<[_]>::len), Some(5));
689        assert!(JsonValue::Null.as_str().is_none());
690        assert!(JsonValue::Null.as_bool().is_none());
691        assert!(JsonValue::Null.as_array().is_none());
692        assert!(JsonValue::Null.as_object().is_none());
693
694        value
695            .get_mut("count")
696            .map(|slot| *slot = JsonValue::from(4usize))
697            .unwrap();
698        assert_eq!(value.get("count").and_then(JsonValue::as_f64), Some(4.0));
699
700        value
701            .as_object_mut()
702            .unwrap()
703            .push(("extra".to_string(), JsonValue::from("ok".to_string())));
704        let mut array = JsonValue::array(vec![JsonValue::from(1usize)]);
705        array.as_array_mut().unwrap().push(JsonValue::from(2usize));
706        assert_eq!(array.as_array().unwrap().len(), 2);
707
708        #[allow(deprecated)]
709        let encoded =
710            JsonValue::String("quote\" slash\\ tab\t ctrl\x01".to_string()).to_json_string();
711        assert!(encoded.contains("\\\""));
712        assert!(encoded.contains("\\\\"));
713        assert!(encoded.contains("\\t"));
714        assert!(encoded.contains("\\u0001"));
715        assert_eq!(format!("{}", JsonValue::Bool(false)), "false");
716    }
717
718    #[test]
719    fn parser_accepts_scalars_escapes_empty_containers_and_trailing_space() {
720        assert_eq!(parse_json(" null \n").unwrap(), JsonValue::Null);
721        assert_eq!(parse_json("true").unwrap(), JsonValue::Bool(true));
722        assert_eq!(parse_json("false").unwrap(), JsonValue::Bool(false));
723        assert_eq!(parse_json("-12.5e+2").unwrap(), JsonValue::Number(-1250.0));
724        assert_eq!(
725            parse_json("3.14159265358979323846").unwrap(),
726            JsonValue::Decimal("3.14159265358979323846".to_string())
727        );
728        assert_eq!(
729            parse_json("18446744073709551616").unwrap(),
730            JsonValue::Decimal("18446744073709551616".to_string())
731        );
732        assert_eq!(parse_json("[]").unwrap(), JsonValue::Array(Vec::new()));
733        assert_eq!(parse_json("{}").unwrap(), JsonValue::Object(Vec::new()));
734
735        let escaped = parse_json(r#""\"\\\/\b\f\n\r\t\u00e9\uD83D\uDCA9""#).unwrap();
736        let text = escaped.as_str().unwrap();
737        assert!(text.contains('"'));
738        assert!(text.contains('\\'));
739        assert!(text.contains('/'));
740        assert!(text.contains('\x08'));
741        assert!(text.contains('\x0c'));
742        assert!(text.contains('\n'));
743        assert!(text.contains('\r'));
744        assert!(text.contains('\t'));
745        assert!(text.chars().any(|ch| ch as u32 == 0x00E9));
746        assert_eq!(text.chars().last(), char::from_u32(0x1F4A9));
747    }
748
749    #[test]
750    fn parser_rejects_invalid_tokens_numbers_strings_and_separators() {
751        let cases = [
752            "",
753            "truth",
754            "nul",
755            "-",
756            "01",
757            "1.",
758            "1e",
759            r#""unterminated"#,
760            r#""\x""#,
761            r#""\u12""#,
762            r#""\u12zz""#,
763            r#""\uD800""#,
764            r#""\uD800\u0000""#,
765            r#""\uDC00""#,
766            "[1 2]",
767            "[1,]",
768            r#"{"a" 1}"#,
769            r#"{"a":1 "b":2}"#,
770            "{} trailing",
771            "?",
772        ];
773
774        for case in cases {
775            assert!(parse_json(case).is_err(), "{case:?}");
776        }
777    }
778
779    proptest! {
780        #[test]
781        fn beyond_native_decimal_text_round_trips(
782            prefix in "[1-9][0-9]{18,36}",
783            suffix in "[0-9]{1,24}"
784        ) {
785            let input = format!("{prefix}.{suffix}");
786            let parsed = parse_json(&input).expect("generated decimal parses");
787            prop_assert_eq!(&parsed, &JsonValue::Decimal(input.clone()));
788            #[allow(deprecated)]
789            let output = parsed.to_json_string();
790            prop_assert_eq!(output, input);
791        }
792    }
793
794    #[test]
795    fn deeply_nested_json_is_rejected_instead_of_overflowing_the_stack() {
796        // `parse_value` recurses through arrays and objects, and the HTTP
797        // edge feeds it 32 MiB bodies on unauthenticated routes
798        // (`/auth/login`, `/auth/bootstrap`). Without a depth cap a body of
799        // a few hundred thousand `[` aborts the process.
800        let bomb = "[".repeat((JSON_MAX_NESTING_DEPTH as usize) + 5);
801        let err = parse_json(&bomb).expect_err("nesting bomb must be refused");
802        assert!(err.contains("nesting depth"), "unexpected error: {err}");
803
804        let object_bomb = "{\"a\":".repeat((JSON_MAX_NESTING_DEPTH as usize) + 5);
805        let err = parse_json(&object_bomb).expect_err("object nesting bomb must be refused");
806        assert!(err.contains("nesting depth"), "unexpected error: {err}");
807    }
808
809    #[test]
810    fn nesting_just_below_the_cap_still_parses() {
811        let depth = (JSON_MAX_NESTING_DEPTH as usize) - 1;
812        let doc = format!("{}{}", "[".repeat(depth), "]".repeat(depth));
813        parse_json(&doc).expect("documents under the cap must still parse");
814    }
815}