module json2 // increment checks eof and increments checker by one @[inline; markused] fn (mut checker Decoder) increment(message string) ! { if checker.checker_idx + 1 == checker.json.len { if message == '' { return Error{} } checker.checker_error('EOF: ' + message)! } checker.checker_idx++ } // skip_whitespace checks eof and increments checker until next non whitespace character @[inline; markused] fn (mut checker Decoder) skip_whitespace(message string) ! { for checker.json[checker.checker_idx] in whitespace_chars { checker.increment(message)! } } // check_json_format checks if the JSON string is valid and updates the decoder state. @[markused] fn (mut checker Decoder) check_json_format() ! { checker.skip_whitespace('empty json')! start_idx_position := checker.checker_idx // The values nested in this one are added after it, while it is checked. value_idx := checker.values_info.len match checker.json[checker.checker_idx] { `"` { checker.add_value(.string) checker.check_string()! checker.values_info[value_idx].has_escape = checker.string_has_escape } `-`, `0`...`9` { checker.add_value(.number) checker.check_number()! } `t`, `f` { checker.add_value(.boolean) checker.check_boolean()! } `n` { checker.add_value(.null) checker.check_null()! } `[` { checker.add_value(.array) checker.check_array()! } `{` { checker.add_value(.object) checker.check_object()! } else { checker.checker_error('unknown value kind')! } } checker.values_info[value_idx].length = checker.checker_idx + 1 - start_idx_position checker.increment('') or { return } checker.skip_whitespace('') or { return } if checker.json[checker.checker_idx] !in [`,`, `:`, `}`, `]`] { checker.checker_error('invalid value. Unexpected character after ${checker.values_info[value_idx].value_kind} end')! } } @[markused] fn (mut checker Decoder) check_string() ! { checker.string_has_escape = false checker.increment('string not closed')! // check if the JSON string is a valid escape sequence for checker.json[checker.checker_idx] != `"` { if checker.json[checker.checker_idx] == `\\` { checker.string_has_escape = true checker.increment('invalid escape sequence')! escaped_char := checker.json[checker.checker_idx] match escaped_char { `/`, `b`, `f`, `n`, `r`, `t`, `"`, `\\` {} `u` { // check if the JSON string is a valid unicode escape sequence escaped_char_last_index := checker.checker_idx + 4 if escaped_char_last_index < checker.json.len { // 2 bytes for the unicode escape sequence `\u` checker.increment('invalid escape sequence')! for checker.checker_idx < escaped_char_last_index { match checker.json[checker.checker_idx] { `0`...`9`, `a`...`f`, `A`...`F` { checker.increment('invalid unicode escape sequence')! } else { return checker.checker_error('invalid unicode escape sequence') } } } continue } else { return checker.checker_error('short unicode escape sequence ${checker.json[checker.checker_idx - 1..checker.json.len - 1]}') } } else { return checker.checker_error('unknown escape sequence') } } } checker.increment('string not closed')! } } @[markused] fn (mut checker Decoder) check_number() ! { // check if the JSON string is a valid float or integer if checker.json[checker.checker_idx] == `-` { checker.increment('expected digit')! } // integer part if checker.json[checker.checker_idx] == `0` { checker.increment('') or { return } } else if checker.json[checker.checker_idx] >= `1` && checker.json[checker.checker_idx] <= `9` { checker.increment('') or { return } for checker.json[checker.checker_idx] >= `0` && checker.json[checker.checker_idx] <= `9` { checker.increment('') or { return } } } else { return checker.checker_error('expected digit got ${checker.json[checker.checker_idx].ascii_str()}') } // fraction part if checker.json[checker.checker_idx] == `.` { checker.increment('expected digit')! if !(checker.json[checker.checker_idx] >= `0` && checker.json[checker.checker_idx] <= `9`) { return checker.checker_error('expected digit got ${checker.json[checker.checker_idx].ascii_str()}') } for checker.json[checker.checker_idx] >= `0` && checker.json[checker.checker_idx] <= `9` { checker.increment('') or { return } } } // exponent part if checker.json[checker.checker_idx] == `e` || checker.json[checker.checker_idx] == `E` { checker.increment('expected digit')! if checker.json[checker.checker_idx] == `-` || checker.json[checker.checker_idx] == `+` { checker.increment('expected digit')! } if !(checker.json[checker.checker_idx] >= `0` && checker.json[checker.checker_idx] <= `9`) { return checker.checker_error('expected digit got ${checker.json[checker.checker_idx].ascii_str()}') } for checker.json[checker.checker_idx] >= `0` && checker.json[checker.checker_idx] <= `9` { checker.increment('') or { return } } } checker.checker_idx-- } @[markused] fn (mut checker Decoder) check_boolean() ! { // check if the JSON string is a valid boolean match checker.json[checker.checker_idx] { `t` { if checker.json.len - checker.checker_idx <= 3 { return checker.checker_error('EOF error: expecting `true`') } is_not_ok := unsafe { vmemcmp(checker.json.str + checker.checker_idx, c'true', 'true'.len) } if is_not_ok != 0 { return checker.checker_error('invalid boolean value. Got `${checker.json[checker.checker_idx..checker.checker_idx + 4]}` instead of `true`') } checker.checker_idx += 3 } `f` { if checker.json.len - checker.checker_idx <= 4 { return checker.checker_error('EOF error: expecting `false`') } is_not_ok := unsafe { vmemcmp(checker.json.str + checker.checker_idx, c'false', 'false'.len) } if is_not_ok != 0 { return checker.checker_error('invalid boolean value. Got `${checker.json[checker.checker_idx..checker.checker_idx + 5]}` instead of `false`') } checker.checker_idx += 4 } else { return checker.checker_error('invalid boolean') } } } @[markused] fn (mut checker Decoder) check_null() ! { // check if the JSON string is a null value if checker.json.len - checker.checker_idx <= 3 { return checker.checker_error('EOF error: expecting `null`') } is_not_ok := unsafe { vmemcmp(checker.json.str + checker.checker_idx, c'null', 'null'.len) } if is_not_ok != 0 { return checker.checker_error('invalid null value. Got `${checker.json[checker.checker_idx..checker.checker_idx + 4]}` instead of `null`') } checker.checker_idx += 3 } @[markused] fn (mut checker Decoder) check_nested_value(message string) ! { value_idx := checker.values_info.len checker.check_json_format()! value := checker.values_info[value_idx] // A nested value cannot consume the final byte: its parent still needs a // delimiter. Otherwise EOF leaves the cursor on the value's last byte and // the enclosing loop may parse it again or reuse an inner closing bracket. if value.length >= checker.json.len - value.position { return checker.checker_error(message) } } // max_nesting_depth guards the recursion of check_array/check_object: // V's C/WASM backends run on fixed-size stacks, which the checker's // recursion can exhaust at a few thousand nesting levels (measured ~2,600 // on an 8 MiB main-thread stack; Go has no comparable bound because // goroutine stacks grow). A bounded parsing error is preferable to the // process abort that unchecked recursion produces. const max_nesting_depth = 512 @[markused] fn (mut checker Decoder) check_array() ! { if checker.checker_depth >= max_nesting_depth { return checker.checker_error('exceeded the maximum nesting depth') } checker.checker_depth++ defer { checker.checker_depth-- } checker.increment('expected array end')! checker.skip_whitespace('expected array end')! for checker.json[checker.checker_idx] != `]` { checker.check_nested_value('expected array end')! checker.skip_whitespace('expected array end')! if checker.json[checker.checker_idx] == `,` { checker.increment('expected array value')! checker.skip_whitespace('') or {} if checker.json[checker.checker_idx] == `]` { return checker.checker_error('Cannot use `,`, before `]`') } } } } @[markused] fn (mut checker Decoder) check_object() ! { if checker.checker_depth >= max_nesting_depth { return checker.checker_error('exceeded the maximum nesting depth') } checker.checker_depth++ defer { checker.checker_depth-- } checker.increment('expected object end')! checker.skip_whitespace('expected object end')! for checker.json[checker.checker_idx] != `}` { if checker.json[checker.checker_idx] != `"` { checker.checker_error('Expecting object key')! } checker.check_json_format()! checker.skip_whitespace('expected `:`')! if checker.json[checker.checker_idx] != `:` { checker.checker_error('expected `:`, got `${checker.json[checker.checker_idx].ascii_str()}`')! } checker.increment('expected object value')! checker.skip_whitespace('expected object value')! checker.check_nested_value('expected object end')! checker.skip_whitespace('expected object end')! if checker.json[checker.checker_idx] == `,` { checker.increment('expected object key')! checker.skip_whitespace('') or {} if checker.json[checker.checker_idx] == `}` { return checker.checker_error('Cannot use `,`, before `}`') } } } }