From 7c1006a6709bbffcfdc9c172ee4f443861268c06 Mon Sep 17 00:00:00 2001 From: ksss Date: Tue, 28 Jul 2026 15:47:23 +0900 Subject: [PATCH] Stop the lexer reading past the end of a byte_range MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `rbs_next_char` ends the input when `byte_pos == end_pos`. `rbs_skip` advances by a whole character, so a multibyte character starting before `end_pos` and ending after it steps over the boundary and equality never holds again — the lexer then reads to the end of the string. The realistic way to land inside a character is to pass a character offset where a byte offset is expected, the mistake #2945 fixed in `parse_inline_*_annotation`. `"日本語"` is 5 characters but 11 bytes, so offset 5 falls inside `本`: Parser.parse_type('"日本語" | Integer', byte_range: 0...5) #=> Types::Union spanning the whole input, rather than an error `require_eof: true` does not catch it, because the lexer really is at EOF by then. It needs a character to straddle the boundary, so ASCII-only input never hits it. Compare with `>=` so stepping over the boundary still ends the input. --- src/lexstate.c | 2 +- test/rbs/type_parsing_test.rb | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/lexstate.c b/src/lexstate.c index 79f275717..eb4b4939c 100644 --- a/src/lexstate.c +++ b/src/lexstate.c @@ -120,7 +120,7 @@ unsigned int rbs_peek(rbs_lexer_t *lexer) { } bool rbs_next_char(rbs_lexer_t *lexer, unsigned int *codepoint, size_t *byte_len) { - if (RBS_UNLIKELY(lexer->current.byte_pos == lexer->end_pos)) { + if (RBS_UNLIKELY(lexer->current.byte_pos >= lexer->end_pos)) { return false; } diff --git a/test/rbs/type_parsing_test.rb b/test/rbs/type_parsing_test.rb index 9a95f146e..f58c42c68 100644 --- a/test/rbs/type_parsing_test.rb +++ b/test/rbs/type_parsing_test.rb @@ -1074,4 +1074,23 @@ def test_parse__byte_range_incorrect_in_euc_jp assert_equal RBS::TypeName.parse("Foo"), Parser.parse_type(euc, byte_range: 2...).name end + + def test_parse__byte_range_ending_mid_character + source = '"日本語" | Integer' + + # Character count 5 as a byte offset lands inside `本` (bytes 4...7). The + # lexer used to step over the boundary and read the whole string instead. + assert_raises RBS::ParsingError do + Parser.parse_type(source, byte_range: 0...'"日本語"'.size) + end + + Parser.parse_type(source, byte_range: 0...'"日本語"'.bytesize, require_eof: true).tap do |type| + assert_instance_of Types::Literal, type + assert_equal "日本語", type.literal + end + + Parser.parse_type("Integer", byte_range: 0...9999).tap do |type| + assert_instance_of Types::ClassInstance, type + end + end end