Skip to main content

jsonette/parser/
tolerant.rs

1/*
2 * Copyright (c) 2026 DevEtte.
3 *
4 * This project is dual-licensed under both the MIT License and the
5 * Apache License, Version 2.0 (the "License"). You may not use this
6 * file except in compliance with one of these licenses.
7 *
8 * You may obtain a copy of the Licenses at:
9 * - MIT: https://opensource.org
10 * - Apache 2.0: http://apache.org
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 */
18
19//! Strict JSON parser implementation carrying byte-accurate spans for AST nodes.
20
21use crate::json_node::{JsonNode, KeyValuePair};
22use crate::types::{Diagnostic, Span};
23
24macro_rules! t_err {
25    ($self:expr, $pos:expr, $msg:expr $(,)?) => {{
26        let d = $self.error($pos, $msg);
27        $self.diagnostics.push(d);
28    }};
29}
30
31/// Tolerant parsing: Fails entirely if the JSON is invalid.
32/// Returns the parsed tree or a list of diagnostic errors.
33/// Primarily used for final validation.
34///
35/// # Arguments
36///
37/// * `input` - The raw JSON string slice to parse.
38///
39/// # Returns
40///
41/// * `Some(JsonNode)` - The parsed JSON abstract syntax tree (AST) on successful parse.
42/// * `Err(Vec<Diagnostic>)` - A list of syntax or structural errors found during parsing.
43pub fn parse(input: &str) -> (Option<JsonNode>, Vec<Diagnostic>) {
44    let mut parser = Parser::new(input);
45    let node = parser.parse_value();
46    parser.skip_whitespace();
47    if parser.cursor < parser.input.len() {
48        let err = parser.error(
49            parser.cursor,
50            "Unexpected trailing characters after JSON value",
51        );
52        parser.diagnostics.push(err);
53    }
54    (node, parser.diagnostics)
55}
56
57struct Parser<'a> {
58    pub diagnostics: Vec<Diagnostic>,
59    /// The input bytes slice of the JSON document.
60    input: &'a [u8],
61    /// The original input string slice for number parsing and error reporting.
62    input_str: &'a str,
63    /// The current byte offset cursor in the input.
64    cursor: usize,
65}
66
67impl<'a> Parser<'a> {
68    /// Creates a new Parser instance for the given JSON input.
69    fn new(input: &'a str) -> Self {
70        Parser {
71            input: input.as_bytes(),
72            input_str: input,
73            cursor: 0,
74            diagnostics: Vec::new(),
75        }
76    }
77
78    /// Returns the character byte at the current cursor, or `None` if EOF is reached.
79    fn peek(&self) -> Option<u8> {
80        if self.cursor < self.input.len() {
81            Some(self.input[self.cursor])
82        } else {
83            None
84        }
85    }
86
87    /// Returns the character byte at one position ahead of the current cursor, or `None` if EOF is reached.
88    fn peek_next(&self) -> Option<u8> {
89        if self.cursor + 1 < self.input.len() {
90            Some(self.input[self.cursor + 1])
91        } else {
92            None
93        }
94    }
95
96    /// Advances the cursor by one byte.
97    fn advance(&mut self) {
98        if self.cursor < self.input.len() {
99            self.cursor += 1;
100        }
101    }
102
103    /// Skips any ASCII whitespace characters (spaces, tabs, newlines, carriage returns)
104    /// and single-line/multi-line comments if they are allowed in configuration.
105    fn skip_whitespace(&mut self) {
106        let allow_comments = false; // crate::settings::get_settings().parser.allow_comments;
107        loop {
108            let start = self.cursor;
109            // 1. Skip standard whitespace
110            while let Some(b) = self.peek() {
111                if b == b' ' || b == b'\t' || b == b'\n' || b == b'\r' {
112                    self.advance();
113                } else {
114                    break;
115                }
116            }
117            // 2. Skip comments if enabled
118            if allow_comments && self.peek() == Some(b'/') {
119                match self.peek_next() {
120                    Some(b'/') => {
121                        // Line comment: skip until newline or EOF
122                        self.advance(); // skip '/'
123                        self.advance(); // skip '/'
124                        while let Some(c) = self.peek() {
125                            if c == b'\n' {
126                                self.advance();
127                                break;
128                            }
129                            self.advance();
130                        }
131                        continue;
132                    }
133                    Some(b'*') => {
134                        // Block comment: skip until '*/' or EOF
135                        self.advance(); // skip '/'
136                        self.advance(); // skip '*'
137                        while let Some(c) = self.peek() {
138                            if c == b'*' && self.peek_next() == Some(b'/') {
139                                self.advance(); // skip '*'
140                                self.advance(); // skip '/'
141                                break;
142                            }
143                            self.advance();
144                        }
145                        continue;
146                    }
147                    _ => {}
148                }
149            }
150
151            if self.cursor == start {
152                break;
153            }
154        }
155    }
156
157    /// Helper to create a single-character `Diagnostic` error starting at the given position.
158    fn error(&self, pos: usize, message: impl Into<String>) -> Diagnostic {
159        let end = (pos + 1).min(self.input.len());
160        Diagnostic {
161            span: Span { start: pos, end },
162            message: message.into(),
163        }
164    }
165
166    /// Main entry point to parse a JSON value (null, bool, number, string, array, object).
167    fn parse_value(&mut self) -> Option<JsonNode> {
168        self.skip_whitespace();
169        let start = self.cursor;
170        let b = match self.peek() {
171            Some(b) => b,
172            None => {
173                return {
174                    t_err!(self, start, "Unexpected end of input");
175                    None
176                };
177            }
178        };
179
180        match b {
181            b'n' => self.parse_null(),
182            b't' | b'f' => self.parse_bool(),
183            b'"' => self.parse_string_node(),
184            b'[' => self.parse_array(),
185            b'{' => self.parse_object(),
186            b'-' | b'0'..=b'9' => self.parse_number(),
187            _ => {
188                t_err!(self, start, format!("Unexpected character '{}'", b as char));
189                None
190            }
191        }
192    }
193
194    /// Parses a JSON null value.
195    fn parse_null(&mut self) -> Option<JsonNode> {
196        let start = self.cursor;
197        if self.cursor + 4 <= self.input.len()
198            && &self.input[self.cursor..self.cursor + 4] == b"null"
199        {
200            self.cursor += 4;
201            Some(JsonNode::Null(Span {
202                start,
203                end: self.cursor,
204            }))
205        } else {
206            {
207                t_err!(self, start, "Expected 'null'");
208                None
209            }
210        }
211    }
212
213    /// Parses a JSON boolean value (true or false).
214    fn parse_bool(&mut self) -> Option<JsonNode> {
215        let start = self.cursor;
216        if self.cursor + 4 <= self.input.len()
217            && &self.input[self.cursor..self.cursor + 4] == b"true"
218        {
219            self.cursor += 4;
220            Some(JsonNode::Bool(
221                true,
222                Span {
223                    start,
224                    end: self.cursor,
225                },
226            ))
227        } else if self.cursor + 5 <= self.input.len()
228            && &self.input[self.cursor..self.cursor + 5] == b"false"
229        {
230            self.cursor += 5;
231            Some(JsonNode::Bool(
232                false,
233                Span {
234                    start,
235                    end: self.cursor,
236                },
237            ))
238        } else {
239            {
240                t_err!(self, start, "Expected boolean value");
241                None
242            }
243        }
244    }
245
246    /// Parses a raw string value, decoding escape characters and surrogate pairs,
247    /// and returns the decoded string and its source span.
248    fn parse_string_raw(&mut self) -> Option<(String, Span)> {
249        let start = self.cursor;
250        if self.peek() != Some(b'"') {
251            return {
252                t_err!(self, start, "Expected opening quote for string");
253                None
254            };
255        }
256        self.advance(); // consume opening quote
257
258        let mut s = String::new();
259        while let Some(b) = self.peek() {
260            match b {
261                b'"' => {
262                    self.advance(); // consume closing quote
263                    return Some((
264                        s,
265                        Span {
266                            start,
267                            end: self.cursor,
268                        },
269                    ));
270                }
271                b'\\' => {
272                    self.advance(); // consume backslash
273                    let esc = match self.peek() {
274                        Some(esc) => esc,
275                        None => {
276                            return {
277                                t_err!(self, self.cursor, "Unterminated string escape");
278                                None
279                            };
280                        }
281                    };
282                    self.advance(); // consume escape char
283                    match esc {
284                        b'"' => s.push('"'),
285                        b'\\' => s.push('\\'),
286                        b'/' => s.push('/'),
287                        b'b' => s.push('\x08'),
288                        b'f' => s.push('\x0c'),
289                        b'n' => s.push('\n'),
290                        b'r' => s.push('\r'),
291                        b't' => s.push('\t'),
292                        b'u' => {
293                            if self.cursor + 4 > self.input.len() {
294                                return {
295                                    t_err!(self, self.cursor, "Invalid unicode escape sequence");
296                                    None
297                                };
298                            }
299                            let hex_str =
300                                std::str::from_utf8(&self.input[self.cursor..self.cursor + 4])
301                                    .map_err(|_| {
302                                        let err = self
303                                            .error(self.cursor, "Invalid utf-8 in unicode escape");
304                                        self.diagnostics.push(err);
305                                    })
306                                    .ok()?;
307                            let code_point = u16::from_str_radix(hex_str, 16)
308                                .map_err(|_| {
309                                    let err =
310                                        self.error(self.cursor, "Invalid hex in unicode escape");
311                                    self.diagnostics.push(err);
312                                })
313                                .ok()?;
314                            self.cursor += 4;
315
316                            if (0xD800..=0xDBFF).contains(&code_point) {
317                                if self.cursor + 6 <= self.input.len()
318                                    && &self.input[self.cursor..self.cursor + 2] == b"\\u"
319                                {
320                                    self.cursor += 2;
321                                    let low_hex_str = std::str::from_utf8(
322                                        &self.input[self.cursor..self.cursor + 4],
323                                    )
324                                    .map_err(|_| {
325                                        let err = self
326                                            .error(self.cursor, "Invalid utf-8 in low surrogate");
327                                        self.diagnostics.push(err);
328                                    })
329                                    .ok()?;
330                                    let low_code_point = u16::from_str_radix(low_hex_str, 16)
331                                        .map_err(|_| {
332                                            let err = self
333                                                .error(self.cursor, "Invalid hex in low surrogate");
334                                            self.diagnostics.push(err);
335                                        })
336                                        .ok()?;
337                                    self.cursor += 4;
338                                    if (0xDC00..=0xDFFF).contains(&low_code_point) {
339                                        let utf32 = (((code_point - 0xD800) as u32) << 10)
340                                            + (low_code_point - 0xDC00) as u32
341                                            + 0x10000;
342                                        if let Some(c) = std::char::from_u32(utf32) {
343                                            s.push(c);
344                                        } else {
345                                            return {
346                                                t_err!(
347                                                    self,
348                                                    self.cursor - 12,
349                                                    "Invalid surrogate pair",
350                                                );
351                                                None
352                                            };
353                                        }
354                                    } else {
355                                        return {
356                                            t_err!(
357                                                self,
358                                                self.cursor - 6,
359                                                "Expected low surrogate after high surrogate",
360                                            );
361                                            None
362                                        };
363                                    }
364                                } else {
365                                    return {
366                                        t_err!(
367                                            self,
368                                            self.cursor,
369                                            "Expected low surrogate after high surrogate",
370                                        );
371                                        None
372                                    };
373                                }
374                            } else if (0xDC00..=0xDFFF).contains(&code_point) {
375                                return {
376                                    t_err!(
377                                        self,
378                                        self.cursor - 6,
379                                        "Unexpected low surrogate without high surrogate",
380                                    );
381                                    None
382                                };
383                            } else {
384                                if let Some(c) = std::char::from_u32(code_point as u32) {
385                                    s.push(c);
386                                } else {
387                                    return {
388                                        t_err!(self, self.cursor - 6, "Invalid unicode code point");
389                                        None
390                                    };
391                                }
392                            }
393                        }
394                        _ => {
395                            return {
396                                t_err!(
397                                    self,
398                                    self.cursor - 1,
399                                    format!("Invalid escape character '{}'", esc as char),
400                                );
401                                None
402                            };
403                        }
404                    }
405                }
406                b @ 0..=0x1f => {
407                    return {
408                        t_err!(self, self.cursor, "Control characters must be escaped");
409                        None
410                    };
411                }
412                _ => {
413                    let tail = &self.input_str[self.cursor..];
414                    let c = match tail.chars().next() {
415                        Some(ch) => ch,
416                        None => {
417                            return {
418                                t_err!(self, self.cursor, "Unexpected EOF");
419                                None
420                            };
421                        }
422                    };
423                    self.cursor += c.len_utf8();
424                    s.push(c);
425                }
426            }
427        }
428        {
429            t_err!(self, start, "Unterminated string");
430            Some((
431                s,
432                Span {
433                    start,
434                    end: self.cursor,
435                },
436            ))
437        }
438    }
439
440    /// Parses a JSON string value.
441    fn parse_string_node(&mut self) -> Option<JsonNode> {
442        let (s, span) = self.parse_string_raw()?;
443        Some(JsonNode::String(s, span))
444    }
445
446    /// Parses a JSON number value.
447    fn parse_number(&mut self) -> Option<JsonNode> {
448        let start = self.cursor;
449
450        if self.peek() == Some(b'-') {
451            self.advance();
452        }
453
454        match self.peek() {
455            Some(b'0') => {
456                self.advance();
457            }
458            Some(b) if b.is_ascii_digit() => {
459                while let Some(next_b) = self.peek() {
460                    if next_b.is_ascii_digit() {
461                        self.advance();
462                    } else {
463                        break;
464                    }
465                }
466            }
467            _ => {
468                return {
469                    t_err!(self, start, "Expected digit for number");
470                    None
471                };
472            }
473        }
474
475        if self.peek() == Some(b'.') {
476            self.advance();
477            while let Some(next_b) = self.peek() {
478                if next_b.is_ascii_digit() {
479                    self.advance();
480                } else {
481                    break;
482                }
483            }
484        }
485
486        if let Some(b'e' | b'E') = self.peek() {
487            self.advance();
488            if let Some(b'+' | b'-') = self.peek() {
489                self.advance();
490            }
491            while let Some(next_b) = self.peek() {
492                if next_b.is_ascii_digit() {
493                    self.advance();
494                } else {
495                    break;
496                }
497            }
498        }
499
500        let end = self.cursor;
501        let span = Span { start, end };
502        let raw_str = self.input_str[start..end].to_string();
503
504        let val: f64 = raw_str.parse().unwrap_or(0.0);
505
506        Some(JsonNode::Number(val, raw_str, span))
507    }
508
509    /// Parses a JSON array value.
510    fn parse_array(&mut self) -> Option<JsonNode> {
511        let start = self.cursor;
512        if self.peek() != Some(b'[') {
513            {
514                t_err!(self, start, "Expected '['");
515                return None;
516            }
517        }
518        self.advance(); // consume '['
519
520        self.skip_whitespace();
521        if self.peek() == Some(b']') {
522            self.advance(); // consume ']'
523            return Some(JsonNode::Array(
524                vec![],
525                Span {
526                    start,
527                    end: self.cursor,
528                },
529            ));
530        }
531
532        let mut elements = Vec::new();
533        #[allow(clippy::while_let_loop)]
534        loop {
535            let val = if let Some(v) = self.parse_value() {
536                v
537            } else {
538                break;
539            };
540            elements.push(val);
541
542            self.skip_whitespace();
543            match self.peek() {
544                Some(b',') => {
545                    self.advance();
546                    self.skip_whitespace();
547                    if self.peek() == Some(b']') {
548                        if !crate::settings::get_settings().parser.allow_trailing_commas {
549                            t_err!(self, self.cursor, "Trailing commas are not allowed in JSON");
550                        }
551                        self.advance();
552                        break;
553                    }
554                }
555                Some(b']') => {
556                    self.advance();
557                    break;
558                }
559                Some(b) => {
560                    t_err!(
561                        self,
562                        self.cursor,
563                        format!(
564                            "Expected ',' or ']' after array element, found '{}'",
565                            b as char
566                        )
567                    );
568                    break;
569                }
570                None => {
571                    t_err!(self, self.cursor, "Unterminated array");
572                    break;
573                }
574            }
575        }
576
577        Some(JsonNode::Array(
578            elements,
579            Span {
580                start,
581                end: self.cursor,
582            },
583        ))
584    }
585
586    /// Parses a JSON object value.
587    fn parse_object(&mut self) -> Option<JsonNode> {
588        let start = self.cursor;
589        if self.peek() != Some(b'{') {
590            {
591                t_err!(self, start, "Expected '{'");
592                return None;
593            }
594        }
595        self.advance(); // consume '{'
596
597        self.skip_whitespace();
598        if self.peek() == Some(b'}') {
599            self.advance(); // consume '}'
600            return Some(JsonNode::Object(
601                vec![],
602                Span {
603                    start,
604                    end: self.cursor,
605                },
606            ));
607        }
608
609        let mut pairs = Vec::new();
610        #[allow(clippy::while_let_loop)]
611        loop {
612            self.skip_whitespace();
613            let key_start = self.cursor;
614            if self.peek() != Some(b'"') {
615                {
616                    t_err!(self, key_start, "Expected string key in object");
617                    break;
618                }
619            }
620            let (key, _) = if let Some(k) = self.parse_string_raw() {
621                k
622            } else {
623                break;
624            };
625
626            self.skip_whitespace();
627            let colon_pos = self.cursor;
628            if self.peek() != Some(b':') {
629                {
630                    t_err!(self, colon_pos, "Expected ':' after key");
631                    break;
632                }
633            }
634            self.advance(); // consume ':'
635
636            let val = if let Some(v) = self.parse_value() {
637                v
638            } else {
639                break;
640            };
641            pairs.push(KeyValuePair { key, value: val });
642
643            self.skip_whitespace();
644            match self.peek() {
645                Some(b',') => {
646                    self.advance();
647                    self.skip_whitespace();
648                    if self.peek() == Some(b'}') {
649                        if !crate::settings::get_settings().parser.allow_trailing_commas {
650                            t_err!(self, self.cursor, "Trailing commas are not allowed in JSON");
651                        }
652                        self.advance();
653                        break;
654                    }
655                }
656                Some(b'}') => {
657                    self.advance();
658                    break;
659                }
660                Some(b) => {
661                    t_err!(
662                        self,
663                        self.cursor,
664                        format!(
665                            "Expected ',' or '}}' after object member, found '{}'",
666                            b as char
667                        )
668                    );
669                    break;
670                }
671                None => {
672                    t_err!(self, self.cursor, "Unterminated object");
673                    break;
674                }
675            }
676        }
677
678        Some(JsonNode::Object(
679            pairs,
680            Span {
681                start,
682                end: self.cursor,
683            },
684        ))
685    }
686}
687
688#[cfg(test)]
689mod tolerant_tests {
690    use super::*;
691
692    /// **Test Case**: Tolerant Parsing of Dangling Values
693    ///
694    /// ### Description
695    /// Verifies that parsing an incomplete object key returns a partial AST and a diagnostic error.
696    ///
697    /// ### Test Procedure
698    /// 1. Parse an object ending abruptly at the colon (`{"a":`).
699    ///
700    /// ### Expected Result
701    /// Returns `Some(JsonNode)` and a non-empty `diagnostics` vector.
702    #[test]
703    fn test_tolerant_dangling_value() {
704        let (node, diagnostics) = parse(r#"{"a":"#);
705        assert!(node.is_some());
706        assert!(!diagnostics.is_empty());
707    }
708
709    /// **Test Case**: Tolerant Parsing of Trailing Object Commas
710    ///
711    /// ### Description
712    /// Verifies that parsing an object with a trailing comma recovers cleanly.
713    ///
714    /// ### Test Procedure
715    /// 1. Parse `{"a": 1,}`.
716    ///
717    /// ### Expected Result
718    /// Returns `Some(JsonNode)` representing the parsed pairs and logs a diagnostic error.
719    #[test]
720    fn test_tolerant_trailing_comma() {
721        let (node, diagnostics) = parse(r#"{"a": 1,"#);
722        assert!(node.is_some());
723        assert!(!diagnostics.is_empty());
724    }
725
726    /// **Test Case**: Tolerant Parsing of Unclosed Strings
727    ///
728    /// ### Description
729    /// Verifies that an unclosed string is captured up to the EOF.
730    ///
731    /// ### Test Procedure
732    /// 1. Parse `{"a": "unclosed`.
733    ///
734    /// ### Expected Result
735    /// Returns `Some(JsonNode)` capturing the string and logs an unclosed string error.
736    #[test]
737    fn test_tolerant_unclosed_string() {
738        let (node, diagnostics) = parse(r#"{"a": "unclosed"#);
739        assert!(node.is_some());
740        assert!(!diagnostics.is_empty());
741    }
742
743    /// **Test Case**: Tolerant Parsing of Trailing Array Commas
744    ///
745    /// ### Description
746    /// Verifies that parsing an array with a trailing comma recovers cleanly.
747    ///
748    /// ### Test Procedure
749    /// 1. Parse `[1, 2,]`.
750    ///
751    /// ### Expected Result
752    /// Returns `Some(JsonNode)` containing the parsed elements and a diagnostic error.
753    #[test]
754    fn test_tolerant_array_trailing_comma() {
755        let (node, diagnostics) = parse(r#"[1, 2,"#);
756        assert!(node.is_some());
757        assert!(!diagnostics.is_empty());
758    }
759
760    /// **Test Case**: Tolerant Parsing of Just an Opening Brace
761    ///
762    /// ### Description
763    /// Verifies that an empty, unclosed object brace recovers a minimal AST.
764    ///
765    /// ### Test Procedure
766    /// 1. Parse `{`.
767    ///
768    /// ### Expected Result
769    /// Returns `Some(JsonNode::Object)` and a diagnostic error.
770    #[test]
771    fn test_tolerant_just_opening_brace() {
772        let (node, diagnostics) = parse(r#"{"#);
773        assert!(node.is_some());
774        assert!(!diagnostics.is_empty());
775    }
776}