diff --git a/ext/json/ext/parser/parser.c b/ext/json/ext/parser/parser.c index e4f140d7..9762fb81 100644 --- a/ext/json/ext/parser/parser.c +++ b/ext/json/ext/parser/parser.c @@ -1642,7 +1642,7 @@ ALWAYS_INLINE(static) bool json_parse_any(JSON_ParserState *state, JSON_ParserCo state->cursor++; value = json_decode_array(state, config, 0); break; - } else if (resumable && next == 0) { + } else if (resumable && eos(state)) { state->cursor = value_start; return false; } @@ -1691,8 +1691,14 @@ ALWAYS_INLINE(static) bool json_parse_any(JSON_ParserState *state, JSON_ParserCo } case 0: - return false; - + // peek() returns 0 both at end-of-stream and for a literal NUL byte in the + // buffer. Only a genuine EOS means "feed me more"; a NUL byte that is not at + // EOS is just an invalid character. + if (eos(state)) { + return false; + } else { + raise_syntax_error("unexpected NULL byte: %s", state); + } default: raise_syntax_error("unexpected character: %s", state); } @@ -1807,7 +1813,7 @@ ALWAYS_INLINE(static) bool json_parse_any(JSON_ParserState *state, JSON_ParserCo case JSON_PHASE_OBJECT_KEY: JSON_UNREACHABLE_RETURN(false); case JSON_PHASE_OBJECT_COLON: goto JSON_PHASE_OBJECT_COLON; } - } else if (resumable && next_char == 0) { + } else if (resumable && eos(state)) { return false; } else { raise_syntax_error("expected ',' or ']' after array value", state); @@ -1858,7 +1864,7 @@ ALWAYS_INLINE(static) bool json_parse_any(JSON_ParserState *state, JSON_ParserCo case JSON_PHASE_OBJECT_KEY: JSON_UNREACHABLE_RETURN(false); case JSON_PHASE_OBJECT_COLON: goto JSON_PHASE_OBJECT_COLON; } - } else if (resumable && next_char == 0) { + } else if (resumable && eos(state)) { return false; } else { raise_syntax_error("expected ',' or '}' after object value, got: %s", state); diff --git a/test/json/resumable_parser_test.rb b/test/json/resumable_parser_test.rb index 40dcb4a4..b48b9e05 100644 --- a/test/json/resumable_parser_test.rb +++ b/test/json/resumable_parser_test.rb @@ -133,6 +133,30 @@ def test_parse_byte_by_byte_numbers assert_resumed_parsing('123 ') end + def test_nul_byte_is_a_syntax_error + # A NUL byte in a structural position must raise, not stall forever waiting for more input + # (peek() returns 0 both at EOS and for a literal NUL byte). + assert_parse_error "\x00" # document value + assert_parse_error "[\x00]" # first array element + assert_parse_error "[1\x00]" # after an array element (',' or ']' expected) + assert_parse_error "[1,\x00]" # array element after ',' + assert_parse_error "{\x00}" # object key + assert_parse_error "{\"a\":1\x00}" # after an object value (',' or '}' expected) + assert_parse_error "{\"a\":1,\x00}" # object key after ',' + end + + def test_incomplete_input_at_structural_positions_resumes + # Counterpart of test_nul_byte_is_a_syntax_error: a genuine EOS at the same positions must + # stay incomplete (return false), not raise -- this is what distinguishes EOS from a NUL. + assert_incomplete "[" + assert_incomplete "[1" + assert_incomplete "[1," + assert_incomplete "{" + assert_incomplete "{\"a\"" + assert_incomplete "{\"a\":1" + assert_incomplete "{\"a\":1," + end + def test_rest @parser << '[1, 2, 3, "unterminated string' refute @parser.parse @@ -316,6 +340,20 @@ def test_buffer_shrink private + def assert_parse_error(json) + parser = new_parser + parser << json + assert_raise(JSON::ParserError, "expected a parse error for #{json.inspect}") do + parser.parse + end + end + + def assert_incomplete(json) + parser = new_parser + parser << json + refute(parser.parse, "expected #{json.inspect} not to produce a value") + end + def assert_partial_value(expected, json) parser = new_parser parser << json