Skip to main content

squeeze/
json.rs

1//! Strict JSON finder.
2//!
3//! Finds the first valid JSON value rooted at an object (`{...}`) or array
4//! (`[...]`) in the input. Bare numbers, strings, and literals are intentionally
5//! not surfaced — they're too easy to false-positive on prose.
6//!
7//! Validation follows RFC 8259: it checks string escapes (including `\uXXXX`
8//! with UTF-16 surrogate pairing), number shape, and that the only allowed
9//! JSON literals are `true`, `false`, and `null`. Inputs whose braces happen
10//! to balance but contain garbage (`{not really json}`) are rejected.
11//!
12//! Documents nested deeper than `MAX_DEPTH` (256 levels) are refused outright:
13//! they yield no match at all rather than an arbitrary inner fragment.
14
15use super::{Finder, Memo};
16use std::ops::Range;
17
18const MAX_DEPTH: usize = 256;
19
20/// Why a parse attempt failed.
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22enum ParseError {
23    /// Malformed JSON.
24    Syntax,
25    /// Well-formed so far but nested deeper than `MAX_DEPTH`.
26    TooDeep,
27}
28
29type ParseResult = Result<usize, ParseError>;
30
31#[inline]
32fn memchr2(n1: u8, n2: u8, haystack: &[u8]) -> Option<usize> {
33    haystack.iter().position(|&b| b == n1 || b == n2)
34}
35
36#[inline]
37fn skip_ws(input: &[u8], mut pos: usize) -> usize {
38    while pos < input.len() && matches!(input[pos], b' ' | b'\t' | b'\n' | b'\r') {
39        pos += 1;
40    }
41    pos
42}
43
44/// True for bytes that can extend a nesting run: opening brackets and the
45/// whitespace between them.
46#[inline]
47fn is_open_or_ws(b: u8) -> bool {
48    matches!(b, b'{' | b'[' | b' ' | b'\t' | b'\n' | b'\r')
49}
50
51/// End of the run of opening brackets/whitespace starting at `pos`.
52fn skip_open_run(input: &[u8], pos: usize) -> usize {
53    let mut p = pos;
54    while p < input.len() && is_open_or_ws(input[p]) {
55        p += 1;
56    }
57    p
58}
59
60/// Whether `pos` sits inside a run of opening brackets/whitespace whose
61/// first bracket fails the depth limit. `find` never retries inside such a
62/// run (a too-deep document yields no match at all, not an inner
63/// fragment), so `try_at` must not match there either.
64fn inside_too_deep_run(input: &[u8], pos: usize) -> bool {
65    let mut start = pos;
66    while start > 0 && is_open_or_ws(input[start - 1]) {
67        start -= 1;
68    }
69    match (start..pos).find(|&i| matches!(input[i], b'{' | b'[')) {
70        Some(first) => parse_value(input, first, 0) == Err(ParseError::TooDeep),
71        None => false,
72    }
73}
74
75fn parse_value(input: &[u8], pos: usize, depth: usize) -> ParseResult {
76    if depth > MAX_DEPTH {
77        return Err(ParseError::TooDeep);
78    }
79    let pos = skip_ws(input, pos);
80    if pos >= input.len() {
81        return Err(ParseError::Syntax);
82    }
83    match input[pos] {
84        b'{' => parse_object(input, pos, depth + 1),
85        b'[' => parse_array(input, pos, depth + 1),
86        b'"' => parse_string(input, pos).ok_or(ParseError::Syntax),
87        b't' => parse_literal(input, pos, b"true").ok_or(ParseError::Syntax),
88        b'f' => parse_literal(input, pos, b"false").ok_or(ParseError::Syntax),
89        b'n' => parse_literal(input, pos, b"null").ok_or(ParseError::Syntax),
90        b'-' | b'0'..=b'9' => parse_number(input, pos).ok_or(ParseError::Syntax),
91        _ => Err(ParseError::Syntax),
92    }
93}
94
95fn parse_object(input: &[u8], start: usize, depth: usize) -> ParseResult {
96    debug_assert_eq!(input[start], b'{');
97    let mut pos = start + 1;
98    pos = skip_ws(input, pos);
99    if pos < input.len() && input[pos] == b'}' {
100        return Ok(pos + 1);
101    }
102    loop {
103        pos = skip_ws(input, pos);
104        if pos >= input.len() || input[pos] != b'"' {
105            return Err(ParseError::Syntax);
106        }
107        pos = parse_string(input, pos).ok_or(ParseError::Syntax)?;
108        pos = skip_ws(input, pos);
109        if pos >= input.len() || input[pos] != b':' {
110            return Err(ParseError::Syntax);
111        }
112        pos += 1;
113        pos = parse_value(input, pos, depth)?;
114        pos = skip_ws(input, pos);
115        if pos >= input.len() {
116            return Err(ParseError::Syntax);
117        }
118        match input[pos] {
119            b',' => pos += 1,
120            b'}' => return Ok(pos + 1),
121            _ => return Err(ParseError::Syntax),
122        }
123    }
124}
125
126fn parse_array(input: &[u8], start: usize, depth: usize) -> ParseResult {
127    debug_assert_eq!(input[start], b'[');
128    let mut pos = start + 1;
129    pos = skip_ws(input, pos);
130    if pos < input.len() && input[pos] == b']' {
131        return Ok(pos + 1);
132    }
133    loop {
134        pos = parse_value(input, pos, depth)?;
135        pos = skip_ws(input, pos);
136        if pos >= input.len() {
137            return Err(ParseError::Syntax);
138        }
139        match input[pos] {
140            b',' => pos += 1,
141            b']' => return Ok(pos + 1),
142            _ => return Err(ParseError::Syntax),
143        }
144    }
145}
146
147fn parse_string(input: &[u8], start: usize) -> Option<usize> {
148    debug_assert_eq!(input[start], b'"');
149    let mut pos = start + 1;
150    while pos < input.len() {
151        let b = input[pos];
152        if b == b'"' {
153            return Some(pos + 1);
154        }
155        if b == b'\\' {
156            pos += 1;
157            if pos >= input.len() {
158                return None;
159            }
160            match input[pos] {
161                b'"' | b'\\' | b'/' | b'b' | b'f' | b'n' | b'r' | b't' => pos += 1,
162                b'u' => {
163                    let unit = parse_hex4(input, pos + 1)?;
164                    pos += 5;
165                    if (0xD800..=0xDBFF).contains(&unit) {
166                        // A high surrogate must be immediately followed by
167                        // a `\u` low surrogate; anything else cannot be
168                        // decoded (serde_json rejects lone surrogates).
169                        if pos + 1 >= input.len() || input[pos] != b'\\' || input[pos + 1] != b'u' {
170                            return None;
171                        }
172                        let low = parse_hex4(input, pos + 2)?;
173                        if !(0xDC00..=0xDFFF).contains(&low) {
174                            return None;
175                        }
176                        pos += 6;
177                    } else if (0xDC00..=0xDFFF).contains(&unit) {
178                        // Lone low surrogate.
179                        return None;
180                    }
181                }
182                _ => return None,
183            }
184        } else if b < 0x20 {
185            // Unescaped control character is illegal in JSON strings.
186            return None;
187        } else {
188            pos += 1;
189        }
190    }
191    None
192}
193
194/// Parse exactly four hex digits starting at `start` into a UTF-16 code
195/// unit.
196fn parse_hex4(input: &[u8], start: usize) -> Option<u16> {
197    if start + 4 > input.len() {
198        return None;
199    }
200    let mut value: u16 = 0;
201    for &b in &input[start..start + 4] {
202        let digit = (b as char).to_digit(16)?;
203        value = (value << 4) | digit as u16;
204    }
205    Some(value)
206}
207
208fn parse_number(input: &[u8], start: usize) -> Option<usize> {
209    let mut pos = start;
210    if input[pos] == b'-' {
211        pos += 1;
212        if pos >= input.len() {
213            return None;
214        }
215    }
216    // Integer part.
217    match input.get(pos)? {
218        b'0' => pos += 1,
219        b'1'..=b'9' => {
220            pos += 1;
221            while pos < input.len() && input[pos].is_ascii_digit() {
222                pos += 1;
223            }
224        }
225        _ => return None,
226    }
227    // Fraction.
228    if pos < input.len() && input[pos] == b'.' {
229        pos += 1;
230        let frac_start = pos;
231        while pos < input.len() && input[pos].is_ascii_digit() {
232            pos += 1;
233        }
234        if pos == frac_start {
235            return None;
236        }
237    }
238    // Exponent.
239    if pos < input.len() && (input[pos] == b'e' || input[pos] == b'E') {
240        pos += 1;
241        if pos < input.len() && (input[pos] == b'+' || input[pos] == b'-') {
242            pos += 1;
243        }
244        let exp_start = pos;
245        while pos < input.len() && input[pos].is_ascii_digit() {
246            pos += 1;
247        }
248        if pos == exp_start {
249            return None;
250        }
251    }
252    Some(pos)
253}
254
255fn parse_literal(input: &[u8], start: usize, lit: &[u8]) -> Option<usize> {
256    let end = start + lit.len();
257    if end <= input.len() && &input[start..end] == lit {
258        Some(end)
259    } else {
260        None
261    }
262}
263
264#[derive(Default)]
265pub struct Json {}
266
267impl Finder for Json {
268    fn id(&self) -> &'static str {
269        "json"
270    }
271
272    fn dispatchable(&self) -> bool {
273        true
274    }
275
276    fn could_start_at(&self, byte: u8) -> bool {
277        matches!(byte, b'{' | b'[')
278    }
279
280    fn try_at(&self, input: &[u8], pos: usize) -> Option<Range<usize>> {
281        self.try_at_memo(input, pos, &mut Memo::default())
282    }
283
284    fn try_at_memo(&self, input: &[u8], pos: usize, memo: &mut Memo) -> Option<Range<usize>> {
285        if !matches!(input[pos], b'{' | b'[') {
286            return None;
287        }
288        // Every bracket of a run whose first bracket is too deep yields
289        // nothing (`find` skips the whole run), so remember the run instead
290        // of re-parsing hundreds of levels from each of its brackets.
291        if memo.covers(pos) {
292            return None;
293        }
294        let too_deep = match parse_value(input, pos, 0) {
295            Ok(end) => {
296                // Keep parity with `find`: no inner fragments of too-deep
297                // documents.
298                if !inside_too_deep_run(input, pos) {
299                    return Some(pos..end);
300                }
301                true
302            }
303            Err(ParseError::TooDeep) => true,
304            Err(ParseError::Syntax) => false,
305        };
306        if too_deep {
307            let mut start = pos;
308            while start > 0 && is_open_or_ws(input[start - 1]) {
309                start -= 1;
310            }
311            *memo = Memo {
312                start,
313                end: skip_open_run(input, pos),
314                aux: 0,
315            };
316        }
317        None
318    }
319
320    fn find(&self, s: &str) -> Option<Range<usize>> {
321        let input = s.as_bytes();
322        let mut idx = 0;
323        while let Some(offset) = memchr2(b'{', b'[', &input[idx..]) {
324            idx += offset;
325            match parse_value(input, idx, 0) {
326                Ok(end) => return Some(idx..end),
327                // Skip the whole run of opening brackets/whitespace that
328                // blew the depth limit: a document nested deeper than
329                // MAX_DEPTH yields no match at all, never an arbitrary
330                // inner fragment. This also keeps pathological
331                // all-bracket lines linear instead of O(MAX_DEPTH * n).
332                Err(ParseError::TooDeep) => idx = skip_open_run(input, idx),
333                Err(ParseError::Syntax) => idx += 1,
334            }
335        }
336        None
337    }
338}
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343
344    #[test]
345    fn id_should_return_json() {
346        let finder = Json::default();
347        assert_eq!("json", finder.id());
348    }
349
350    #[test]
351    fn try_at_extracts_json_at_exact_position() {
352        let finder = Json::default();
353        let input = br#"x {"key": "value"} y"#;
354        assert_eq!(Some(2..18), finder.try_at(input, 2));
355        assert_eq!(None, finder.try_at(input, 0));
356    }
357
358    // Objects
359    #[test]
360    fn find_should_extract_simple_object() {
361        let finder = Json::default();
362        let input = r#"data: {"key": "value"}"#;
363        let range = finder.find(input).unwrap();
364        assert_eq!(r#"{"key": "value"}"#, &input[range]);
365    }
366
367    #[test]
368    fn find_should_extract_nested_object() {
369        let finder = Json::default();
370        let input = r#"{"a": {"b": "c"}}"#;
371        let range = finder.find(input).unwrap();
372        assert_eq!(r#"{"a": {"b": "c"}}"#, &input[range]);
373    }
374
375    #[test]
376    fn find_should_extract_object_with_array() {
377        let finder = Json::default();
378        let input = r#"{"items": [1, 2, 3]}"#;
379        let range = finder.find(input).unwrap();
380        assert_eq!(r#"{"items": [1, 2, 3]}"#, &input[range]);
381    }
382
383    #[test]
384    fn find_should_extract_empty_object() {
385        let finder = Json::default();
386        let input = "result: {}";
387        let range = finder.find(input).unwrap();
388        assert_eq!("{}", &input[range]);
389    }
390
391    // Arrays
392    #[test]
393    fn find_should_extract_simple_array() {
394        let finder = Json::default();
395        let input = "data: [1, 2, 3]";
396        let range = finder.find(input).unwrap();
397        assert_eq!("[1, 2, 3]", &input[range]);
398    }
399
400    #[test]
401    fn find_should_extract_nested_array() {
402        let finder = Json::default();
403        let input = "[[1, 2], [3, 4]]";
404        let range = finder.find(input).unwrap();
405        assert_eq!("[[1, 2], [3, 4]]", &input[range]);
406    }
407
408    #[test]
409    fn find_should_extract_empty_array() {
410        let finder = Json::default();
411        let input = "items: []";
412        let range = finder.find(input).unwrap();
413        assert_eq!("[]", &input[range]);
414    }
415
416    // String handling
417    #[test]
418    fn find_should_handle_escaped_quotes() {
419        let finder = Json::default();
420        let input = r#"{"msg": "say \"hello\""}"#;
421        let range = finder.find(input).unwrap();
422        assert_eq!(r#"{"msg": "say \"hello\""}"#, &input[range]);
423    }
424
425    #[test]
426    fn find_should_handle_braces_in_strings() {
427        let finder = Json::default();
428        let input = r#"{"template": "{name}"}"#;
429        let range = finder.find(input).unwrap();
430        assert_eq!(r#"{"template": "{name}"}"#, &input[range]);
431    }
432
433    #[test]
434    fn find_should_handle_brackets_in_strings() {
435        let finder = Json::default();
436        let input = r#"{"pattern": "[a-z]"}"#;
437        let range = finder.find(input).unwrap();
438        assert_eq!(r#"{"pattern": "[a-z]"}"#, &input[range]);
439    }
440
441    #[test]
442    fn find_should_handle_escaped_backslash() {
443        let finder = Json::default();
444        let input = r#"{"path": "C:\\Users\\foo"}"#;
445        let range = finder.find(input).unwrap();
446        assert_eq!(r#"{"path": "C:\\Users\\foo"}"#, &input[range]);
447    }
448
449    // Edge cases
450    #[test]
451    fn find_should_reject_unclosed_object() {
452        let finder = Json::default();
453        assert!(finder.find(r#"{"key": "value""#).is_none());
454    }
455
456    #[test]
457    fn find_should_reject_unclosed_array() {
458        let finder = Json::default();
459        assert!(finder.find("[1, 2, 3").is_none());
460    }
461
462    #[test]
463    fn find_should_handle_empty_input() {
464        let finder = Json::default();
465        assert!(finder.find("").is_none());
466    }
467
468    #[test]
469    fn find_should_extract_json_from_log_line() {
470        let finder = Json::default();
471        let input = r#"2024-01-15 INFO: {"event": "login", "user": "alice"} processed"#;
472        let range = finder.find(input).unwrap();
473        assert_eq!(r#"{"event": "login", "user": "alice"}"#, &input[range]);
474    }
475
476    #[test]
477    fn find_should_extract_first_json_object() {
478        let finder = Json::default();
479        let input = r#"{"a": 1} and {"b": 2}"#;
480        let range = finder.find(input).unwrap();
481        assert_eq!(r#"{"a": 1}"#, &input[range]);
482    }
483
484    // Multiple
485    #[test]
486    fn find_should_extract_multiple_json_iteratively() {
487        let finder = Json::default();
488        let input = r#"{"a": 1} and [2, 3]"#;
489
490        let mut results = Vec::new();
491        let mut idx = 0;
492        while idx < input.len() {
493            if let Some(range) = finder.find(&input[idx..]) {
494                results.push(&input[idx + range.start..idx + range.end]);
495                idx += range.end;
496            } else {
497                break;
498            }
499        }
500
501        assert_eq!(vec![r#"{"a": 1}"#, "[2, 3]"], results);
502    }
503
504    #[test]
505    fn find_should_handle_deeply_nested() {
506        let finder = Json::default();
507        let input = r#"{"a": {"b": {"c": {"d": "deep"}}}}"#;
508        let range = finder.find(input).unwrap();
509        assert_eq!(input, &input[range]);
510    }
511
512    #[test]
513    fn find_should_skip_unclosed_and_find_next() {
514        let finder = Json::default();
515        let input = r#"{unclosed and {"valid": true}"#;
516        let range = finder.find(input).unwrap();
517        assert_eq!(r#"{"valid": true}"#, &input[range]);
518    }
519
520    // --- Regression: memchr-accelerated string scanning ---
521
522    #[test]
523    fn find_should_handle_long_string_value() {
524        let finder = Json::default();
525        let long_val = "a".repeat(10000);
526        let input = format!(r#"{{"key": "{}"}}"#, long_val);
527        let range = finder.find(&input).unwrap();
528        assert_eq!(&input, &input[range]);
529    }
530
531    #[test]
532    fn find_should_handle_string_with_many_escapes() {
533        let finder = Json::default();
534        let escapes = r#"\\\\\\\\\\\\\\\\\\\\\"end"#;
535        let input = format!(r#"{{"key": "{}"}}"#, escapes);
536        let range = finder.find(&input).unwrap();
537        assert_eq!(&input, &input[range]);
538    }
539
540    #[test]
541    fn find_should_handle_string_with_braces_inside() {
542        let finder = Json::default();
543        let input = r#"{"template": "{{not nested}}"}"#;
544        let range = finder.find(input).unwrap();
545        assert_eq!(input, &input[range]);
546    }
547
548    #[test]
549    fn find_should_handle_unclosed_string_in_object() {
550        let finder = Json::default();
551        let input = r#"{"key": "unterminated"#;
552        assert!(finder.find(input).is_none());
553    }
554
555    #[test]
556    fn find_should_handle_empty_strings() {
557        let finder = Json::default();
558        let input = r#"{"": ""}"#;
559        let range = finder.find(input).unwrap();
560        assert_eq!(r#"{"": ""}"#, &input[range]);
561    }
562
563    #[test]
564    fn find_should_handle_escape_at_string_end() {
565        let finder = Json::default();
566        let input = r#"{"key": "val\\"}"#;
567        let range = finder.find(input).unwrap();
568        assert_eq!(input, &input[range]);
569    }
570
571    // --- Strict-mode rejections ---
572
573    #[test]
574    fn find_should_reject_balanced_garbage() {
575        let finder = Json::default();
576        assert!(finder.find("{not really json}").is_none());
577    }
578
579    #[test]
580    fn find_should_reject_object_without_colon() {
581        let finder = Json::default();
582        assert!(finder.find(r#"{"key" "value"}"#).is_none());
583    }
584
585    #[test]
586    fn find_should_reject_object_unquoted_key() {
587        let finder = Json::default();
588        assert!(finder.find(r#"{key: "value"}"#).is_none());
589    }
590
591    #[test]
592    fn find_should_reject_trailing_comma_in_object() {
593        let finder = Json::default();
594        assert!(finder.find(r#"{"a": 1,}"#).is_none());
595    }
596
597    #[test]
598    fn find_should_reject_trailing_comma_in_array() {
599        let finder = Json::default();
600        assert!(finder.find("[1, 2,]").is_none());
601    }
602
603    #[test]
604    fn find_should_reject_single_quoted_strings() {
605        let finder = Json::default();
606        assert!(finder.find(r#"{'key': 'value'}"#).is_none());
607    }
608
609    #[test]
610    fn find_should_reject_javascript_undefined() {
611        let finder = Json::default();
612        assert!(finder.find(r#"{"k": undefined}"#).is_none());
613    }
614
615    #[test]
616    fn find_should_reject_bare_number_with_leading_zero() {
617        let finder = Json::default();
618        assert!(finder.find("[01]").is_none());
619    }
620
621    #[test]
622    fn find_should_reject_bare_dot_number() {
623        let finder = Json::default();
624        // JSON numbers must have a digit before the dot.
625        assert!(finder.find("[.5]").is_none());
626    }
627
628    #[test]
629    fn find_should_reject_trailing_dot_number() {
630        let finder = Json::default();
631        // JSON numbers must have at least one digit after the dot.
632        assert!(finder.find("[1.]").is_none());
633    }
634
635    #[test]
636    fn find_should_reject_invalid_escape_sequence() {
637        let finder = Json::default();
638        assert!(finder.find(r#"{"k": "bad \q escape"}"#).is_none());
639    }
640
641    #[test]
642    fn find_should_reject_short_unicode_escape() {
643        let finder = Json::default();
644        assert!(finder.find(r#"{"k": "\u12"}"#).is_none());
645    }
646
647    #[test]
648    fn find_should_reject_non_hex_unicode_escape() {
649        let finder = Json::default();
650        assert!(finder.find(r#"{"k": "\uZZZZ"}"#).is_none());
651    }
652
653    #[test]
654    fn find_should_reject_unescaped_control_char_in_string() {
655        let finder = Json::default();
656        let input = "{\"k\": \"line1\nline2\"}";
657        assert!(finder.find(input).is_none());
658    }
659
660    #[test]
661    fn find_should_accept_number_with_exponent() {
662        let finder = Json::default();
663        let input = "[1.5e10, -2.0E-3]";
664        let range = finder.find(input).unwrap();
665        assert_eq!(input, &input[range]);
666    }
667
668    #[test]
669    fn find_should_accept_literals() {
670        let finder = Json::default();
671        let input = "[true, false, null]";
672        let range = finder.find(input).unwrap();
673        assert_eq!(input, &input[range]);
674    }
675
676    #[test]
677    fn find_should_reject_truncated_literal() {
678        let finder = Json::default();
679        assert!(finder.find("[tru]").is_none());
680    }
681
682    #[test]
683    fn find_should_reject_uppercase_literal() {
684        let finder = Json::default();
685        assert!(finder.find("[True]").is_none());
686    }
687
688    #[test]
689    fn find_should_accept_whitespace_around_values() {
690        let finder = Json::default();
691        let input = "{  \"k\"  :  \"v\"  ,  \"n\"  :  42  }";
692        let range = finder.find(input).unwrap();
693        assert_eq!(input, &input[range]);
694    }
695
696    #[test]
697    fn find_should_accept_unicode_escapes() {
698        let finder = Json::default();
699        let input = r#"{"k": "éclair"}"#;
700        let range = finder.find(input).unwrap();
701        assert_eq!(input, &input[range]);
702    }
703
704    #[test]
705    fn find_should_reject_array_with_two_consecutive_commas() {
706        let finder = Json::default();
707        assert!(finder.find("[1,,2]").is_none());
708    }
709
710    #[test]
711    fn find_should_reject_extra_close_brace() {
712        let finder = Json::default();
713        // The first valid object is `{"a":1}`. We stop at its end; trailing
714        // `}` is not the finder's concern.
715        let input = r#"{"a":1}}"#;
716        let r = finder.find(input).unwrap();
717        assert_eq!(r#"{"a":1}"#, &input[r]);
718    }
719}