Skip to content

Commit f210e6d

Browse files
authored
Merge pull request #3083 from Shopify/reject-unreachable-start-pos
Reject a byte position the lexer cannot start on
2 parents 023fa8a + 49d5b5f commit f210e6d

4 files changed

Lines changed: 98 additions & 14 deletions

File tree

ext/rbs_extension/main.c

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -146,44 +146,66 @@ static VALUE parse_type_try(VALUE a) {
146146
return rbs_struct_to_ruby_value(ctx, type);
147147
}
148148

149-
static void validate_position_range(int start_pos, int end_pos) {
149+
/**
150+
* `end_pos` may point past the end of the buffer: clamping with a large
151+
* number instead of measuring the buffer is ordinary, and the lexer stops at
152+
* the end on its own.
153+
* */
154+
static void validate_position_range(VALUE string, int start_pos, int end_pos) {
150155
if (start_pos < 0 || end_pos < 0) {
151156
rb_raise(rb_eArgError, "negative position range: %d...%d", start_pos, end_pos);
152157
}
153158
if (start_pos > end_pos) {
154159
rb_raise(rb_eArgError, "invalid position range: %d...%d", start_pos, end_pos);
155160
}
161+
162+
long size = RSTRING_LEN(string);
163+
if ((long) start_pos > size) {
164+
rb_raise(rb_eArgError, "position range starts past the end of the buffer: %d...%d, buffer is %ld bytes", start_pos, end_pos, size);
165+
}
156166
}
157167

158168
static rbs_lexer_t *alloc_lexer_from_buffer(rbs_allocator_t *allocator, VALUE string, rb_encoding *encoding, int start_pos, int end_pos) {
159-
validate_position_range(start_pos, end_pos);
169+
validate_position_range(string, start_pos, end_pos);
160170

161171
const char *encoding_name = rb_enc_name(encoding);
162172

163-
return rbs_lexer_new(
173+
rbs_lexer_t *lexer = rbs_lexer_new(
164174
allocator,
165175
rbs_string_from_ruby_string(string),
166176
rbs_encoding_find((const uint8_t *) encoding_name, (const uint8_t *) (encoding_name + strlen(encoding_name))),
167177
start_pos,
168178
end_pos
169179
);
180+
181+
if (lexer == NULL) {
182+
rb_raise(rb_eArgError, "position range starts inside a character: %d...%d", start_pos, end_pos);
183+
}
184+
185+
return lexer;
170186
}
171187

172188
static rbs_parser_t *alloc_parser_from_buffer(VALUE buffer, int start_pos, int end_pos) {
173-
validate_position_range(start_pos, end_pos);
174-
175189
VALUE string = rb_funcall(buffer, rb_intern("content"), 0);
176190
StringValue(string);
177191

192+
validate_position_range(string, start_pos, end_pos);
193+
178194
rb_encoding *encoding = rb_enc_get(string);
179195
const char *encoding_name = rb_enc_name(encoding);
180196

181-
return rbs_parser_new(
197+
rbs_parser_t *parser = rbs_parser_new(
182198
rbs_string_from_ruby_string(string),
183199
rbs_encoding_find((const uint8_t *) encoding_name, (const uint8_t *) (encoding_name + strlen(encoding_name))),
184200
start_pos,
185201
end_pos
186202
);
203+
204+
if (parser == NULL) {
205+
rb_raise(rb_eArgError, "position range starts inside a character: %d...%d", start_pos, end_pos);
206+
}
207+
208+
return parser;
187209
}
188210

189211
static VALUE rbsparser_parse_type(VALUE self, VALUE buffer, VALUE start_pos, VALUE end_pos, VALUE variables, VALUE require_eof, VALUE void_allowed, VALUE self_allowed, VALUE classish_allowed) {

include/rbs/parser.h

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,17 +89,24 @@ RBS_NODISCARD bool rbs_parser_insert_typevar(rbs_parser_t *parser, rbs_constant_
8989
* VALUE string = rb_funcall(buffer, rb_intern("content"), 0);
9090
* rbs_lexer_new(string, 0, 31) // New rbs_lexer_t with buffer content
9191
* ```
92+
*
93+
* Returns `NULL` when `start_pos` is not the first byte of a character in
94+
* `string` -- inside one, or past the end of the buffer. The lexer reaches
95+
* `start_pos` a character at a time, so nowhere else is a position it can
96+
* start from.
9297
* */
93-
rbs_lexer_t *rbs_lexer_new(rbs_allocator_t *, rbs_string_t string, const rbs_encoding_t *encoding, int start_pos, int end_pos);
98+
RBS_NODISCARD rbs_lexer_t *rbs_lexer_new(rbs_allocator_t *, rbs_string_t string, const rbs_encoding_t *encoding, int start_pos, int end_pos);
9499

95100
/**
96101
* Allocate new rbs_parser_t object.
97102
*
98103
* ```
99104
* rbs_parser_new(buffer, string, encoding, 0, 1);
100105
* ```
106+
*
107+
* Returns `NULL` for a `start_pos` that `rbs_lexer_new` rejects.
101108
* */
102-
rbs_parser_t *rbs_parser_new(rbs_string_t string, const rbs_encoding_t *encoding, int start_pos, int end_pos);
109+
RBS_NODISCARD rbs_parser_t *rbs_parser_new(rbs_string_t string, const rbs_encoding_t *encoding, int start_pos, int end_pos);
103110
void rbs_parser_free(rbs_parser_t *parser);
104111

105112
/**

src/parser.c

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3539,12 +3539,18 @@ rbs_lexer_t *rbs_lexer_new(rbs_allocator_t *allocator, rbs_string_t string, cons
35393539
lexer->current_character_bytes = 1;
35403540
}
35413541

3542-
if (start_pos > 0) {
3543-
while (lexer->current.byte_pos < start_pos) {
3544-
rbs_skip(lexer);
3545-
}
3542+
// `rbs_skip` moves a whole character at a time, and moves nothing at all
3543+
// once the input is spent, so this walk can only ever stand on the first
3544+
// byte of a character.
3545+
while (lexer->current.byte_pos < start_pos && lexer->current_code_point != '\0') {
3546+
rbs_skip(lexer);
35463547
}
35473548

3549+
// Stopping anywhere else means `start_pos` is a position the lexer cannot
3550+
// start from: over it, and the walk stepped across a character that
3551+
// straddles it; short of it, and the input ran out first.
3552+
if (lexer->current.byte_pos != start_pos) return NULL;
3553+
35483554
lexer->start = lexer->current;
35493555

35503556
return lexer;
@@ -3554,6 +3560,11 @@ rbs_parser_t *rbs_parser_new(rbs_string_t string, const rbs_encoding_t *encoding
35543560
rbs_allocator_t *allocator = rbs_allocator_init();
35553561

35563562
rbs_lexer_t *lexer = rbs_lexer_new(allocator, string, encoding, start_pos, end_pos);
3563+
if (lexer == NULL) {
3564+
rbs_allocator_free(allocator);
3565+
return NULL;
3566+
}
3567+
35573568
rbs_parser_t *parser = rbs_allocator_alloc(allocator, rbs_parser_t);
35583569

35593570
*parser = (rbs_parser_t) {

test/rbs/type_parsing_test.rb

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1019,14 +1019,58 @@ def test_parse__range_works
10191019
end
10201020

10211021
def test_parse__byte_range_incorrect
1022-
# We want a better error handling ergonomics, but currently simply raises a syntax error.
1022+
# `"🐕🐈"` is 10 bytes: `"` then two four-byte characters then `"`. Bytes 2
1023+
# to 4 are inside the first of them, and the lexer can only ever stop on a
1024+
# character boundary, so there is no honest answer to give.
10231025

10241026
input = '"🐕🐈"'
10251027

1028+
(2..4).each do |start|
1029+
exn = assert_raises ArgumentError do
1030+
Parser.parse_type(input, byte_range: start...)
1031+
end
1032+
1033+
assert_equal "position range starts inside a character: #{start}...10", exn.message
1034+
end
1035+
1036+
# Byte 5 opens `🐈`, so lexing starts exactly where it was asked to. This
1037+
# is the error the three positions above used to be silently rounded up
1038+
# to.
10261039
exn = assert_raises RBS::ParsingError do
1027-
Parser.parse_type(input, byte_range: 2...)
1040+
Parser.parse_type(input, byte_range: 5...)
10281041
end
10291042

10301043
assert_equal "a.rbs:1:2...1:3: Syntax error: unexpected token for simple type, token=`🐈` (ErrorToken)", exn.message
10311044
end
1045+
1046+
def test_parse__byte_range_starting_past_the_end
1047+
# Reaching `start_pos` means stepping to it, and there is nothing left to
1048+
# step over here, so the lexer used to walk forever.
1049+
exn = assert_raises ArgumentError do
1050+
Parser.parse_type("Integer", byte_range: 20...30)
1051+
end
1052+
1053+
assert_equal "position range starts past the end of the buffer: 20...30, buffer is 7 bytes", exn.message
1054+
1055+
# The end of the buffer itself is a boundary like any other, and an
1056+
# `end_pos` past it is how a caller clamps without measuring.
1057+
assert_nil Parser.parse_type("Integer", byte_range: 7...7)
1058+
assert_equal "Integer", Parser.parse_type("Integer", byte_range: 0...9999).to_s
1059+
end
1060+
1061+
def test_parse__byte_range_incorrect_in_euc_jp
1062+
# In EUC-JP the second byte of a character sits in the same range as a
1063+
# first byte, so decoding at the offset would happily report a character.
1064+
# Only the walk from the start of the buffer knows byte 1 is inside one.
1065+
euc = (+"\xC6\xFCFoo").force_encoding(Encoding::EUC_JP) # 日Foo
1066+
1067+
exn = assert_raises ArgumentError do
1068+
Parser.parse_type(euc, byte_range: 1...)
1069+
end
1070+
1071+
assert_equal "position range starts inside a character: 1...5", exn.message
1072+
1073+
assert_equal RBS::TypeName.parse("Foo"),
1074+
Parser.parse_type(euc, byte_range: 2...).name
1075+
end
10321076
end

0 commit comments

Comments
 (0)