diff --git a/ext/rbs_extension/main.c b/ext/rbs_extension/main.c index edb4bf304..bcbb73386 100644 --- a/ext/rbs_extension/main.c +++ b/ext/rbs_extension/main.c @@ -145,10 +145,17 @@ static VALUE parse_type_try(VALUE a) { return rbs_struct_to_ruby_value(ctx, type); } -static rbs_lexer_t *alloc_lexer_from_buffer(rbs_allocator_t *allocator, VALUE string, rb_encoding *encoding, int start_pos, int end_pos) { +static void validate_position_range(int start_pos, int end_pos) { if (start_pos < 0 || end_pos < 0) { rb_raise(rb_eArgError, "negative position range: %d...%d", start_pos, end_pos); } + if (start_pos > end_pos) { + rb_raise(rb_eArgError, "invalid position range: %d...%d", start_pos, end_pos); + } +} + +static rbs_lexer_t *alloc_lexer_from_buffer(rbs_allocator_t *allocator, VALUE string, rb_encoding *encoding, int start_pos, int end_pos) { + validate_position_range(start_pos, end_pos); const char *encoding_name = rb_enc_name(encoding); @@ -162,9 +169,7 @@ static rbs_lexer_t *alloc_lexer_from_buffer(rbs_allocator_t *allocator, VALUE st } static rbs_parser_t *alloc_parser_from_buffer(VALUE buffer, int start_pos, int end_pos) { - if (start_pos < 0 || end_pos < 0) { - rb_raise(rb_eArgError, "negative position range: %d...%d", start_pos, end_pos); - } + validate_position_range(start_pos, end_pos); VALUE string = rb_funcall(buffer, rb_intern("content"), 0); StringValue(string); diff --git a/test/rbs/parser_test.rb b/test/rbs/parser_test.rb index fa5b26e46..e99e3eab0 100644 --- a/test/rbs/parser_test.rb +++ b/test/rbs/parser_test.rb @@ -1030,6 +1030,21 @@ class Foo[T < Integer] < Bar # Comment assert_equal [:pEOF, '', 57...57], tokens.shift.then { |t| [t[0], t[1].source, t[1].range] } end + def test_invalid_position_range_raises + # Regression: start_pos > end_pos used to cause an infinite loop in the lexer. + assert_raises(ArgumentError) do + RBS::Parser._parse_signature(buffer(""), 1, 0) + end + end + + def test_invalid_byte_range_in_parse_type_raises + # Regression: parse_type's byte_range: keyword reaches _parse_type directly, + # which used to hang on reversed ranges. + assert_raises(ArgumentError) do + RBS::Parser.parse_type("", byte_range: 1..0) + end + end + def test_invalid_utf8_byte_in_comment_does_not_hang # Regression: invalid UTF-8 byte in a comment used to loop forever in the lexer. source = "# \xC2".dup.force_encoding(Encoding::UTF_8)