Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions ext/json/ext/parser/parser.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
38 changes: 38 additions & 0 deletions test/json/resumable_parser_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading