Skip to main content

rich/
json.rs

1//! JSON pretty-printing.
2//!
3//! Port of upstream `rich/json.py`. [`Json`] parses a JSON string and renders it
4//! with 2-space indentation (matching Python's `json.dumps(indent=2)`) and the
5//! default JSON highlight colors.
6//!
7//! Non-ASCII strings render as UTF-8, matching upstream (`rich.json.JSON`
8//! defaults to `ensure_ascii=False`); object keys keep input order, and a
9//! repeated key keeps its first position but its last value — what both
10//! `dict` and serde_json's `preserve_order` do. Number formatting follows
11//! `json.dumps` too: floats are written
12//! with Python's `float.__repr__` ([`crate::pyformat::float_repr`]).
13//! Custom indent/sort options are deferred — see docs/DIVERGENCES.md.
14//!
15//! ## Why the parser is hand-written
16//!
17//! Upstream's parser is Python's `json`, which differs from `serde_json` in two
18//! important ways this module reproduces:
19//!
20//! * `json.loads` accepts (and `json.dumps(allow_nan=True)` emits) the
21//!   non-finite literals `NaN`, `Infinity` and `-Infinity`. `serde_json` has no
22//!   `Value` that can hold them and rejects the documents outright.
23//! * `serde_json` caps nesting at 128 levels, so a 200-deep document — which
24//!   CPython parses without complaint — came back as "invalid JSON".
25//!
26//! Raising a recursion limit only moves the failure to a stack overflow, so
27//! parsing, rendering and *dropping* the tree here are all iterative: nesting
28//! depth costs heap, never stack. String decoding uses `serde_json`; integer
29//! tokens retain their exact digits, finite floats use Python's `repr`, and
30//! overflowing exponents become signed Infinity as in Python.
31//!
32//! Nesting is still bounded, at [`MAX_DEPTH`] levels, where CPython raises
33//! `RecursionError`: `json.loads` recurses in C, and CPython 3.12+ stops just
34//! short of 10 000 levels (3.11 at its 1 000-frame recursion limit). Rendering
35//! indents every line by its depth, so an unbounded document costs quadratic
36//! memory: a 100 000-deep array asked for tens of gigabytes and was killed
37//! rather than reporting an error. The limit accepts every document CPython
38//! 3.12+ accepts; the few levels between its exact (stack-dependent) cutoff
39//! and `MAX_DEPTH` are the only difference.
40
41use std::collections::HashMap;
42
43/// The deepest nesting [`Json::new`] accepts (DIVERGENCES #25). Deeper
44/// documents fail with the error CPython's `json.loads` raises
45/// (`RecursionError`); see the module docs.
46pub const MAX_DEPTH: usize = 10_000;
47
48use crate::console::{Console, ConsoleOptions};
49use crate::errors::{Result, RichError};
50use crate::measure::Measurement;
51use crate::protocol::Renderable;
52use crate::segment::Segment;
53use crate::style::Style;
54
55/// A parsed JSON document, rendered with syntax highlighting. Mirrors `rich.json.JSON`.
56pub struct Json {
57    value: Node,
58    styles: JsonStyles,
59    /// See [`Json::no_wrap`].
60    no_wrap: bool,
61    #[cfg(feature = "json-escape-safe")]
62    escape_safe: bool,
63}
64
65/// A parsed JSON value.
66///
67/// Scalars keep the form they will be printed in: numbers are stored already
68/// normalized where needed, strings already decoded (upstream re-encodes them
69/// through `json.dumps`, so `"A"` prints as `"A"`).
70#[derive(Debug)]
71enum Node {
72    Null,
73    Bool(bool),
74    Number(String),
75    /// `NaN`, `Infinity` or `-Infinity`. Python's `json` round-trips these;
76    /// rich's `JSONHighlighter` has no rule that matches them, so they print
77    /// unstyled.
78    NonFinite(&'static str),
79    Str(String),
80    Array(Vec<Node>),
81    Object(Vec<(String, Node)>),
82}
83
84impl Drop for Node {
85    /// Dismantle the tree with an explicit stack.
86    ///
87    /// The compiler's drop glue recurses once per nesting level, so a document
88    /// deep enough to parse (parsing has no depth limit here) would overflow
89    /// the stack on the way out — a crash with no error message at all, which
90    /// is worse than the rejection this module used to hand out.
91    fn drop(&mut self) {
92        let mut pending: Vec<Node> = Vec::new();
93        take_children(self, &mut pending);
94        while let Some(mut node) = pending.pop() {
95            take_children(&mut node, &mut pending);
96            // `node` drops here with its children already moved out, so this
97            // same `drop` runs against an empty container and stops.
98        }
99    }
100}
101
102/// Move a node's children into `out`, leaving the node childless.
103fn take_children(node: &mut Node, out: &mut Vec<Node>) {
104    match node {
105        Node::Array(items) => out.append(items),
106        Node::Object(entries) => out.extend(entries.drain(..).map(|(_, value)| value)),
107        _ => {}
108    }
109}
110
111struct JsonStyles {
112    brace: Style,
113    key: Style,
114    string: Style,
115    number: Style,
116    bool_true: Style,
117    bool_false: Style,
118    null: Style,
119}
120
121impl JsonStyles {
122    fn defaults() -> Self {
123        let s = |spec: &str| Style::parse(spec).expect("valid built-in style");
124        JsonStyles {
125            brace: s("bold"),
126            key: s("bold blue"),
127            string: s("green"),
128            number: s("bold cyan"),
129            bool_true: s("italic bright_green"),
130            bool_false: s("italic bright_red"),
131            null: s("italic magenta"),
132        }
133    }
134}
135
136/// One entry of the render work list, consumed newest-first.
137enum Task<'a> {
138    /// Render this value indented `usize` levels deep.
139    Value(&'a Node, usize),
140    /// Emit a segment that has already been decided.
141    Emit(Segment),
142}
143
144impl Json {
145    /// Parse `text` as JSON. Returns an error if it is not valid JSON.
146    pub fn new(text: &str) -> Result<Self> {
147        Ok(Json {
148            value: Parser::new(text).parse_document()?,
149            styles: JsonStyles::defaults(),
150            no_wrap: false,
151            #[cfg(feature = "json-escape-safe")]
152            escape_safe: false,
153        })
154    }
155
156    /// Opt in to escape-aware display boundaries (requires `json-escape-safe`).
157    /// Cropping omits partial escapes. Folding preserves escapes that fit the
158    /// width; narrower widths split oversized escapes to avoid losing content.
159    /// The default remains Python rich's ordinary word folding/cropping.
160    #[cfg(feature = "json-escape-safe")]
161    pub fn escape_safe(mut self, enabled: bool) -> Self {
162        self.escape_safe = enabled;
163        self
164    }
165
166    /// Keep `rich.json.JSON`'s `self.text.no_wrap = True`, which **crops** each
167    /// line at the available width instead of wrapping it.
168    ///
169    /// Defaults to `false`, because that is what a *top-level*
170    /// `Console.print(JSON(...))` produces and that is how this renderable is
171    /// normally reached. Upstream's flag really is set, but `Console.print`
172    /// never renders the `Text` it is set on: `_collect_renderables` sees a
173    /// `Text`, buffers it, and `check_text` hands back
174    /// `Text(sep, justify=…, end=…).join(buffered)` — and `Text.join` starts
175    /// from `self.blank_copy()`, i.e. from the *separator's* metadata. The
176    /// separator has `no_wrap=None`, so the copy that actually renders wraps
177    /// with the default `fold` overflow.
178    ///
179    /// Turn it on whenever the document is **nested** inside another
180    /// renderable — a `Panel`, `Padding`, `Styled`, `Constrain`, or rich-cli's
181    /// `ForceWidth`. Those are `ConsoleRenderable`s, so `_collect_renderables`
182    /// appends them untouched and the inner `Text` reaches
183    /// `Text.__rich_console__` with the flag intact; `Text.wrap` then skips
184    /// `divide_line` and only `truncate`s to the width.
185    ///
186    /// The two are not interchangeable. Wrapping keeps every character;
187    /// cropping discards what does not fit, which for JSON means the printed
188    /// document no longer parses — so the choice has to follow upstream's,
189    /// not taste.
190    #[must_use]
191    pub fn no_wrap(mut self, no_wrap: bool) -> Self {
192        self.no_wrap = no_wrap;
193        self
194    }
195
196    /// Flatten the document into segments, iteratively.
197    ///
198    /// A recursive walk would descend once per nesting level and overflow the
199    /// stack on the deep documents the parser now accepts.
200    fn render_value(&self) -> Vec<Segment> {
201        let brace = |text: &str| Segment::new(text.to_string(), Some(self.styles.brace.clone()));
202        let plain = |text: String| Segment::new(text, None);
203
204        let mut out = Vec::new();
205        let mut stack = vec![Task::Value(&self.value, 0)];
206        while let Some(task) = stack.pop() {
207            let (node, level) = match task {
208                Task::Emit(segment) => {
209                    out.push(segment);
210                    continue;
211                }
212                Task::Value(node, level) => (node, level),
213            };
214            match node {
215                Node::Null => out.push(Segment::new(
216                    "null".to_string(),
217                    Some(self.styles.null.clone()),
218                )),
219                Node::Bool(true) => out.push(Segment::new(
220                    "true".to_string(),
221                    Some(self.styles.bool_true.clone()),
222                )),
223                Node::Bool(false) => out.push(Segment::new(
224                    "false".to_string(),
225                    Some(self.styles.bool_false.clone()),
226                )),
227                Node::Number(number) => out.push(Segment::new(
228                    number.clone(),
229                    Some(self.styles.number.clone()),
230                )),
231                Node::NonFinite(literal) => out.push(plain((*literal).to_string())),
232                Node::Str(string) => out.push(Segment::new(
233                    quote(string),
234                    Some(self.styles.string.clone()),
235                )),
236                Node::Array(items) => {
237                    out.push(brace("["));
238                    if items.is_empty() {
239                        out.push(brace("]"));
240                        continue;
241                    }
242                    out.push(plain("\n".to_string()));
243                    // Pushed back-to-front, so they pop in document order.
244                    stack.push(Task::Emit(brace("]")));
245                    stack.push(Task::Emit(plain("  ".repeat(level))));
246                    let last = items.len() - 1;
247                    for (index, item) in items.iter().enumerate().rev() {
248                        stack.push(Task::Emit(plain("\n".to_string())));
249                        if index != last {
250                            stack.push(Task::Emit(plain(",".to_string())));
251                        }
252                        stack.push(Task::Value(item, level + 1));
253                        stack.push(Task::Emit(plain("  ".repeat(level + 1))));
254                    }
255                }
256                Node::Object(entries) => {
257                    out.push(brace("{"));
258                    if entries.is_empty() {
259                        out.push(brace("}"));
260                        continue;
261                    }
262                    out.push(plain("\n".to_string()));
263                    stack.push(Task::Emit(brace("}")));
264                    stack.push(Task::Emit(plain("  ".repeat(level))));
265                    let last = entries.len() - 1;
266                    for (index, (key, item)) in entries.iter().enumerate().rev() {
267                        stack.push(Task::Emit(plain("\n".to_string())));
268                        if index != last {
269                            stack.push(Task::Emit(plain(",".to_string())));
270                        }
271                        stack.push(Task::Value(item, level + 1));
272                        stack.push(Task::Emit(plain(": ".to_string())));
273                        stack.push(Task::Emit(Segment::new(
274                            quote(key),
275                            Some(self.styles.key.clone()),
276                        )));
277                        stack.push(Task::Emit(plain("  ".repeat(level + 1))));
278                    }
279                }
280            }
281        }
282        out
283    }
284}
285
286impl Renderable for Json {
287    /// Upstream `JSON.__rich__` returns its highlighted `Text`, so measuring a
288    /// `JSON` is `Text.__rich_measure__` over the formatted document.
289    fn measure(&self, _console: &Console, _options: &ConsoleOptions) -> Measurement {
290        let plain: String = self
291            .render_value()
292            .iter()
293            .map(|s| s.text.as_str())
294            .collect();
295        let (minimum, maximum) = crate::text::Text::new(plain).measurement();
296        Measurement::new(minimum, maximum)
297    }
298
299    fn rich_render(&self, _console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
300        let segments = self.render_value();
301        #[cfg(feature = "json-escape-safe")]
302        if self.escape_safe {
303            return escape_safe_lines(&segments, options.max_width, self.no_wrap);
304        }
305        if self.no_wrap {
306            // `Text.wrap` with `no_wrap` keeps the line whole and then calls
307            // `line.truncate(width, overflow="fold")`, which is a crop. See
308            // [`Json::no_wrap`] for when upstream gets here.
309            Segment::crop_lines(&segments, options.max_width)
310        } else {
311            // The break must land at a *word* boundary: the joined copy carries
312            // no overflow either, so upstream wraps with the default `fold`
313            // overflow and only splits mid-word when a single token is wider
314            // than the line.
315            Segment::fold_lines_words(&segments, options.max_width)
316        }
317    }
318}
319
320/// Tokenize each physical line into JSON escapes and ordinary graphemes, then
321/// choose every boundary from the space actually remaining. No stale absolute
322/// wrap points survive an adjusted escape boundary (#98).
323#[cfg(feature = "json-escape-safe")]
324fn escape_safe_lines(segments: &[Segment], width: usize, crop: bool) -> Vec<Segment> {
325    if width == 0 {
326        return Vec::new();
327    }
328    let lines = Segment::split_lines(segments);
329    let last = lines.len().saturating_sub(1);
330    let mut out = Vec::new();
331    for (line_index, line) in lines.into_iter().enumerate() {
332        let plain: String = line
333            .iter()
334            .filter(|s| !s.control)
335            .map(|s| s.text.as_str())
336            .collect();
337        // Keep upstream's exact behavior when there is no escape to protect.
338        if !plain.contains('\\') {
339            out.extend(if crop {
340                Segment::crop_lines(&line, width)
341            } else {
342                Segment::fold_lines_words(&line, width)
343            });
344        } else {
345            let (spans, _) = crate::cells::split_graphemes(&plain);
346            let mut atoms = Vec::new();
347            let mut index = 0;
348            while index < spans.len() {
349                let (start, mut end, mut cells) = spans[index];
350                if plain.as_bytes()[start] == b'\\' {
351                    let escape_end = start
352                        + if plain.as_bytes().get(start + 1) == Some(&b'u') {
353                            6
354                        } else {
355                            2
356                        };
357                    while end < escape_end && index + 1 < spans.len() {
358                        index += 1;
359                        end = spans[index].1;
360                        cells += spans[index].2;
361                    }
362                    if !crop && cells > width {
363                        // An atom wider than the whole console cannot both fit
364                        // and stay atomic. Split its ASCII spelling rather than
365                        // overrun into Console's final crop and lose characters.
366                        for offset in start..escape_end {
367                            atoms.push((offset, offset + 1, 1));
368                        }
369                        if end > escape_end {
370                            atoms.push((escape_end, end, 0));
371                        }
372                        index += 1;
373                        continue;
374                    }
375                }
376                atoms.push((start, end, cells));
377                index += 1;
378            }
379            let mut breaks = Vec::new();
380            let mut cells = 0;
381            let mut stop = plain.len();
382            for (start, _, atom_width) in atoms {
383                if cells + atom_width > width {
384                    if crop {
385                        stop = start;
386                        break;
387                    }
388                    if cells > 0 {
389                        breaks.push(start);
390                        cells = 0;
391                    }
392                }
393                cells += atom_width;
394            }
395            let mut position = 0;
396            let mut next = 0;
397            for segment in line {
398                if segment.control {
399                    out.push(segment);
400                    continue;
401                }
402                let mut buffer = String::new();
403                for ch in segment.text.chars() {
404                    if position >= stop {
405                        break;
406                    }
407                    if breaks.get(next) == Some(&position) {
408                        if !buffer.is_empty() {
409                            out.push(Segment::new(
410                                std::mem::take(&mut buffer),
411                                segment.style.clone(),
412                            ));
413                        }
414                        out.push(Segment::line());
415                        next += 1;
416                    }
417                    buffer.push(ch);
418                    position += ch.len_utf8();
419                }
420                if !buffer.is_empty() {
421                    out.push(Segment::new(buffer, segment.style));
422                }
423            }
424        }
425        if line_index != last {
426            out.push(Segment::line());
427        }
428    }
429    out
430}
431
432/// Serialize a string as a JSON string literal (quoted + escaped).
433fn quote(string: &str) -> String {
434    serde_json::to_string(string).unwrap_or_else(|_| format!("{string:?}"))
435}
436
437/// A container being filled in, held on the parser's explicit stack.
438enum Frame {
439    Array(Vec<Node>),
440    Object {
441        entries: Vec<(String, Node)>,
442        /// Key -> position in `entries`, so a repeated key overwrites in place
443        /// (`{"a": 1, "a": 2}` is one entry) without an O(n^2) rescan. Dropped
444        /// with the frame, so only the objects on the current path pay for it.
445        seen: HashMap<String, usize>,
446        /// The key whose value is currently being parsed.
447        key: String,
448    },
449}
450
451/// A non-recursive JSON reader. Structure is walked with an explicit stack;
452/// scalar tokens are handed to `serde_json` for decoding so escapes, number
453/// formats and their rejections stay identical to the rest of the workspace.
454struct Parser<'a> {
455    src: &'a str,
456    bytes: &'a [u8],
457    pos: usize,
458}
459
460impl<'a> Parser<'a> {
461    fn new(src: &'a str) -> Self {
462        Parser {
463            src,
464            bytes: src.as_bytes(),
465            pos: 0,
466        }
467    }
468
469    fn parse_document(&mut self) -> Result<Node> {
470        let value = self.parse_value()?;
471        self.skip_whitespace();
472        if self.pos != self.bytes.len() {
473            return Err(self.error("trailing characters"));
474        }
475        Ok(value)
476    }
477
478    /// Parse one value, descending into containers with an explicit stack.
479    fn parse_value(&mut self) -> Result<Node> {
480        let mut stack: Vec<Frame> = Vec::new();
481        let mut node: Node;
482
483        'descend: loop {
484            self.skip_whitespace();
485            // CPython's scanner enters a recursive call per container, empty
486            // ones included, and raises once the recursion limit is reached.
487            if let Some(kind @ (b'[' | b'{')) = self.peek() {
488                if stack.len() >= MAX_DEPTH {
489                    let what = if kind == b'[' { "array" } else { "object" };
490                    return Err(self.error(&format!(
491                        "maximum recursion depth exceeded while decoding a JSON {what} \
492                         from a unicode string"
493                    )));
494                }
495            }
496            match self.peek() {
497                Some(b'[') => {
498                    self.pos += 1;
499                    self.skip_whitespace();
500                    if self.peek() == Some(b']') {
501                        self.pos += 1;
502                        node = Node::Array(Vec::new());
503                    } else {
504                        stack.push(Frame::Array(Vec::new()));
505                        continue 'descend;
506                    }
507                }
508                Some(b'{') => {
509                    self.pos += 1;
510                    self.skip_whitespace();
511                    if self.peek() == Some(b'}') {
512                        self.pos += 1;
513                        node = Node::Object(Vec::new());
514                    } else {
515                        let key = self.parse_key()?;
516                        stack.push(Frame::Object {
517                            entries: Vec::new(),
518                            seen: HashMap::new(),
519                            key,
520                        });
521                        continue 'descend;
522                    }
523                }
524                _ => node = self.parse_scalar()?,
525            }
526
527            // `node` is finished: hand it to its parent, then close as many
528            // containers as end here.
529            loop {
530                let Some(frame) = stack.last_mut() else {
531                    return Ok(node);
532                };
533                let closing = match frame {
534                    Frame::Array(items) => {
535                        items.push(node);
536                        b']'
537                    }
538                    Frame::Object { entries, seen, key } => {
539                        let key = std::mem::take(key);
540                        match seen.get(&key) {
541                            Some(&at) => entries[at].1 = node,
542                            None => {
543                                seen.insert(key.clone(), entries.len());
544                                entries.push((key, node));
545                            }
546                        }
547                        b'}'
548                    }
549                };
550                self.skip_whitespace();
551                match self.peek() {
552                    Some(b',') => {
553                        self.pos += 1;
554                        if closing == b'}' {
555                            let next_key = self.parse_key()?;
556                            if let Some(Frame::Object { key, .. }) = stack.last_mut() {
557                                *key = next_key;
558                            }
559                        }
560                        continue 'descend;
561                    }
562                    Some(byte) if byte == closing => {
563                        self.pos += 1;
564                        node = match stack.pop() {
565                            Some(Frame::Array(items)) => Node::Array(items),
566                            Some(Frame::Object { entries, .. }) => Node::Object(entries),
567                            None => unreachable!("the frame was just borrowed"),
568                        };
569                    }
570                    _ if closing == b']' => return Err(self.error("expected `,` or `]`")),
571                    _ => return Err(self.error("expected `,` or `}`")),
572                }
573            }
574        }
575    }
576
577    /// Parse `"key" :`, leaving the parser on the value.
578    fn parse_key(&mut self) -> Result<String> {
579        self.skip_whitespace();
580        if self.peek() != Some(b'"') {
581            return Err(self.error("key must be a string"));
582        }
583        let key = self.parse_string()?;
584        self.skip_whitespace();
585        if self.peek() != Some(b':') {
586            return Err(self.error("expected `:`"));
587        }
588        self.pos += 1;
589        Ok(key)
590    }
591
592    fn parse_scalar(&mut self) -> Result<Node> {
593        match self.peek() {
594            Some(b'"') => Ok(Node::Str(self.parse_string()?)),
595            Some(b't') => {
596                self.expect_literal("true")?;
597                Ok(Node::Bool(true))
598            }
599            Some(b'f') => {
600                self.expect_literal("false")?;
601                Ok(Node::Bool(false))
602            }
603            Some(b'n') => {
604                self.expect_literal("null")?;
605                Ok(Node::Null)
606            }
607            // Python's json emits and accepts these three (`allow_nan=True` is
608            // the default both ways), so upstream renders documents containing
609            // them instead of rejecting the file.
610            Some(b'N') => {
611                self.expect_literal("NaN")?;
612                Ok(Node::NonFinite("NaN"))
613            }
614            Some(b'I') => {
615                self.expect_literal("Infinity")?;
616                Ok(Node::NonFinite("Infinity"))
617            }
618            Some(b'-') if self.src[self.pos..].starts_with("-Infinity") => {
619                self.pos += "-Infinity".len();
620                Ok(Node::NonFinite("-Infinity"))
621            }
622            Some(b'-' | b'0'..=b'9') => self.parse_number(),
623            Some(_) => Err(self.error("expected value")),
624            None => Err(self.error("EOF while parsing a value")),
625        }
626    }
627
628    /// Read a string token and decode it with `serde_json`, so escapes, lone
629    /// surrogates and raw control characters behave exactly as before.
630    fn parse_string(&mut self) -> Result<String> {
631        let start = self.pos;
632        let mut end = self.pos + 1;
633        loop {
634            match self.bytes.get(end) {
635                None => return Err(self.error_at(self.bytes.len(), "EOF while parsing a string")),
636                Some(b'"') => {
637                    end += 1;
638                    break;
639                }
640                Some(b'\\') => {
641                    end += 1;
642                    // Step over the escaped character whole. A multi-byte
643                    // character after a backslash is invalid JSON, but `end`
644                    // must still land on a UTF-8 boundary or slicing panics
645                    // before `serde_json` gets to reject it.
646                    match self.src[end..].chars().next() {
647                        Some(ch) => end += ch.len_utf8(),
648                        None => {
649                            return Err(
650                                self.error_at(self.bytes.len(), "EOF while parsing a string")
651                            )
652                        }
653                    }
654                }
655                // Continuation bytes are never `"` or `\`, so scanning byte by
656                // byte cannot mistake one for a delimiter.
657                Some(_) => end += 1,
658            }
659        }
660        let decoded: String = serde_json::from_str(&self.src[start..end])
661            .map_err(|error| self.error_at(start, &describe(&error)))?;
662        self.pos = end;
663        Ok(decoded)
664    }
665
666    /// Validate JSON's number grammar before decoding, preserving arbitrary-size
667    /// integers and Python's float overflow to Infinity (#74).
668    fn parse_number(&mut self) -> Result<Node> {
669        let start = self.pos;
670        let mut end = start;
671        while matches!(
672            self.bytes.get(end),
673            Some(b'-' | b'+' | b'.' | b'e' | b'E' | b'0'..=b'9')
674        ) {
675            end += 1;
676        }
677        let token = &self.src[start..end];
678        let digits = token.as_bytes();
679        let mut i = usize::from(digits.first() == Some(&b'-'));
680        if digits.get(i) == Some(&b'0') {
681            i += 1;
682        } else {
683            let first = i;
684            while digits.get(i).is_some_and(u8::is_ascii_digit) {
685                i += 1;
686            }
687            if i == first {
688                return Err(self.error_at(start, "invalid number"));
689            }
690        }
691        let mut floating = false;
692        if digits.get(i) == Some(&b'.') {
693            floating = true;
694            i += 1;
695            let first = i;
696            while digits.get(i).is_some_and(u8::is_ascii_digit) {
697                i += 1;
698            }
699            if i == first {
700                return Err(self.error_at(start, "invalid number"));
701            }
702        }
703        if matches!(digits.get(i), Some(b'e' | b'E')) {
704            floating = true;
705            i += 1;
706            if matches!(digits.get(i), Some(b'+' | b'-')) {
707                i += 1;
708            }
709            let first = i;
710            while digits.get(i).is_some_and(u8::is_ascii_digit) {
711                i += 1;
712            }
713            if i == first {
714                return Err(self.error_at(start, "invalid number"));
715            }
716        }
717        if i != digits.len() {
718            return Err(self.error_at(start, "invalid number"));
719        }
720        self.pos = end;
721        if !floating {
722            return Ok(Node::Number(
723                if token == "-0" { "0" } else { token }.to_string(),
724            ));
725        }
726        let value: f64 = token
727            .parse()
728            .map_err(|_| self.error_at(start, "invalid number"))?;
729        if value.is_infinite() {
730            return Ok(Node::NonFinite(if value.is_sign_negative() {
731                "-Infinity"
732            } else {
733                "Infinity"
734            }));
735        }
736        // `json.dumps` writes a float with `float.__repr__`.
737        Ok(Node::Number(crate::pyformat::float_repr(value)))
738    }
739
740    fn expect_literal(&mut self, literal: &str) -> Result<()> {
741        if self.src[self.pos..].starts_with(literal) {
742            self.pos += literal.len();
743            Ok(())
744        } else {
745            Err(self.error("expected value"))
746        }
747    }
748
749    fn peek(&self) -> Option<u8> {
750        self.bytes.get(self.pos).copied()
751    }
752
753    fn skip_whitespace(&mut self) {
754        while matches!(self.peek(), Some(b' ' | b'\t' | b'\n' | b'\r')) {
755            self.pos += 1;
756        }
757    }
758
759    fn error(&self, message: &str) -> RichError {
760        self.error_at(self.pos, message)
761    }
762
763    fn error_at(&self, pos: usize, message: &str) -> RichError {
764        let (line, column) = self.line_column(pos);
765        RichError::Json(format!("{message} at line {line} column {column}"))
766    }
767
768    fn line_column(&self, pos: usize) -> (usize, usize) {
769        let mut pos = pos.min(self.src.len());
770        while !self.src.is_char_boundary(pos) {
771            pos -= 1;
772        }
773        let before = &self.src[..pos];
774        let line = 1 + before.matches('\n').count();
775        let column = before
776            .rsplit('\n')
777            .next()
778            .map_or(0, |tail| tail.chars().count())
779            + 1;
780        (line, column)
781    }
782}
783
784/// `serde_json`'s message without its own `at line … column …` suffix, which
785/// counts from the start of the token slice rather than the document.
786fn describe(error: &serde_json::Error) -> String {
787    let text = error.to_string();
788    match text.find(" at line ") {
789        Some(at) => text[..at].to_string(),
790        None => text,
791    }
792}
793
794#[cfg(test)]
795mod tests {
796    use super::*;
797    use crate::color::ColorSystem;
798
799    fn render(text: &str) -> String {
800        let console = Console::builder()
801            .force_terminal(true)
802            .color_system(Some(ColorSystem::Truecolor))
803            .width(40)
804            .build();
805        console.render_to_string(&Json::new(text).unwrap())
806    }
807
808    fn render_plain(text: &str, width: usize) -> String {
809        let console = Console::builder().width(width).color_system(None).build();
810        console.render_to_string(&Json::new(text).expect("valid json"))
811    }
812
813    #[test]
814    fn empty_collections_stay_inline() {
815        assert_eq!(render("{}"), "\x1b[1m{\x1b[0m\x1b[1m}\x1b[0m");
816        assert_eq!(render("[]"), "\x1b[1m[\x1b[0m\x1b[1m]\x1b[0m");
817    }
818
819    #[test]
820    fn object_with_scalars() {
821        assert_eq!(
822            render(r#"{"ok": false}"#),
823            "\x1b[1m{\x1b[0m\n  \x1b[1;34m\"ok\"\x1b[0m: \x1b[3;91mfalse\x1b[0m\n\x1b[1m}\x1b[0m"
824        );
825    }
826
827    #[test]
828    fn invalid_json_errors() {
829        assert!(Json::new("{not json}").is_err());
830    }
831
832    #[test]
833    fn non_ascii_stays_utf8_in_input_order() {
834        // Upstream's JSON defaults to ensure_ascii=False, so accented/symbol
835        // characters render as UTF-8 (not \uXXXX), and keys keep input order.
836        // (Byte-parity is guaranteed by the `json_unicode` golden.)
837        let out = render("{\"name\": \"caf\u{e9}\", \"emoji\": \"\u{2764}\"}");
838        assert!(out.contains("caf\u{e9}"), "café stays UTF-8: {out:?}");
839        assert!(out.contains('\u{2764}'), "heart stays UTF-8");
840        let name_at = out.find("name").expect("name key present");
841        let emoji_at = out.find("emoji").expect("emoji key present");
842        assert!(name_at < emoji_at, "keys keep input order");
843    }
844
845    #[test]
846    fn a_long_value_is_wrapped_rather_than_cropped() {
847        // A long string value used to be cut mid-token, so the printed document
848        // was missing data -- and, for JSON, no longer parseable -- at exit 0.
849        let payload = format!("{{\"k\": \"{}\"}}", "y".repeat(120));
850        let out = render_plain(&payload, 40);
851        assert_eq!(
852            out.matches('y').count(),
853            120,
854            "characters were dropped:
855{out}"
856        );
857    }
858
859    /// Upstream wraps the JSON at word boundaries: `JSON.text` asks for
860    /// `no_wrap`, but `Console.print` re-joins it through `Text(sep).join(...)`
861    /// and the joined copy carries neither the flag nor an overflow, so the
862    /// default `fold` word wrap applies. Character folding split words
863    /// (`over t` / `he lazy`), which no upstream output ever shows.
864    ///
865    /// Captured from rich 15.0.0:
866    /// `Console(width=40).print(JSON(...))`.
867    #[test]
868    fn wrapping_breaks_at_word_boundaries() {
869        let payload = r#"{"k": "the quick brown fox jumps over the lazy dog and keeps running for a very long time indeed"}"#;
870        assert_eq!(
871            render_plain(payload, 40),
872            "{\n  \"k\": \"the quick brown fox jumps over \n\
873             the lazy dog and keeps running for a \n\
874             very long time indeed\"\n}"
875        );
876    }
877
878    #[cfg(feature = "json-escape-safe")]
879    #[test]
880    fn adjusted_escape_boundaries_preserve_the_remaining_payload() {
881        let input = format!(r#"{{"v":"aa\u0001{}"}}"#, "b".repeat(40));
882        for width in 6..=20 {
883            let output = Console::builder()
884                .width(width)
885                .force_terminal(false)
886                .build()
887                .render_to_string(&Json::new(&input).unwrap().escape_safe(true));
888            assert_eq!(output.matches('b').count(), 40, "width {width}: {output:?}");
889        }
890    }
891
892    #[cfg(feature = "json-escape-safe")]
893    #[test]
894    fn wrapping_keeps_json_escapes_atomic_at_narrow_widths() {
895        let payload = r#"{"v":"a\"b\\c\nd\u0001e"}"#;
896        for width in 8..=14 {
897            let output = Console::builder()
898                .width(width)
899                .force_terminal(false)
900                .build()
901                .render_to_string(&Json::new(payload).unwrap().escape_safe(true));
902            for line in output.lines() {
903                let bytes = line.as_bytes();
904                let mut index = 0;
905                while index < bytes.len() {
906                    if bytes[index] != b'\\' {
907                        index += 1;
908                        continue;
909                    }
910                    assert!(
911                        index + 1 < bytes.len(),
912                        "split escape at width {width}: {output:?}"
913                    );
914                    if bytes[index + 1] == b'u' {
915                        assert!(
916                            index + 6 <= bytes.len(),
917                            "split unicode escape at width {width}: {output:?}"
918                        );
919                        index += 6;
920                    } else {
921                        index += 2;
922                    }
923                }
924            }
925        }
926    }
927
928    #[cfg(feature = "json-escape-safe")]
929    #[test]
930    fn escape_folding_preserves_bytes_even_below_the_escape_width() {
931        let payload = r#"{"v":"a\"b\\c\nd\u0001eeeeeeeeeeee"}"#;
932        let wide = Console::builder()
933            .width(100)
934            .force_terminal(false)
935            .build()
936            .render_to_string(&Json::new(payload).unwrap().escape_safe(true))
937            .replace('\n', "");
938        for width in 1..=20 {
939            let output = Console::builder()
940                .width(width)
941                .force_terminal(false)
942                .build()
943                .render_to_string(&Json::new(payload).unwrap().escape_safe(true));
944            assert_eq!(output.replace('\n', ""), wide, "width {width}");
945            assert!(output
946                .lines()
947                .all(|line| crate::cells::cell_len(line) <= width));
948        }
949    }
950
951    #[cfg(feature = "json-escape-safe")]
952    #[test]
953    fn escape_cropping_never_emits_a_partial_escape() {
954        let payload = r#""a\"b\\c\nd\u0001eeee""#;
955        for width in 1..=24 {
956            let output = Console::builder()
957                .width(width)
958                .force_terminal(false)
959                .build()
960                .render_to_string(&Json::new(payload).unwrap().no_wrap(true).escape_safe(true));
961            let mut chars = output.chars();
962            while let Some(c) = chars.next() {
963                if c == '\\' {
964                    let next = chars.next().expect("complete short escape");
965                    if next == 'u' {
966                        for _ in 0..4 {
967                            assert!(chars.next().is_some_and(|c| c.is_ascii_hexdigit()));
968                        }
969                    }
970                }
971            }
972        }
973    }
974
975    /// Nested inside another renderable, `JSON.text.no_wrap` survives and each
976    /// line is **cropped** at the width rather than wrapped — see
977    /// [`Json::no_wrap`]. Wrapping here instead was silent content loss: with
978    /// `rich -j doc.json -w 120` on an 80-column console the document was laid
979    /// out at 120 and then cropped to 80 by `Console.print`, so whole runs
980    /// vanished and the surviving text read as if it were contiguous.
981    ///
982    /// Captured from rich-cli 1.8.1 driven by rich 15.0.0:
983    /// `COLUMNS=80 rich -j long.json -w 40`.
984    #[test]
985    fn a_nested_document_is_cropped_rather_than_wrapped() {
986        let payload = r#"{"k": "the quick brown fox jumps over the lazy dog and keeps running for a very long time indeed"}"#;
987        let console = Console::builder().width(40).color_system(None).build();
988        let json = Json::new(payload).expect("valid json").no_wrap(true);
989        assert_eq!(
990            console.render_to_string(&json),
991            "{\n  \"k\": \"the quick brown fox jumps over t\n}"
992        );
993
994        // …and the wrap is still the default, because a bare
995        // `Console.print(JSON(...))` loses the flag in `Text.join`.
996        assert_eq!(
997            render_plain(payload, 40),
998            "{\n  \"k\": \"the quick brown fox jumps over \n\
999             the lazy dog and keeps running for a \n\
1000             very long time indeed\"\n}"
1001        );
1002    }
1003
1004    /// A crop must not cut a double-width character in half: `set_cell_size`
1005    /// drops the straddling character and the line comes out one cell short,
1006    /// never one cell over.
1007    #[test]
1008    fn cropping_never_splits_a_wide_character() {
1009        let console = Console::builder().width(12).color_system(None).build();
1010        let json = Json::new("{\"k\": \"\u{1f306}\u{1f306}\u{1f306}\"}")
1011            .expect("valid json")
1012            .no_wrap(true);
1013        for line in console.render_to_string(&json).lines() {
1014            assert!(
1015                crate::cells::cell_len(line) <= 12,
1016                "line {line:?} overflows the crop"
1017            );
1018        }
1019    }
1020
1021    /// serde_json's default float parser takes a fast path that can land 1 ULP
1022    /// from the value in the file, so the rendered number parsed back to a
1023    /// *different* double. The `float_roundtrip` feature makes parsing exact.
1024    #[test]
1025    fn floats_round_trip_exactly() {
1026        for literal in [
1027            "-938371.9565467801",
1028            "0.1",
1029            "1.7976931348623157e308",
1030            "5e-324",
1031            "3.141592653589793",
1032        ] {
1033            let out = render_plain(&format!("{{\"v\": {literal}}}"), 120);
1034            let rendered: String = out
1035                .split(':')
1036                .nth(1)
1037                .expect("a value after the key")
1038                .trim()
1039                .trim_end_matches(['}', ' ', '\n'])
1040                .to_string();
1041            let want: f64 = literal.parse().expect("literal parses");
1042            let got: f64 = rendered
1043                .parse()
1044                .unwrap_or_else(|_| panic!("rendered {rendered:?}"));
1045            assert_eq!(
1046                got.to_bits(),
1047                want.to_bits(),
1048                "{literal} rendered as {rendered} — a different double"
1049            );
1050        }
1051    }
1052
1053    /// serde_json stops at 128 levels, so a 200-deep document — which CPython
1054    /// parses without complaint — was reported as invalid JSON and the CLI
1055    /// exited 1 on a file upstream renders.
1056    #[test]
1057    fn deep_nesting_is_not_rejected() {
1058        for depth in [128, 129, 200, 1000] {
1059            let payload = format!("{}1{}", "[".repeat(depth), "]".repeat(depth));
1060            let json = Json::new(&payload)
1061                .unwrap_or_else(|error| panic!("depth {depth} rejected: {error}"));
1062            let console = Console::builder()
1063                .width(4 * depth + 8)
1064                .color_system(None)
1065                .build();
1066            let out = console.render_to_string(&json);
1067            assert_eq!(
1068                out.matches('[').count(),
1069                depth,
1070                "depth {depth} did not render every level"
1071            );
1072        }
1073    }
1074
1075    /// Parsing and dropping must cost heap, not stack: a recursive parser (or
1076    /// the compiler's recursive drop glue) turns a deep document into a stack
1077    /// overflow, which kills the process without even an error message.
1078    #[test]
1079    fn very_deep_nesting_does_not_overflow_the_stack() {
1080        let payload = format!("{}1{}", "[".repeat(MAX_DEPTH), "]".repeat(MAX_DEPTH));
1081        let json = Json::new(&payload).expect("deep document parses");
1082        drop(json);
1083        // Drop glue on a tree far past the parse limit stays iterative too.
1084        let mut node = Node::Number("1".to_string());
1085        for _ in 0..100_000 {
1086            node = Node::Array(vec![node]);
1087        }
1088        drop(node);
1089    }
1090
1091    /// Past `MAX_DEPTH` the document is refused with CPython's
1092    /// `RecursionError` message instead of being rendered: its indentation
1093    /// alone is quadratic in the depth, and a 100 000-deep array exhausted
1094    /// memory and was killed.
1095    #[test]
1096    fn nesting_past_the_limit_is_an_error() {
1097        for (open, close, what) in [("[", "]", "array"), ("{\"a\":", "}", "object")] {
1098            for depth in [MAX_DEPTH + 1, 100_000] {
1099                let payload = format!("{}1{}", open.repeat(depth), close.repeat(depth));
1100                let error = Json::new(&payload).err().expect("too deep");
1101                assert!(
1102                    error.to_string().contains(&format!(
1103                        "maximum recursion depth exceeded while decoding a JSON {what}"
1104                    )),
1105                    "{error}"
1106                );
1107            }
1108        }
1109        // An empty container counts as a level, as a call in CPython's scanner.
1110        let payload = format!("{}[]{}", "[".repeat(MAX_DEPTH), "]".repeat(MAX_DEPTH));
1111        assert!(Json::new(&payload).is_err());
1112        let payload = format!(
1113            "{}[]{}",
1114            "[".repeat(MAX_DEPTH - 1),
1115            "]".repeat(MAX_DEPTH - 1)
1116        );
1117        assert!(Json::new(&payload).is_ok());
1118    }
1119
1120    /// Python's json accepts and emits `NaN` / `Infinity` / `-Infinity`
1121    /// (`allow_nan=True` is the default), and rich's JSONHighlighter has no
1122    /// rule that matches them, so upstream prints them *unstyled*. serde_json
1123    /// rejected the whole document.
1124    ///
1125    /// Captured from rich 15.0.0:
1126    /// `Console(width=40, force_terminal=True).print(JSON(...))`.
1127    #[test]
1128    fn non_finite_numbers_render_unstyled() {
1129        assert_eq!(
1130            render(r#"{"a": NaN, "b": Infinity, "c": -Infinity, "d": 1.5}"#),
1131            "\x1b[1m{\x1b[0m\n  \x1b[1;34m\"a\"\x1b[0m: NaN,\n  \x1b[1;34m\"b\"\x1b[0m: \
1132             Infinity,\n  \x1b[1;34m\"c\"\x1b[0m: -Infinity,\n  \x1b[1;34m\"d\"\x1b[0m: \
1133             \x1b[1;36m1.5\x1b[0m\n\x1b[1m}\x1b[0m"
1134        );
1135        // Python spells them with those exact capitalisations and nothing else.
1136        for rejected in [r#"{"a": nan}"#, r#"{"a": inf}"#, r#"{"a": -inf}"#] {
1137            assert!(Json::new(rejected).is_err(), "{rejected} should not parse");
1138        }
1139    }
1140
1141    /// The hand-written reader must accept and reject exactly what serde_json
1142    /// does for bounded numbers — Python also accepts overflowing exponents.
1143    #[test]
1144    fn acceptance_matches_serde_json() {
1145        let samples = [
1146            "{}",
1147            "[]",
1148            "  {\t\"a\" :\n1 }  ",
1149            r#"{"a": 1, "a": 2}"#,
1150            r#"{"a": [1, {"b": null}], "c": "x"}"#,
1151            "0",
1152            "-0",
1153            "0.0",
1154            "1e10",
1155            "1E+10",
1156            "1e-7",
1157            "12345678901234567890",
1158            "-12345678901234567890123456789012345",
1159            "01",
1160            "1.",
1161            ".1",
1162            "+1",
1163            "1e",
1164            "-",
1165            "--1",
1166            "-i",
1167            "-Inf",
1168            "Infinit",
1169            "NAN",
1170            "1 2",
1171            "",
1172            "   ",
1173            "{",
1174            "[",
1175            "]",
1176            "}",
1177            "[,]",
1178            "[1,]",
1179            r#"{"a": 1,}"#,
1180            r#"{a: 1}"#,
1181            r#"{'a': 1}"#,
1182            r#"{"a" 1}"#,
1183            "[1 2]",
1184            "truex",
1185            "tru",
1186            "nul",
1187            r#""unterminated"#,
1188            r#""\q""#,
1189            r#""é""#,
1190            r#""😀""#,
1191            r#""\ud800""#,
1192            "\"raw\nnewline\"",
1193            r#""café ❤""#,
1194            "\"\u{e9}\\\"",
1195            "\u{feff}{}",
1196            "[[[[1]]]]",
1197        ];
1198        for sample in samples {
1199            let ours = Json::new(sample).is_ok();
1200            let theirs = serde_json::from_str::<serde_json::Value>(sample).is_ok();
1201            assert_eq!(ours, theirs, "disagreed about {sample:?}");
1202        }
1203    }
1204
1205    #[test]
1206    fn python_numbers_preserve_large_integers_and_overflow() {
1207        for number in [
1208            "1234567890123456789012345678901234567890",
1209            "-1234567890123456789012345678901234567890",
1210        ] {
1211            assert_eq!(render_plain(number, 100), number);
1212        }
1213        assert_eq!(render_plain("-0", 100), "0");
1214        assert_eq!(render_plain("1e400", 100), "Infinity");
1215        assert_eq!(render_plain("-1e999", 100), "-Infinity");
1216        for invalid in ["01", "-01", "1.e2", "1e+", "1e400x", "--1", "1+2", ".1"] {
1217            assert!(Json::new(invalid).is_err(), "accepted {invalid}");
1218        }
1219    }
1220
1221    /// And the tree it builds must be the tree serde_json would have built.
1222    /// `serde_json::to_string_pretty` happens to use the very layout upstream's
1223    /// `json.dumps(indent=2)` does, so it doubles as a reference dump: key
1224    /// order, repeated-key collapsing, string escaping and number formatting
1225    /// all have to agree.
1226    #[test]
1227    fn the_parsed_tree_matches_serde_json() {
1228        let samples = [
1229            r#"{"name": "Alice", "age": 30, "admin": true, "tags": ["a", "b"], "meta": null}"#,
1230            r#"{"a": 1, "b": 2, "a": 3}"#,
1231            r#"{"a": {"b": {"c": [1, [], {}, [[2]]]}}}"#,
1232            r#"{"k": "A\t\"x\"A\\\/é"}"#,
1233            r#"[0, 12345678901234567890, -7]"#,
1234            r#"{"café": "❤", "": ""}"#,
1235            "[]",
1236            "{}",
1237            "\"top level\"",
1238            "1234",
1239        ];
1240        for sample in samples {
1241            let reference: serde_json::Value =
1242                serde_json::from_str(sample).expect("sample is valid JSON");
1243            assert_eq!(
1244                render_plain(sample, 10_000),
1245                serde_json::to_string_pretty(&reference).expect("value re-serialises"),
1246                "diverged on {sample}"
1247            );
1248        }
1249    }
1250
1251    /// Floats are written with Python's `float.__repr__`, as `json.dumps` does,
1252    /// not serde_json's spelling (`1e-7`, `10000000000.0`).
1253    #[test]
1254    fn floats_render_as_python_repr() {
1255        let out = render_plain(
1256            "[-0.5, 1e10, 1E+10, 1e-7, 1.7976931348623157e308, 1e16]",
1257            10_000,
1258        );
1259        let values: Vec<&str> = out
1260            .lines()
1261            .filter_map(|line| line.trim().strip_suffix(',').or(Some(line.trim())))
1262            .filter(|value| !matches!(*value, "[" | "]"))
1263            .collect();
1264        assert_eq!(
1265            values,
1266            [
1267                "-0.5",
1268                "10000000000.0",
1269                "10000000000.0",
1270                "1e-07",
1271                "1.7976931348623157e+308",
1272                "1e+16"
1273            ]
1274        );
1275    }
1276
1277    /// A repeated key collapses to one entry — first position, last value —
1278    /// which is what both `dict` and serde_json's `preserve_order` produce.
1279    #[test]
1280    fn a_repeated_key_keeps_its_position_and_last_value() {
1281        assert_eq!(
1282            render_plain(r#"{"a": 1, "b": 2, "a": 3}"#, 40),
1283            "{\n  \"a\": 3,\n  \"b\": 2\n}"
1284        );
1285    }
1286
1287    /// Escapes are decoded and re-encoded, because upstream re-serialises the
1288    /// parsed data with `json.dumps`.
1289    #[test]
1290    fn escapes_are_re_encoded_like_dumps() {
1291        assert_eq!(
1292            render_plain(r#"{"k": "A\t\"x\""}"#, 60),
1293            "{\n  \"k\": \"A\\t\\\"x\\\"\"\n}"
1294        );
1295    }
1296}