Skip to main content

ic_json/
lib.rs

1//! A minimal JSON reader and writer.
2//!
3//! The MCP server speaks JSON-RPC, which means something in this workspace has
4//! to *parse* JSON, not merely emit it. Rather than take a dependency and break
5//! the zero-dependency property at the last mile, this is a complete
6//! [RFC 8259](https://www.rfc-editor.org/rfc/rfc8259) parser in a few hundred
7//! lines, exercised by round-trip and malformed-input tests.
8//!
9//! It lived inside the CLI binary until the test-vector harness needed it too.
10//! Parsing JSON is not a command-line concern, and a binary crate is a place
11//! code goes to become unreachable.
12
13#![forbid(unsafe_code)]
14#![deny(missing_docs)]
15#![warn(clippy::all)]
16
17use std::collections::BTreeMap;
18use std::fmt::Write as _;
19
20/// A JSON value.
21#[derive(Debug, Clone, PartialEq)]
22pub enum Json {
23    /// `null`
24    Null,
25    /// `true` or `false`
26    Bool(bool),
27    /// Any JSON number, held as `f64`.
28    Number(f64),
29    /// A string, already unescaped.
30    String(String),
31    /// An array.
32    Array(Vec<Json>),
33    /// An object. Ordered so that output is deterministic.
34    Object(BTreeMap<String, Json>),
35}
36
37impl Json {
38    /// Look up a key, if this is an object.
39    pub fn get(&self, key: &str) -> Option<&Json> {
40        match self {
41            Json::Object(m) => m.get(key),
42            _ => None,
43        }
44    }
45
46    /// Borrow as a string, if this is one.
47    pub fn as_str(&self) -> Option<&str> {
48        match self {
49            Json::String(s) => Some(s),
50            _ => None,
51        }
52    }
53
54    /// Read as an `i64`, if this is a number.
55    pub fn as_i64(&self) -> Option<i64> {
56        match self {
57            Json::Number(n) => Some(*n as i64),
58            _ => None,
59        }
60    }
61
62    /// Read as a slice of elements, if this is an array.
63    ///
64    /// Returns `None` for a non-array rather than an empty slice, so a caller
65    /// can tell "not an array" from "an array with nothing in it" — a
66    /// distinction that matters when the value came from somewhere else.
67    pub fn as_array(&self) -> Option<&[Json]> {
68        match self {
69            Json::Array(items) => Some(items),
70            _ => None,
71        }
72    }
73
74    /// Read as an `f64`, if this is a number.
75    ///
76    /// [`Json::as_i64`] truncates; this does not, so a caller that needs the
77    /// value as written has somewhere to get it.
78    pub fn as_f64(&self) -> Option<f64> {
79        match self {
80            Json::Number(n) => Some(*n),
81            _ => None,
82        }
83    }
84
85    /// Read as a bool, if this is one.
86    pub fn as_bool(&self) -> Option<bool> {
87        match self {
88            Json::Bool(b) => Some(*b),
89            _ => None,
90        }
91    }
92
93    /// Build an object from key-value pairs.
94    pub fn object<const N: usize>(pairs: [(&str, Json); N]) -> Json {
95        let mut m = BTreeMap::new();
96        for (k, v) in pairs {
97            m.insert(k.to_string(), v);
98        }
99        Json::Object(m)
100    }
101
102    /// Convenience constructor for a string value.
103    pub fn str(s: impl Into<String>) -> Json {
104        Json::String(s.into())
105    }
106
107    /// Convenience constructor for a numeric value.
108    pub fn num(n: impl Into<f64>) -> Json {
109        Json::Number(n.into())
110    }
111
112    fn write(&self, out: &mut String) {
113        match self {
114            Json::Null => out.push_str("null"),
115            Json::Bool(true) => out.push_str("true"),
116            Json::Bool(false) => out.push_str("false"),
117            Json::Number(n) => {
118                if n.fract() == 0.0 && n.is_finite() && n.abs() < 9e15 {
119                    let _ = write!(out, "{}", *n as i64);
120                } else {
121                    let _ = write!(out, "{n}");
122                }
123            }
124            Json::String(s) => write_string(s, out),
125            Json::Array(items) => {
126                out.push('[');
127                for (i, item) in items.iter().enumerate() {
128                    if i > 0 {
129                        out.push(',');
130                    }
131                    item.write(out);
132                }
133                out.push(']');
134            }
135            Json::Object(map) => {
136                out.push('{');
137                for (i, (k, v)) in map.iter().enumerate() {
138                    if i > 0 {
139                        out.push(',');
140                    }
141                    write_string(k, out);
142                    out.push(':');
143                    v.write(out);
144                }
145                out.push('}');
146            }
147        }
148    }
149}
150
151impl std::fmt::Display for Json {
152    /// Serialize to compact JSON text.
153    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154        let mut out = String::new();
155        self.write(&mut out);
156        f.write_str(&out)
157    }
158}
159
160fn write_string(s: &str, out: &mut String) {
161    out.push('"');
162    for c in s.chars() {
163        match c {
164            '"' => out.push_str("\\\""),
165            '\\' => out.push_str("\\\\"),
166            '\n' => out.push_str("\\n"),
167            '\r' => out.push_str("\\r"),
168            '\t' => out.push_str("\\t"),
169            c if (c as u32) < 0x20 => {
170                let _ = write!(out, "\\u{:04x}", c as u32);
171            }
172            c => out.push(c),
173        }
174    }
175    out.push('"');
176}
177
178/// How deeply arrays and objects may nest before parsing is refused.
179///
180/// Recursive descent uses stack in proportion to nesting, and a stack overflow
181/// in Rust is an abort rather than an error: nothing unwinds and no caller can
182/// recover. Before this limit existed, two kilobytes of `[[[[...]]]]` ended the
183/// process -- which matters because `ic mcp` parses JSON-RPC from whatever
184/// is on the other end of its stdin.
185///
186/// 64 is chosen against measurement rather than taste. The deepest document this
187/// project produces is the JSON Schema export, at 7; JSON-LD reaches 5 and the
188/// SBOM 6. That is an order of magnitude of headroom, and far below the roughly
189/// one thousand that exhausts the stack.
190pub const MAX_DEPTH: usize = 64;
191
192/// Parse a JSON document.
193///
194/// Nesting deeper than [`MAX_DEPTH`] is an error rather than a crash.
195pub fn parse(input: &str) -> Result<Json, String> {
196    let bytes: Vec<char> = input.chars().collect();
197    let mut p = Parser {
198        input: &bytes,
199        pos: 0,
200        depth: 0,
201    };
202    p.skip_ws();
203    let value = p.value()?;
204    p.skip_ws();
205    if p.pos != p.input.len() {
206        return Err(format!("trailing input at position {}", p.pos));
207    }
208    Ok(value)
209}
210
211struct Parser<'a> {
212    input: &'a [char],
213    pos: usize,
214    /// How many arrays and objects are open at this point.
215    depth: usize,
216}
217
218impl Parser<'_> {
219    fn peek(&self) -> Option<char> {
220        self.input.get(self.pos).copied()
221    }
222
223    fn bump(&mut self) -> Option<char> {
224        let c = self.peek();
225        if c.is_some() {
226            self.pos += 1;
227        }
228        c
229    }
230
231    fn skip_ws(&mut self) {
232        while matches!(self.peek(), Some(' ' | '\t' | '\n' | '\r')) {
233            self.pos += 1;
234        }
235    }
236
237    fn expect(&mut self, c: char) -> Result<(), String> {
238        if self.bump() == Some(c) {
239            Ok(())
240        } else {
241            Err(format!("expected '{c}' at position {}", self.pos))
242        }
243    }
244
245    fn literal(&mut self, word: &str) -> Result<(), String> {
246        for c in word.chars() {
247            if self.bump() != Some(c) {
248                return Err(format!("invalid literal near position {}", self.pos));
249            }
250        }
251        Ok(())
252    }
253
254    fn value(&mut self) -> Result<Json, String> {
255        self.skip_ws();
256        match self.peek() {
257            Some('n') => {
258                self.literal("null")?;
259                Ok(Json::Null)
260            }
261            Some('t') => {
262                self.literal("true")?;
263                Ok(Json::Bool(true))
264            }
265            Some('f') => {
266                self.literal("false")?;
267                Ok(Json::Bool(false))
268            }
269            Some('"') => Ok(Json::String(self.string()?)),
270            Some('[') => self.array(),
271            Some('{') => self.object(),
272            Some(c) if c == '-' || c.is_ascii_digit() => self.number(),
273            Some(c) => Err(format!("unexpected '{c}' at position {}", self.pos)),
274            None => Err("unexpected end of input".to_string()),
275        }
276    }
277
278    fn string(&mut self) -> Result<String, String> {
279        self.expect('"')?;
280        let mut out = String::new();
281        loop {
282            match self.bump() {
283                None => return Err("unterminated string".to_string()),
284                Some('"') => return Ok(out),
285                Some('\\') => match self.bump() {
286                    Some('"') => out.push('"'),
287                    Some('\\') => out.push('\\'),
288                    Some('/') => out.push('/'),
289                    Some('b') => out.push('\u{8}'),
290                    Some('f') => out.push('\u{c}'),
291                    Some('n') => out.push('\n'),
292                    Some('r') => out.push('\r'),
293                    Some('t') => out.push('\t'),
294                    Some('u') => {
295                        let mut code = 0u32;
296                        for _ in 0..4 {
297                            let c = self.bump().ok_or("truncated \\u escape")?;
298                            let d = c.to_digit(16).ok_or("invalid \\u escape")?;
299                            code = code * 16 + d;
300                        }
301                        // Surrogate pairs are joined; a lone surrogate becomes
302                        // the replacement character rather than an error, which
303                        // matches what lenient JSON consumers do.
304                        if (0xD800..0xDC00).contains(&code) && self.peek() == Some('\\') {
305                            self.pos += 1;
306                            self.expect('u')?;
307                            let mut low = 0u32;
308                            for _ in 0..4 {
309                                let c = self.bump().ok_or("truncated \\u escape")?;
310                                let d = c.to_digit(16).ok_or("invalid \\u escape")?;
311                                low = low * 16 + d;
312                            }
313                            code = 0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00);
314                        }
315                        out.push(char::from_u32(code).unwrap_or('\u{FFFD}'));
316                    }
317                    _ => return Err("invalid escape".to_string()),
318                },
319                Some(c) if (c as u32) < 0x20 => {
320                    return Err("unescaped control character in string".to_string())
321                }
322                Some(c) => out.push(c),
323            }
324        }
325    }
326
327    fn number(&mut self) -> Result<Json, String> {
328        let start = self.pos;
329        if self.peek() == Some('-') {
330            self.pos += 1;
331        }
332        while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
333            self.pos += 1;
334        }
335        if self.peek() == Some('.') {
336            self.pos += 1;
337            while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
338                self.pos += 1;
339            }
340        }
341        if matches!(self.peek(), Some('e' | 'E')) {
342            self.pos += 1;
343            if matches!(self.peek(), Some('+' | '-')) {
344                self.pos += 1;
345            }
346            while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
347                self.pos += 1;
348            }
349        }
350        let text: String = self.input[start..self.pos].iter().collect();
351        text.parse::<f64>()
352            .map(Json::Number)
353            .map_err(|_| format!("invalid number '{text}'"))
354    }
355
356    /// Parse an array, counting the nesting.
357    ///
358    /// The counter is taken and released here rather than inside
359    /// `array_inner`, which returns from several places: a decrement missed on
360    /// one of them would leak depth and start rejecting long documents that are
361    /// not deep at all, which is worse than what this guards against and far
362    /// harder to notice.
363    fn array(&mut self) -> Result<Json, String> {
364        self.depth += 1;
365        if self.depth > MAX_DEPTH {
366            return Err(format!(
367                "nesting deeper than {MAX_DEPTH} at position {}",
368                self.pos
369            ));
370        }
371        let parsed = self.array_inner();
372        self.depth -= 1;
373        parsed
374    }
375
376    fn array_inner(&mut self) -> Result<Json, String> {
377        self.expect('[')?;
378        let mut items = Vec::new();
379        self.skip_ws();
380        if self.peek() == Some(']') {
381            self.pos += 1;
382            return Ok(Json::Array(items));
383        }
384        loop {
385            items.push(self.value()?);
386            self.skip_ws();
387            match self.bump() {
388                Some(',') => continue,
389                Some(']') => return Ok(Json::Array(items)),
390                _ => return Err("expected ',' or ']'".to_string()),
391            }
392        }
393    }
394
395    /// Parse an object, counting the nesting.
396    ///
397    /// The counter is taken and released here rather than inside
398    /// `object_inner`, which returns from several places: a decrement missed on
399    /// one of them would leak depth and start rejecting long documents that are
400    /// not deep at all, which is worse than what this guards against and far
401    /// harder to notice.
402    fn object(&mut self) -> Result<Json, String> {
403        self.depth += 1;
404        if self.depth > MAX_DEPTH {
405            return Err(format!(
406                "nesting deeper than {MAX_DEPTH} at position {}",
407                self.pos
408            ));
409        }
410        let parsed = self.object_inner();
411        self.depth -= 1;
412        parsed
413    }
414
415    fn object_inner(&mut self) -> Result<Json, String> {
416        self.expect('{')?;
417        let mut map = BTreeMap::new();
418        self.skip_ws();
419        if self.peek() == Some('}') {
420            self.pos += 1;
421            return Ok(Json::Object(map));
422        }
423        loop {
424            self.skip_ws();
425            let key = self.string()?;
426            self.skip_ws();
427            self.expect(':')?;
428            let value = self.value()?;
429            map.insert(key, value);
430            self.skip_ws();
431            match self.bump() {
432                Some(',') => continue,
433                Some('}') => return Ok(Json::Object(map)),
434                _ => return Err("expected ',' or '}'".to_string()),
435            }
436        }
437    }
438}
439
440#[cfg(test)]
441mod tests {
442    use super::*;
443
444    /// The new accessors, including the distinction that motivates them.
445    #[test]
446    fn array_and_number_accessors_report_the_right_shape() {
447        let v = parse(r#"{"xs":[1,2],"empty":[],"n":1.5,"s":"t"}"#).unwrap();
448
449        assert_eq!(
450            v.get("xs").and_then(|x| x.as_array()).map(|a| a.len()),
451            Some(2)
452        );
453        // An empty array is still an array, and must not read as absent.
454        assert_eq!(
455            v.get("empty").and_then(|x| x.as_array()).map(|a| a.len()),
456            Some(0)
457        );
458        assert!(
459            v.get("s").unwrap().as_array().is_none(),
460            "a string is not an array"
461        );
462
463        assert_eq!(v.get("n").and_then(|x| x.as_f64()), Some(1.5));
464        // as_i64 truncates where as_f64 does not; both are offered for that reason.
465        assert_eq!(v.get("n").and_then(|x| x.as_i64()), Some(1));
466        assert!(v.get("s").unwrap().as_f64().is_none());
467    }
468
469    /// `n` copies of `unit`, comma separated.
470    fn alloc_join(n: usize, unit: &str) -> String {
471        vec![unit; n].join(",")
472    }
473
474    /// `n` copies of `unit`, comma separated, for units too large to repeat
475    /// cheaply by value.
476    fn alloc_join_with(n: usize, unit: &str) -> String {
477        (0..n).map(|_| unit).collect::<Vec<_>>().join(",")
478    }
479
480    #[test]
481    fn parses_scalars() {
482        assert_eq!(parse("null").unwrap(), Json::Null);
483        assert_eq!(parse("true").unwrap(), Json::Bool(true));
484        assert_eq!(parse("false").unwrap(), Json::Bool(false));
485        assert_eq!(parse("42").unwrap(), Json::Number(42.0));
486        assert_eq!(parse("-1.5e2").unwrap(), Json::Number(-150.0));
487        assert_eq!(parse(r#""hi""#).unwrap(), Json::str("hi"));
488    }
489
490    #[test]
491    fn parses_nested_structures() {
492        let v = parse(r#"{"a":[1,2,{"b":null}],"c":"d"}"#).unwrap();
493        assert_eq!(v.get("c").unwrap().as_str(), Some("d"));
494        let a = v.get("a").unwrap();
495        match a {
496            Json::Array(items) => {
497                assert_eq!(items.len(), 3);
498                assert_eq!(items[2].get("b"), Some(&Json::Null));
499            }
500            _ => panic!("expected an array"),
501        }
502    }
503
504    #[test]
505    fn handles_escapes() {
506        assert_eq!(parse(r#""a\nb""#).unwrap(), Json::str("a\nb"));
507        assert_eq!(parse(r#""a\\b""#).unwrap(), Json::str("a\\b"));
508        assert_eq!(parse(r#""a\"b""#).unwrap(), Json::str("a\"b"));
509        assert_eq!(parse(r#""A""#).unwrap(), Json::str("A"));
510        // A surrogate pair for U+1F600.
511        assert_eq!(parse(r#""😀""#).unwrap(), Json::str("\u{1F600}"));
512    }
513
514    /// Deep nesting is refused, not fatal.
515    ///
516    /// Before the limit existed, `"[" * 1000` overflowed the stack, and a stack
517    /// overflow in Rust aborts: nothing unwinds and no caller can recover. This
518    /// is the case that motivated it, and `ic mcp` is why it matters --
519    /// that server parses JSON-RPC from whatever is on the other end of its
520    /// stdin.
521    #[test]
522    fn deep_nesting_is_refused_rather_than_fatal() {
523        for depth in [MAX_DEPTH + 1, 1_000, 100_000] {
524            let deep = "[".repeat(depth) + &"]".repeat(depth);
525            let err = parse(&deep).expect_err("{depth} deep should be refused");
526            assert!(
527                err.contains("nesting"),
528                "the error should say what was wrong: {err}"
529            );
530
531            let deep = "{\"a\":".repeat(depth) + "1" + &"}".repeat(depth);
532            assert!(
533                parse(&deep).is_err(),
534                "{depth} deep objects should be refused"
535            );
536        }
537    }
538
539    /// The boundary is where it says it is.
540    ///
541    /// Off by one here is either a document refused that should parse or a
542    /// limit that is not the documented one.
543    #[test]
544    fn the_limit_is_exactly_where_it_claims() {
545        let at = "[".repeat(MAX_DEPTH) + &"]".repeat(MAX_DEPTH);
546        assert!(parse(&at).is_ok(), "{MAX_DEPTH} deep should parse");
547
548        let over = "[".repeat(MAX_DEPTH + 1) + &"]".repeat(MAX_DEPTH + 1);
549        assert!(parse(&over).is_err(), "{} deep should not", MAX_DEPTH + 1);
550    }
551
552    /// Depth is not length: a wide document is not a deep one.
553    ///
554    /// The easy mistake in a limit like this is to count the wrong thing and
555    /// start refusing large inputs, which would break the ontology exports --
556    /// 75 algorithms with their parameters and constraints, all of it shallow.
557    #[test]
558    fn a_wide_document_is_not_a_deep_one() {
559        let wide = format!("[{}]", vec!["1"; 50_000].join(","));
560        let parsed = parse(&wide).expect("a flat array of 50,000 items is not deep");
561        match parsed {
562            Json::Array(items) => assert_eq!(items.len(), 50_000),
563            other => panic!("expected an array, got {other:?}"),
564        }
565
566        // And the real thing: whatever this project emits must still parse.
567        // The deepest is the JSON Schema export at 7.
568        let nested = r#"{"a":{"b":{"c":{"d":{"e":{"f":{"g":[1,2,3]}}}}}}}"#;
569        assert!(parse(nested).is_ok());
570    }
571
572    /// The counter must be released, or siblings exhaust it.
573    ///
574    /// This is the bug a depth limit introduces rather than fixes, and it hides
575    /// from the obvious test. A single deep chain enters each level once, so a
576    /// missing decrement never shows; and `parse` builds a fresh parser every
577    /// call, so nothing leaks between documents either. The first version of
578    /// this test did both of those and stayed green when the release was
579    /// deleted.
580    ///
581    /// It shows up across siblings, where the counter should fall between one
582    /// value and the next. That shape is not contrived: the ontology export is
583    /// an object holding an array of seventy-five algorithm objects, every one
584    /// of them a sibling at depth 3.
585    #[test]
586    fn depth_is_released_between_siblings() {
587        // Far more siblings than MAX_DEPTH, and only two deep. This parses if
588        // and only if the counter comes back down between them.
589        let siblings = alloc_join(MAX_DEPTH * 8, "[]");
590        let doc = format!("[{siblings}]");
591        let parsed = parse(&doc).expect("wide and shallow should parse");
592        match parsed {
593            Json::Array(items) => assert_eq!(items.len(), MAX_DEPTH * 8),
594            other => panic!("expected an array, got {other:?}"),
595        }
596
597        // Objects too, which use the other wrapper.
598        let siblings = alloc_join(MAX_DEPTH * 8, "{}");
599        assert!(parse(&format!("[{siblings}]")).is_ok());
600
601        // And nested siblings: each branch goes deep, returns, and the next one
602        // starts from the same level rather than from where the last finished.
603        let branch = "[".repeat(MAX_DEPTH - 2) + &"]".repeat(MAX_DEPTH - 2);
604        let many = alloc_join_with(8, &branch);
605        assert!(
606            parse(&format!("[{many}]")).is_ok(),
607            "deep branches side by side should parse; the counter is not falling"
608        );
609
610        // A refusal must not leave the counter raised either: the early return
611        // on the limit is the exit most likely to skip a decrement.
612        let over = "[".repeat(MAX_DEPTH + 10) + &"]".repeat(MAX_DEPTH + 10);
613        assert!(parse(&over).is_err());
614        assert!(parse(&doc).is_ok(), "a refusal poisoned the next parse");
615    }
616
617    #[test]
618    fn rejects_malformed_input() {
619        for bad in [
620            "",
621            "{",
622            "[1,]",
623            r#"{"a"}"#,
624            r#"{"a":}"#,
625            "tru",
626            "{} {}",
627            "\"unterminated",
628            "01x",
629        ] {
630            assert!(parse(bad).is_err(), "{bad:?} should not parse");
631        }
632    }
633
634    #[test]
635    fn roundtrips_through_serialization() {
636        let cases = [
637            r#"{"a":1,"b":[true,false,null],"c":"x"}"#,
638            r#"[]"#,
639            r#"{}"#,
640            r#"{"nested":{"deep":{"value":-3}}}"#,
641        ];
642        for case in cases {
643            let v = parse(case).unwrap();
644            let text = v.to_string();
645            assert_eq!(parse(&text).unwrap(), v, "round trip of {case}");
646        }
647    }
648
649    #[test]
650    fn serialization_escapes_control_characters() {
651        let v = Json::str("tab\there\u{1}");
652        let text = v.to_string();
653        assert!(text.contains("\\t"));
654        assert!(text.contains("\\u0001"));
655        assert_eq!(parse(&text).unwrap(), v);
656    }
657
658    #[test]
659    fn integers_serialize_without_a_decimal_point() {
660        assert_eq!(Json::num(42.0).to_string(), "42");
661        assert_eq!(Json::num(-7.0).to_string(), "-7");
662        assert_eq!(Json::num(0.5).to_string(), "0.5");
663    }
664
665    #[test]
666    fn object_keys_are_ordered_deterministically() {
667        let a = Json::object([("z", Json::num(1)), ("a", Json::num(2))]);
668        assert_eq!(a.to_string(), r#"{"a":2,"z":1}"#);
669    }
670
671    #[test]
672    fn accessors_return_none_on_type_mismatch() {
673        let v = Json::str("text");
674        assert_eq!(v.as_i64(), None);
675        assert_eq!(v.as_bool(), None);
676        assert_eq!(v.get("key"), None);
677    }
678}