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}