Skip to main content

inillucent_cli/
json.rs

1//! JSON, first-party.
2//!
3//! Invariant: this crate does not take a JSON dependency. `serde_json` is
4//! allow-listed in `docs/invariants/layering.toml` for `inillucent-core` and
5//! `inillucent-bench` only, and the reason is the one the dependency policy
6//! gives for every other row - a production crate carries what it can argue
7//! for. JSON is a grammar that fits on a napkin, the shell has been writing it
8//! since `.mode json` existed, and the only new thing this ticket needs is the
9//! other direction: an MCP server has to *read* a request.
10//!
11//! So this module holds both halves, and `render.rs`'s escaper moved into it
12//! rather than being copied. Two JSON escapers in one crate is precisely the
13//! kind of near-duplicate this repository's tests exist to catch, and the one
14//! that would have been introduced here is the one that matters - the shell's
15//! `.mode json` output and an MCP tool result would have disagreed about a
16//! control character, in a way only a corpus with a tab in it would ever show.
17//!
18//! What is deliberately *not* here: streaming, arbitrary precision, and object
19//! key lookup by hash. An MCP request is a few hundred bytes and a tool result
20//! is written once.
21
22/// A JSON value.
23///
24/// An object keeps its pairs in the order they were written, because a result
25/// whose keys shuffle between runs cannot be compared byte for byte, and byte
26/// comparison is how everything else in this repository is checked.
27#[derive(Debug, Clone, PartialEq)]
28pub enum Json {
29    /// `null`.
30    Null,
31    /// `true` or `false`.
32    Bool(bool),
33    /// A number that arrived, or is being written, as an integer.
34    Int(i64),
35    /// A number with a fractional part or an exponent.
36    Real(f64),
37    /// A string, already unescaped.
38    Text(String),
39    /// An array.
40    Array(Vec<Json>),
41    /// An object, in insertion order.
42    Object(Vec<(String, Json)>),
43}
44
45impl Json {
46    /// Returns the value stored under a key, when this is an object that has one.
47    ///
48    /// @param key - the member name
49    pub fn get(&self, key: &str) -> Option<&Json> {
50        match self {
51            Json::Object(pairs) => pairs
52                .iter()
53                .find(|(name, _)| name == key)
54                .map(|(_, value)| value),
55            _ => None,
56        }
57    }
58
59    /// Returns this value as a string, for the variants that have one.
60    ///
61    /// A number or a boolean answers `None` rather than its rendering: a
62    /// parameter declared as text and given `7` is a caller's mistake, and
63    /// quietly reading it as `"7"` hides the mistake until the SQL is wrong.
64    pub fn text(&self) -> Option<&str> {
65        match self {
66            Json::Text(text) => Some(text),
67            _ => None,
68        }
69    }
70
71    /// Returns this value as an integer, accepting a whole-numbered real.
72    pub fn integer(&self) -> Option<i64> {
73        match self {
74            Json::Int(number) => Some(*number),
75            // A JSON writer that has no integer type - JavaScript's, which is
76            // most of them - writes `5` as a double. Refusing it would refuse
77            // every `limit` an MCP client sends.
78            Json::Real(number) if number.fract() == 0.0 => Some(*number as i64),
79            _ => None,
80        }
81    }
82
83    /// Returns this value as a boolean.
84    pub fn boolean(&self) -> Option<bool> {
85        match self {
86            Json::Bool(value) => Some(*value),
87            _ => None,
88        }
89    }
90
91    /// Returns the elements, when this is an array.
92    pub fn array(&self) -> Option<&[Json]> {
93        match self {
94            Json::Array(items) => Some(items),
95            _ => None,
96        }
97    }
98
99    /// Writes this value as compact JSON.
100    pub fn write(&self) -> String {
101        let mut out = String::new();
102        self.write_into(&mut out);
103        out
104    }
105
106    /// Appends this value's rendering to a buffer.
107    ///
108    /// @param out - the buffer to append to
109    fn write_into(&self, out: &mut String) {
110        match self {
111            Json::Null => out.push_str("null"),
112            Json::Bool(true) => out.push_str("true"),
113            Json::Bool(false) => out.push_str("false"),
114            Json::Int(number) => out.push_str(&number.to_string()),
115            Json::Real(number) => out.push_str(&real(*number)),
116            Json::Text(text) => {
117                out.push('"');
118                out.push_str(&escape(text));
119                out.push('"');
120            }
121            Json::Array(items) => {
122                out.push('[');
123                for (nth, item) in items.iter().enumerate() {
124                    if nth > 0 {
125                        out.push(',');
126                    }
127                    item.write_into(out);
128                }
129                out.push(']');
130            }
131            Json::Object(pairs) => {
132                out.push('{');
133                for (nth, (name, value)) in pairs.iter().enumerate() {
134                    if nth > 0 {
135                        out.push(',');
136                    }
137                    out.push('"');
138                    out.push_str(&escape(name));
139                    out.push_str("\":");
140                    value.write_into(out);
141                }
142                out.push('}');
143            }
144        }
145    }
146
147    /// Writes this value indented, for a human reading a result on a terminal.
148    ///
149    /// @param depth - how many levels in this value sits
150    pub fn pretty(&self, depth: usize) -> String {
151        let pad = "  ".repeat(depth + 1);
152        let close = "  ".repeat(depth);
153        match self {
154            Json::Array(items) if !items.is_empty() => {
155                let inner: Vec<String> = items
156                    .iter()
157                    .map(|item| format!("{pad}{}", item.pretty(depth + 1)))
158                    .collect();
159                format!("[\n{}\n{close}]", inner.join(",\n"))
160            }
161            Json::Object(pairs) if !pairs.is_empty() => {
162                let inner: Vec<String> = pairs
163                    .iter()
164                    .map(|(name, value)| {
165                        format!("{pad}\"{}\": {}", escape(name), value.pretty(depth + 1))
166                    })
167                    .collect();
168                format!("{{\n{}\n{close}}}", inner.join(",\n"))
169            }
170            other => other.write(),
171        }
172    }
173}
174
175/// Renders a double the way JSON allows, which is never `NaN` or `Infinity`.
176///
177/// Neither has a JSON spelling, and a writer that emits the bare word produces
178/// a document no parser will read back. A value the grammar cannot hold becomes
179/// `null`, which is what every other JSON writer does with them.
180///
181/// @param number - the value to render
182fn real(number: f64) -> String {
183    if number.is_finite() {
184        let rendered = format!("{number}");
185        // `1` must not be written where `1.0` was meant: a reader that types
186        // its result from the document would call it an integer.
187        if rendered.contains(['.', 'e', 'E']) {
188            rendered
189        } else {
190            format!("{rendered}.0")
191        }
192    } else {
193        "null".to_string()
194    }
195}
196
197/// Escapes the characters a JSON string may not carry raw.
198///
199/// One of three identical copies until task-1946's M4. The answer lives in
200/// `inillucent_base::json` now; this name stays because the rest of this module
201/// calls it.
202///
203/// @param text - the string to escape
204pub fn escape(text: &str) -> String {
205    inillucent_base::json::escape(text)
206}
207
208/// Builds an object from pairs, so a caller writes one line instead of five.
209///
210/// @param pairs - the members, in the order they should be written
211pub fn object(pairs: Vec<(&str, Json)>) -> Json {
212    Json::Object(
213        pairs
214            .into_iter()
215            .map(|(name, value)| (name.to_string(), value))
216            .collect(),
217    )
218}
219
220/// Builds a JSON string.
221///
222/// @param text - the contents
223pub fn text(text: impl Into<String>) -> Json {
224    Json::Text(text.into())
225}
226
227/// Reads a JSON document, returning what it holds or why it could not be read.
228///
229/// @param source - the document
230pub fn parse(source: &str) -> Result<Json, String> {
231    let characters: Vec<char> = source.chars().collect();
232    let mut reader = Reader {
233        characters: &characters,
234        at: 0,
235        depth: 0,
236    };
237    reader.skip_space();
238    let value = reader.value()?;
239    reader.skip_space();
240    if reader.at < reader.characters.len() {
241        return Err(format!("trailing input at character {}", reader.at));
242    }
243    Ok(value)
244}
245
246/// A cursor over the document's characters.
247struct Reader<'a> {
248    /// The document.
249    characters: &'a [char],
250    /// How far in the cursor sits.
251    at: usize,
252    /// How many objects and arrays are open around the cursor.
253    ///
254    /// **Bounded, because this parser is recursive and its input is not
255    /// trusted** (task-2066 ยง4.1.6). `value` calls `object` and `array`, each
256    /// of which calls `value`, and nothing counted the nesting. A 240 KB line
257    /// of 120,000 `[` overflowed the stack at exit 127 - in the CLI through
258    /// `--params`, and in `inillucent-mcp` through a request line well inside
259    /// the 1 MiB `MAX_REQUEST_BYTES`, which bounds the line and not what is
260    /// inside it. With `panic = "abort"` a stack overflow is not catchable, so
261    /// the server died with one line on stderr and the requests after it were
262    /// never answered.
263    ///
264    /// Charging it here also stops the recursive `Drop` of a deep `Json`
265    /// overflowing on the way out, which a check made anywhere later would not.
266    depth: usize,
267}
268
269/// How deeply an object or array may nest.
270///
271/// A thousand, matching `MAX_DEPTH` in `inillucent-scalar/src/json/parse.rs`.
272/// The two parsers read the same grammar from different callers, and a document
273/// the SQL `json_valid()` refuses cleanly should not be one that ends this
274/// process.
275const MAX_DEPTH: usize = 1000;
276
277impl Reader<'_> {
278    /// Returns the character under the cursor without consuming it.
279    fn peek(&self) -> Option<char> {
280        self.characters.get(self.at).copied()
281    }
282
283    /// Consumes and returns the character under the cursor.
284    fn next(&mut self) -> Option<char> {
285        let character = self.peek();
286        if character.is_some() {
287            self.at += 1;
288        }
289        character
290    }
291
292    /// Advances past any whitespace.
293    fn skip_space(&mut self) {
294        while matches!(self.peek(), Some(character) if character.is_whitespace()) {
295            self.at += 1;
296        }
297    }
298
299    /// Consumes an expected character, or says which one was missing.
300    ///
301    /// @param wanted - the character the grammar requires here
302    fn expect(&mut self, wanted: char) -> Result<(), String> {
303        match self.next() {
304            Some(character) if character == wanted => Ok(()),
305            Some(character) => Err(format!(
306                "expected '{wanted}' at character {}, found '{character}'",
307                self.at - 1
308            )),
309            None => Err(format!("expected '{wanted}', found end of input")),
310        }
311    }
312
313    /// Charges one level of nesting, refusing past the bound.
314    ///
315    /// Paired with `ascend` around the *body* of `object` and `array` rather
316    /// than held as a guard, because a guard borrowing the reader would stop
317    /// the body reading from it at all. The pairing is in one place in each,
318    /// with the body in its own function, so no `?` can leave a level charged -
319    /// and a counter that leaks only on the error path is a parser that starts
320    /// refusing valid documents after it has seen an invalid one.
321    fn descend(&mut self) -> Result<(), String> {
322        if self.depth >= MAX_DEPTH {
323            return Err(format!(
324                "nested more than {MAX_DEPTH} deep at character {}",
325                self.at
326            ));
327        }
328        self.depth = self.depth.saturating_add(1);
329        Ok(())
330    }
331
332    /// Gives one level of nesting back.
333    fn ascend(&mut self) {
334        self.depth = self.depth.saturating_sub(1);
335    }
336
337    /// Reads one value.
338    fn value(&mut self) -> Result<Json, String> {
339        match self.peek() {
340            Some('{') => self.object(),
341            Some('[') => self.array(),
342            Some('"') => Ok(Json::Text(self.string()?)),
343            Some('t') => self.word("true", Json::Bool(true)),
344            Some('f') => self.word("false", Json::Bool(false)),
345            Some('n') => self.word("null", Json::Null),
346            Some(character) if character == '-' || character.is_ascii_digit() => self.number(),
347            Some(character) => Err(format!("unexpected '{character}' at character {}", self.at)),
348            None => Err("unexpected end of input".to_string()),
349        }
350    }
351
352    /// Reads one of the three bare words.
353    ///
354    /// @param word - the literal expected here
355    /// @param value - what it means
356    fn word(&mut self, word: &str, value: Json) -> Result<Json, String> {
357        for wanted in word.chars() {
358            self.expect(wanted)?;
359        }
360        Ok(value)
361    }
362
363    /// Reads an object.
364    fn object(&mut self) -> Result<Json, String> {
365        self.descend()?;
366        let produced = self.object_body();
367        self.ascend();
368        produced
369    }
370
371    /// Reads an object's contents, with its level already charged.
372    fn object_body(&mut self) -> Result<Json, String> {
373        self.expect('{')?;
374        let mut pairs = Vec::new();
375        self.skip_space();
376        if self.peek() == Some('}') {
377            self.at += 1;
378            return Ok(Json::Object(pairs));
379        }
380        loop {
381            self.skip_space();
382            let name = self.string()?;
383            self.skip_space();
384            self.expect(':')?;
385            self.skip_space();
386            let value = self.value()?;
387            pairs.push((name, value));
388            self.skip_space();
389            match self.next() {
390                Some(',') => continue,
391                Some('}') => return Ok(Json::Object(pairs)),
392                Some(character) => {
393                    return Err(format!(
394                        "expected ',' or '}}' at character {}, found '{character}'",
395                        self.at - 1
396                    ))
397                }
398                None => return Err("unterminated object".to_string()),
399            }
400        }
401    }
402
403    /// Reads an array.
404    fn array(&mut self) -> Result<Json, String> {
405        self.descend()?;
406        let produced = self.array_body();
407        self.ascend();
408        produced
409    }
410
411    /// Reads an array's contents, with its level already charged.
412    fn array_body(&mut self) -> Result<Json, String> {
413        self.expect('[')?;
414        let mut items = Vec::new();
415        self.skip_space();
416        if self.peek() == Some(']') {
417            self.at += 1;
418            return Ok(Json::Array(items));
419        }
420        loop {
421            self.skip_space();
422            items.push(self.value()?);
423            self.skip_space();
424            match self.next() {
425                Some(',') => continue,
426                Some(']') => return Ok(Json::Array(items)),
427                Some(character) => {
428                    return Err(format!(
429                        "expected ',' or ']' at character {}, found '{character}'",
430                        self.at - 1
431                    ))
432                }
433                None => return Err("unterminated array".to_string()),
434            }
435        }
436    }
437
438    /// Reads a string, resolving its escapes.
439    fn string(&mut self) -> Result<String, String> {
440        self.expect('"')?;
441        let mut out = String::new();
442        loop {
443            match self.next() {
444                Some('"') => return Ok(out),
445                Some('\\') => match self.next() {
446                    Some('"') => out.push('"'),
447                    Some('\\') => out.push('\\'),
448                    Some('/') => out.push('/'),
449                    Some('n') => out.push('\n'),
450                    Some('r') => out.push('\r'),
451                    Some('t') => out.push('\t'),
452                    Some('b') => out.push('\u{8}'),
453                    Some('f') => out.push('\u{c}'),
454                    Some('u') => out.push(self.escape_sequence()?),
455                    Some(character) => {
456                        return Err(format!("unknown escape '\\{character}'"));
457                    }
458                    None => return Err("unterminated escape".to_string()),
459                },
460                Some(character) => out.push(character),
461                None => return Err("unterminated string".to_string()),
462            }
463        }
464    }
465
466    /// Reads the four hex digits after `\u`, joining a surrogate pair.
467    ///
468    /// A tool argument carrying an emoji arrives as two escapes in a document
469    /// written by a JavaScript client, and a reader that took them one at a
470    /// time would produce two unpaired halves - which is not a `char` and would
471    /// have to become a replacement character. The text would survive the trip
472    /// looking like a bug in the database.
473    fn escape_sequence(&mut self) -> Result<char, String> {
474        let first = self.hex4()?;
475        if (0xD800..0xDC00).contains(&first) {
476            self.expect('\\')?;
477            self.expect('u')?;
478            let second = self.hex4()?;
479            if !(0xDC00..0xE000).contains(&second) {
480                return Err("a high surrogate was not followed by a low one".to_string());
481            }
482            let combined = 0x10000 + ((first - 0xD800) << 10) + (second - 0xDC00);
483            return char::from_u32(combined).ok_or_else(|| "invalid surrogate pair".to_string());
484        }
485        char::from_u32(first).ok_or_else(|| format!("\\u{first:04x} is not a character"))
486    }
487
488    /// Reads exactly four hexadecimal digits.
489    fn hex4(&mut self) -> Result<u32, String> {
490        let mut value = 0u32;
491        for _ in 0..4 {
492            match self.next().and_then(|character| character.to_digit(16)) {
493                Some(digit) => value = value * 16 + digit,
494                None => return Err("a \\u escape needs four hexadecimal digits".to_string()),
495            }
496        }
497        Ok(value)
498    }
499
500    /// Reads a number, keeping integers integral.
501    fn number(&mut self) -> Result<Json, String> {
502        let start = self.at;
503        if self.peek() == Some('-') {
504            self.at += 1;
505        }
506        let mut fractional = false;
507        while let Some(character) = self.peek() {
508            match character {
509                '0'..='9' => self.at += 1,
510                '.' | 'e' | 'E' | '+' | '-' => {
511                    fractional = true;
512                    self.at += 1;
513                }
514                _ => break,
515            }
516        }
517        let literal: String = self
518            .characters
519            .get(start..self.at)
520            .unwrap_or_default()
521            .iter()
522            .collect();
523        if literal.is_empty() {
524            return Err(format!("expected a number at character {start}"));
525        }
526        if !fractional {
527            if let Ok(number) = literal.parse::<i64>() {
528                return Ok(Json::Int(number));
529            }
530        }
531        literal
532            .parse::<f64>()
533            .map(Json::Real)
534            .map_err(|_| format!("'{literal}' is not a number"))
535    }
536}
537
538#[cfg(test)]
539mod tests {
540    use super::*;
541
542    /// The four scalars read back as themselves.
543    #[test]
544    fn scalars_round_trip() {
545        assert_eq!(parse("null"), Ok(Json::Null));
546        assert_eq!(parse("true"), Ok(Json::Bool(true)));
547        assert_eq!(parse("-12"), Ok(Json::Int(-12)));
548        assert_eq!(parse("1.5"), Ok(Json::Real(1.5)));
549        assert_eq!(parse("\"hi\""), Ok(text("hi")));
550    }
551
552    /// An integer stays an integer, which is what keeps a rowid exact.
553    #[test]
554    fn an_integer_is_not_a_double() {
555        assert_eq!(parse("9007199254740993"), Ok(Json::Int(9007199254740993)));
556        assert_eq!(Json::Int(7).write(), "7");
557        assert_eq!(Json::Real(7.0).write(), "7.0");
558    }
559
560    /// A whole-numbered double still answers `integer()`, because most clients
561    /// have no other way to write `5`.
562    #[test]
563    fn a_whole_double_reads_as_an_integer() {
564        assert_eq!(Json::Real(5.0).integer(), Some(5));
565        assert_eq!(Json::Real(5.5).integer(), None);
566    }
567
568    /// Objects keep their order, so two runs produce the same bytes.
569    #[test]
570    fn objects_keep_their_order() {
571        let value = object(vec![("b", Json::Int(1)), ("a", Json::Int(2))]);
572        assert_eq!(value.write(), "{\"b\":1,\"a\":2}");
573    }
574
575    /// Escapes survive both directions.
576    #[test]
577    fn escapes_round_trip() {
578        let original = "a\"b\\c\nd\te\u{1}f";
579        let written = text(original).write();
580        assert_eq!(parse(&written), Ok(text(original)));
581        assert!(written.contains("\\u0001"));
582    }
583
584    /// A surrogate pair becomes one character rather than two halves.
585    #[test]
586    fn a_surrogate_pair_becomes_one_character() {
587        assert_eq!(parse("\"\\ud83d\\ude00\""), Ok(text("\u{1f600}")));
588    }
589
590    /// Nesting works, and a member is found by name.
591    #[test]
592    fn nesting_reads_back() {
593        let value = parse("{\"a\": [1, {\"b\": null}], \"c\": \"d\"}").unwrap_or(Json::Null);
594        assert_eq!(value.get("c").and_then(Json::text), Some("d"));
595        let inner = value.get("a").and_then(Json::array).unwrap_or_default();
596        assert_eq!(inner.len(), 2);
597    }
598
599    /// A document that does not parse says where it stopped.
600    #[test]
601    fn a_bad_document_is_refused() {
602        assert!(parse("{\"a\": }").is_err());
603        assert!(parse("[1, 2").is_err());
604        assert!(parse("nul").is_err());
605        assert!(parse("{} {}").is_err());
606    }
607
608    /// A value JSON cannot hold is written as null rather than as a bare word.
609    #[test]
610    fn a_non_finite_double_becomes_null() {
611        assert_eq!(Json::Real(f64::NAN).write(), "null");
612        assert_eq!(Json::Real(f64::INFINITY).write(), "null");
613    }
614
615    /// Whitespace anywhere the grammar allows it is skipped.
616    #[test]
617    fn whitespace_is_ignored() {
618        assert_eq!(
619            parse("  {\n \"a\" : [ 1 , 2 ]\n}  "),
620            parse("{\"a\":[1,2]}")
621        );
622    }
623}
624
625#[cfg(test)]
626mod fuzz_seeded {
627    /// How many inputs the seeded sweep below reads.
628    const CASES: usize = 20_000;
629
630    /// The characters the generator draws from.
631    ///
632    /// Weighted towards the ones a JSON parser branches on rather than uniform
633    /// bytes, because a sweep of uniform bytes almost never produces a string
634    /// that reaches past the first character and so exercises one branch.
635    const ALPHABET: &[u8] = b"{}[]\",:0123456789.-+eEtruefalsnl /\t\n\r\0\xff";
636
637    /// Returns the next value of a deterministic generator.
638    ///
639    /// @param state - the generator's state, advanced in place
640    fn next(state: &mut u64) -> u64 {
641        *state ^= *state << 13;
642        *state ^= *state >> 7;
643        *state ^= *state << 17;
644        *state
645    }
646
647    /// The MCP request parser never panics on arbitrary text.
648    ///
649    /// **This is the parser every `tools/call` arrives through**, so its input
650    /// is whatever an agent host sends, and the stable-toolchain twin of
651    /// `fuzz/fuzz_targets/json.rs` is what keeps a regression in it failing a
652    /// pull request rather than a scheduled job nobody reads.
653    #[test]
654    fn the_request_parser_never_panics_on_arbitrary_text() {
655        let mut state = 0x1932_0003_u64;
656        let mut parsed = 0usize;
657        for _ in 0..CASES {
658            let length = (next(&mut state) % 64) as usize;
659            let bytes: Vec<u8> = (0..length)
660                .map(|_| {
661                    let at = (next(&mut state) as usize) % ALPHABET.len();
662                    ALPHABET.get(at).copied().unwrap_or(b'?')
663                })
664                .collect();
665            let text = String::from_utf8_lossy(&bytes).into_owned();
666            parsed += usize::from(super::parse(&text).is_ok());
667        }
668        assert!(parsed <= CASES);
669    }
670}