diff --git a/lib/rbs/wasm/parser.rb b/lib/rbs/wasm/parser.rb index c46aedfa0a..9f111150aa 100644 --- a/lib/rbs/wasm/parser.rb +++ b/lib/rbs/wasm/parser.rb @@ -13,82 +13,90 @@ module RBS class Parser class << self def _parse_signature(buffer, start_pos, end_pos) - validate_position_range(start_pos, end_pos) + validate_position_range(buffer, start_pos, end_pos) encoding = buffer.content.encoding.name - success, bytes = WASM::Runtime.instance.parse_signature(buffer.content, encoding, start_pos, end_pos) - raise_parsing_error(buffer, bytes) unless success + status, bytes = WASM::Runtime.instance.parse_signature(buffer.content, encoding, start_pos, end_pos) + raise_parse_failure(buffer, status, bytes, start_pos, end_pos) unless status == WASM::Runtime::OK WASM::Deserializer.deserialize(bytes, buffer) end def _parse_type(buffer, start_pos, end_pos, variables, require_eof, void_allowed, self_allowed, classish_allowed) - validate_position_range(start_pos, end_pos) + validate_position_range(buffer, start_pos, end_pos) validate_variables(variables) encoding = buffer.content.encoding.name - success, bytes = WASM::Runtime.instance.parse_type(buffer.content, encoding, start_pos, end_pos, variables, require_eof, void_allowed, self_allowed, classish_allowed) - raise_parsing_error(buffer, bytes) unless success + status, bytes = WASM::Runtime.instance.parse_type(buffer.content, encoding, start_pos, end_pos, variables, require_eof, void_allowed, self_allowed, classish_allowed) + raise_parse_failure(buffer, status, bytes, start_pos, end_pos) unless status == WASM::Runtime::OK deserialize_or_nil(bytes, buffer) end def _parse_method_type(buffer, start_pos, end_pos, variables, require_eof) - validate_position_range(start_pos, end_pos) + validate_position_range(buffer, start_pos, end_pos) validate_variables(variables) encoding = buffer.content.encoding.name - success, bytes = WASM::Runtime.instance.parse_method_type(buffer.content, encoding, start_pos, end_pos, variables, require_eof) - raise_parsing_error(buffer, bytes) unless success + status, bytes = WASM::Runtime.instance.parse_method_type(buffer.content, encoding, start_pos, end_pos, variables, require_eof) + raise_parse_failure(buffer, status, bytes, start_pos, end_pos) unless status == WASM::Runtime::OK deserialize_or_nil(bytes, buffer) end def _parse_type_params(buffer, start_pos, end_pos, module_type_params) - validate_position_range(start_pos, end_pos) + validate_position_range(buffer, start_pos, end_pos) encoding = buffer.content.encoding.name - success, bytes = WASM::Runtime.instance.parse_type_params(buffer.content, encoding, start_pos, end_pos, module_type_params) - raise_parsing_error(buffer, bytes) unless success + status, bytes = WASM::Runtime.instance.parse_type_params(buffer.content, encoding, start_pos, end_pos, module_type_params) + raise_parse_failure(buffer, status, bytes, start_pos, end_pos) unless status == WASM::Runtime::OK bytes.empty? ? nil : WASM::Deserializer.deserialize_node_list(bytes, buffer) end def _lex(buffer, end_pos) encoding = buffer.content.encoding.name - _success, bytes = WASM::Runtime.instance.lex(buffer.content, encoding, end_pos) + _status, bytes = WASM::Runtime.instance.lex(buffer.content, encoding, end_pos) WASM::Deserializer.deserialize_tokens(bytes, buffer) end def _parse_inline_leading_annotation(buffer, start_pos, end_pos, variables) - validate_position_range(start_pos, end_pos) + validate_position_range(buffer, start_pos, end_pos) validate_variables(variables) encoding = buffer.content.encoding.name - success, bytes = WASM::Runtime.instance.parse_inline_leading_annotation(buffer.content, encoding, start_pos, end_pos, variables) - raise_parsing_error(buffer, bytes) unless success + status, bytes = WASM::Runtime.instance.parse_inline_leading_annotation(buffer.content, encoding, start_pos, end_pos, variables) + raise_parse_failure(buffer, status, bytes, start_pos, end_pos) unless status == WASM::Runtime::OK deserialize_or_nil(bytes, buffer) end def _parse_inline_trailing_annotation(buffer, start_pos, end_pos, variables) - validate_position_range(start_pos, end_pos) + validate_position_range(buffer, start_pos, end_pos) validate_variables(variables) encoding = buffer.content.encoding.name - success, bytes = WASM::Runtime.instance.parse_inline_trailing_annotation(buffer.content, encoding, start_pos, end_pos, variables) - raise_parsing_error(buffer, bytes) unless success + status, bytes = WASM::Runtime.instance.parse_inline_trailing_annotation(buffer.content, encoding, start_pos, end_pos, variables) + raise_parse_failure(buffer, status, bytes, start_pos, end_pos) unless status == WASM::Runtime::OK deserialize_or_nil(bytes, buffer) end private - # Reject negative or reversed ranges before handing them to the parser, - # matching validate_position_range in the C extension (main.c). A reversed - # range would otherwise make the lexer loop forever inside WebAssembly. - def validate_position_range(start_pos, end_pos) + # Reject the position ranges the parser cannot take, matching + # validate_position_range in the C extension (main.c). + # + # `end_pos` past the end of the buffer is fine: clamping with a large + # number instead of measuring the buffer is ordinary, and the lexer stops + # at the end of the input on its own. + def validate_position_range(buffer, start_pos, end_pos) if start_pos < 0 || end_pos < 0 raise ArgumentError, "negative position range: #{start_pos}...#{end_pos}" end if start_pos > end_pos raise ArgumentError, "invalid position range: #{start_pos}...#{end_pos}" end + + size = buffer.content.bytesize + if start_pos > size + raise ArgumentError, "position range starts past the end of the buffer: #{start_pos}...#{end_pos}, buffer is #{size} bytes" + end end # Reject anything that is not nil or an Array of Symbols, matching @@ -112,6 +120,24 @@ def deserialize_or_nil(bytes, buffer) bytes.empty? ? nil : WASM::Deserializer.deserialize(bytes, buffer) end + # Raise for a status other than OK (see rbs_wasm.c). + # + # A negative status is about the range rather than the source text, so it + # comes with an empty result and an ArgumentError, as in the C extension + # (main.c). Starting past the end of the buffer is plain from the + # buffer's size and rejected above, so a start position that comes back + # rejected can only be one inside a character. + def raise_parse_failure(buffer, status, bytes, start_pos, end_pos) + case status + when WASM::Runtime::INVALID_START_POS + raise ArgumentError, "position range starts inside a character: #{start_pos}...#{end_pos}" + when WASM::Runtime::INVALID_RANGE + raise ArgumentError, "invalid position range: #{start_pos}...#{end_pos}" + else + raise_parsing_error(buffer, bytes) + end + end + # Decodes the error blob written by set_error_result (rbs_wasm.c) and raises # the same error the C extension would (see raise_error in main.c). def raise_parsing_error(buffer, blob) diff --git a/lib/rbs/wasm/runtime.rb b/lib/rbs/wasm/runtime.rb index 40859c9eb6..acb7461a3a 100644 --- a/lib/rbs/wasm/runtime.rb +++ b/lib/rbs/wasm/runtime.rb @@ -16,6 +16,14 @@ module WASM class Runtime include MonitorMixin + # Statuses the parse entry points return (see rbs_wasm.c). A negative one + # is about the range the caller asked for rather than the source text, + # and comes with an empty result. + INVALID_START_POS = -2 + INVALID_RANGE = -1 + PARSE_ERROR = 0 + OK = 1 + class << self def instance @instance ||= new @@ -48,9 +56,9 @@ def initialize end # `content` is the whole buffer; `start_pos`/`end_pos` are the character - # range within it to parse. Each method returns [success, bytes]: on success - # `bytes` is the serialized AST, otherwise it is the error blob (see - # set_error_result in rbs_wasm.c). + # range within it to parse. Each method returns [status, bytes]: with OK + # `bytes` is the serialized AST, with PARSE_ERROR it is the error blob (see + # set_error_result in rbs_wasm.c), and with a negative status it is empty. def parse_signature(content, encoding, start_pos, end_pos) run(content, encoding) { |ptr, len, enc_ptr, enc_len| @parse_signature.apply(ptr, len, enc_ptr, enc_len, start_pos, end_pos)[0] } @@ -118,7 +126,7 @@ def run(source, encoding) @memory.write(source_ptr, bytes.to_java_bytes) @memory.write(name_ptr, name.to_java_bytes) unless name_length.zero? status = yield(source_ptr, length, name_ptr, name_length) - [status == 1, read_result] + [i32(status), read_result] ensure @free.apply(source_ptr) @free.apply(name_ptr) @@ -156,6 +164,13 @@ def with_variables(variables) end end + # A WebAssembly i32 comes back in a JVM long, so read the low 32 bits as + # signed: the negative statuses have to stay negative on this side. + def i32(value) + value &= 0xFFFF_FFFF + value >= 0x8000_0000 ? value - 0x1_0000_0000 : value + end + def bool(value) value ? 1 : 0 end diff --git a/wasm/README.md b/wasm/README.md index 174a56a850..1e5bf9e1d5 100644 --- a/wasm/README.md +++ b/wasm/README.md @@ -81,10 +81,21 @@ Memory management and results: Parsing — each takes the whole buffer (`ptr`/`len`), its Ruby encoding name (`enc`/`enc_len`, e.g. `"UTF-8"` or `"EUC-JP"`; falls back to UTF-8 when empty or -unknown), and the character range to parse (`start`/`end`). Each returns `1` on -success or `0` on a parse error. On success the result is the serialized AST; on -error it is an error blob (start/end positions, syntax flag, token type, -message). Type/method-type parsing also takes a buffer of newline-separated +unknown), and the character range to parse (`start`/`end`). Each returns: + +| Status | Meaning | Result | +| --- | --- | --- | +| `1` | Parsed. | The serialized AST. | +| `0` | Parse error. | An error blob (start/end positions, syntax flag, token type, message). | +| `-1` | Negative or reversed range. | Empty. | +| `-2` | `start` is a byte position no character starts at — inside a character, or past the end of the buffer. | Empty. | + +An `end` past the end of the buffer is not an error: it is clamped to the +buffer, which is where lexing stops anyway. The two negative statuses are about +the range the caller asked for rather than the source text, and `RBS::Parser` +turns both into an `ArgumentError`, as the C extension does. + +Type/method-type parsing also takes a buffer of newline-separated type-variable names (`vars`/`vars_len`, with `vars_len < 0` meaning "none"): | Export | Signature | diff --git a/wasm/rbs_wasm.c b/wasm/rbs_wasm.c index efe9a609c9..faf09bcec9 100644 --- a/wasm/rbs_wasm.c +++ b/wasm/rbs_wasm.c @@ -29,6 +29,19 @@ #include "rbs/util/rbs_buffer.h" #include "rbs/util/rbs_encoding.h" +// Status returned by the parse entry points. +// +// A negative status is about the range the caller asked for rather than the +// source text, and leaves the result empty: `RBS_WASM_INVALID_START_POS` is +// the `NULL` `rbs_parser_new` returns for a byte position no character starts +// at -- inside a character, or past the end of the buffer. `RBS::Parser` +// raises ArgumentError for both, as the C extension does for the same `NULL` +// (main.c). +#define RBS_WASM_INVALID_START_POS (-2) +#define RBS_WASM_INVALID_RANGE (-1) +#define RBS_WASM_PARSE_ERROR 0 +#define RBS_WASM_OK 1 + // The result of the most recent parse, living in linear memory until the next // call replaces it. WebAssembly is little-endian, so the multi-byte integers // written below match the little-endian format the Ruby decoder expects. @@ -82,7 +95,7 @@ rbs_wasm_result_len(void) { // [i32 start_char][i32 end_char][u8 syntax_error] // [u32 token_type_len][token_type bytes][u32 message_len][message bytes] // -// Always returns 0, the failure status for the parse functions. +// Always returns RBS_WASM_PARSE_ERROR, the failure status for the parse functions. static int set_error_result(rbs_parser_t *parser) { rbs_error_t *error = parser->error; const char *token_type = rbs_token_type_str(error->token.type); @@ -110,21 +123,30 @@ static int set_error_result(rbs_parser_t *parser) { p += 4; memcpy(p, message, message_len); - return 0; + return RBS_WASM_PARSE_ERROR; } static int set_serialized_result(rbs_parser_t *parser, rbs_node_t *node) { rbs_string_t bytes = rbs_serialize_node(parser->allocator, &parser->constant_pool, node); size_t length = rbs_string_len(bytes); memcpy(allocate_result(length), bytes.start, length); - return 1; + return RBS_WASM_OK; } -// A reversed or out-of-bounds range would make the lexer loop forever, which -// would hang the whole host. Hosts are expected to validate too (RBS::Parser -// raises on bad ranges), but guard here so a stray caller can never wedge the VM. -static bool range_is_valid(int start_pos, int end_pos, int length) { - return start_pos >= 0 && end_pos >= 0 && start_pos <= end_pos && end_pos <= length; +// A negative or reversed range is the caller's mistake rather than anything +// about the source text. Hosts are expected to reject it too (RBS::Parser +// raises on bad ranges), but the ABI is public, so check here as well. +static bool range_is_valid(int start_pos, int end_pos) { + return start_pos >= 0 && end_pos >= 0 && start_pos <= end_pos; +} + +// An `end_pos` past the end of the buffer is how a caller clamps without +// measuring, and the lexer stops at the end of the input on its own -- but it +// only recognises the end where it can read a NUL. The C extension has the +// Ruby string's terminator for that; a buffer the host wrote into linear +// memory has nothing behind it, so its size is where lexing has to stop. +static int clamp_end_pos(int end_pos, int length) { + return end_pos < length ? end_pos : length; } // Resolve a Ruby encoding name (e.g. "UTF-8", "EUC-JP") to an rbs encoding, @@ -171,17 +193,22 @@ static void declare_variables(rbs_parser_t *parser, const char *variables, int v * to parse, so reported locations are absolute (this mirrors * RBS::Parser._parse_signature). * - * @return 1 on success (result is the serialized AST), 0 on a parse error - * (result is an error blob). + * @return RBS_WASM_OK on success (result is the serialized AST), + * RBS_WASM_PARSE_ERROR on a parse error (result is an error blob), or + * a negative status for a range the parser will not take. */ __attribute__((export_name("rbs_wasm_parse_signature"))) int rbs_wasm_parse_signature(const char *source, int length, const char *encoding, int encoding_length, int start_pos, int end_pos) { - if (!range_is_valid(start_pos, end_pos, length)) { + if (!range_is_valid(start_pos, end_pos)) { allocate_result(0); - return 0; + return RBS_WASM_INVALID_RANGE; } rbs_string_t string = rbs_string_new(source, source + length); - rbs_parser_t *parser = rbs_parser_new(string, resolve_encoding(encoding, encoding_length), start_pos, end_pos); + rbs_parser_t *parser = rbs_parser_new(string, resolve_encoding(encoding, encoding_length), start_pos, clamp_end_pos(end_pos, length)); + if (parser == NULL) { + allocate_result(0); + return RBS_WASM_INVALID_START_POS; + } rbs_signature_t *signature = NULL; rbs_parse_signature(parser, &signature); @@ -201,23 +228,30 @@ __attribute__((export_name("rbs_wasm_parse_signature"))) int rbs_wasm_parse_sign * Parse a single RBS type. * * @param variables Newline-separated type variable names (length < 0 for none). - * @return 1 on success, 0 on a parse error. On success with an empty result - * (`rbs_wasm_result_len` == 0), the input was empty (`nil`). + * @return RBS_WASM_OK on success, RBS_WASM_PARSE_ERROR on a parse error, or a + * negative status for a range the parser will not take. On success + * with an empty result (`rbs_wasm_result_len` == 0), the input was + * empty (`nil`). */ __attribute__((export_name("rbs_wasm_parse_type"))) int rbs_wasm_parse_type(const char *source, int length, const char *encoding, int encoding_length, int start_pos, int end_pos, const char *variables, int variables_length, int require_eof, int void_allowed, int self_allowed, int classish_allowed) { - if (!range_is_valid(start_pos, end_pos, length)) { + if (!range_is_valid(start_pos, end_pos)) { allocate_result(0); - return 0; + return RBS_WASM_INVALID_RANGE; } rbs_string_t string = rbs_string_new(source, source + length); - rbs_parser_t *parser = rbs_parser_new(string, resolve_encoding(encoding, encoding_length), start_pos, end_pos); + rbs_parser_t *parser = rbs_parser_new(string, resolve_encoding(encoding, encoding_length), start_pos, clamp_end_pos(end_pos, length)); + if (parser == NULL) { + allocate_result(0); + return RBS_WASM_INVALID_START_POS; + } + declare_variables(parser, variables, variables_length); int status; if (parser->next_token.type == pEOF) { allocate_result(0); - status = 1; + status = RBS_WASM_OK; } else { rbs_node_t *type = NULL; rbs_parse_type(parser, &type, void_allowed != 0, self_allowed != 0, classish_allowed != 0); @@ -240,23 +274,29 @@ __attribute__((export_name("rbs_wasm_parse_type"))) int rbs_wasm_parse_type(cons * Parse a single RBS method type. * * @param variables Newline-separated type variable names (length < 0 for none). - * @return 1 on success, 0 on a parse error. On success with an empty result, - * the input was empty (`nil`). + * @return RBS_WASM_OK on success, RBS_WASM_PARSE_ERROR on a parse error, or a + * negative status for a range the parser will not take. On success + * with an empty result, the input was empty (`nil`). */ __attribute__((export_name("rbs_wasm_parse_method_type"))) int rbs_wasm_parse_method_type(const char *source, int length, const char *encoding, int encoding_length, int start_pos, int end_pos, const char *variables, int variables_length, int require_eof) { - if (!range_is_valid(start_pos, end_pos, length)) { + if (!range_is_valid(start_pos, end_pos)) { allocate_result(0); - return 0; + return RBS_WASM_INVALID_RANGE; } rbs_string_t string = rbs_string_new(source, source + length); - rbs_parser_t *parser = rbs_parser_new(string, resolve_encoding(encoding, encoding_length), start_pos, end_pos); + rbs_parser_t *parser = rbs_parser_new(string, resolve_encoding(encoding, encoding_length), start_pos, clamp_end_pos(end_pos, length)); + if (parser == NULL) { + allocate_result(0); + return RBS_WASM_INVALID_START_POS; + } + declare_variables(parser, variables, variables_length); int status; if (parser->next_token.type == pEOF) { allocate_result(0); - status = 1; + status = RBS_WASM_OK; } else { rbs_method_type_t *method_type = NULL; rbs_parse_method_type(parser, &method_type, require_eof != 0, true); @@ -273,18 +313,22 @@ __attribute__((export_name("rbs_wasm_parse_method_type"))) int rbs_wasm_parse_me * is a serialized node list; an empty result means the input was empty (`nil`). */ __attribute__((export_name("rbs_wasm_parse_type_params"))) int rbs_wasm_parse_type_params(const char *source, int length, const char *encoding, int encoding_length, int start_pos, int end_pos, int module_type_params) { - if (!range_is_valid(start_pos, end_pos, length)) { + if (!range_is_valid(start_pos, end_pos)) { allocate_result(0); - return 0; + return RBS_WASM_INVALID_RANGE; } rbs_string_t string = rbs_string_new(source, source + length); - rbs_parser_t *parser = rbs_parser_new(string, resolve_encoding(encoding, encoding_length), start_pos, end_pos); + rbs_parser_t *parser = rbs_parser_new(string, resolve_encoding(encoding, encoding_length), start_pos, clamp_end_pos(end_pos, length)); + if (parser == NULL) { + allocate_result(0); + return RBS_WASM_INVALID_START_POS; + } int status; if (parser->next_token.type == pEOF) { allocate_result(0); - status = 1; + status = RBS_WASM_OK; } else { rbs_node_list_t *params = NULL; rbs_parse_type_params(parser, module_type_params != 0, ¶ms); @@ -293,7 +337,7 @@ __attribute__((export_name("rbs_wasm_parse_type_params"))) int rbs_wasm_parse_ty rbs_string_t bytes = rbs_serialize_node_list(parser->allocator, &parser->constant_pool, params); size_t n = rbs_string_len(bytes); memcpy(allocate_result(n), bytes.start, n); - status = 1; + status = RBS_WASM_OK; } else { status = set_error_result(parser); } @@ -305,13 +349,18 @@ __attribute__((export_name("rbs_wasm_parse_type_params"))) int rbs_wasm_parse_ty // Shared body for the leading/trailing inline annotation parsers. static int parse_inline_annotation(const char *source, int length, const char *encoding, int encoding_length, int start_pos, int end_pos, const char *variables, int variables_length, bool leading) { - if (!range_is_valid(start_pos, end_pos, length)) { + if (!range_is_valid(start_pos, end_pos)) { allocate_result(0); - return 0; + return RBS_WASM_INVALID_RANGE; } rbs_string_t string = rbs_string_new(source, source + length); - rbs_parser_t *parser = rbs_parser_new(string, resolve_encoding(encoding, encoding_length), start_pos, end_pos); + rbs_parser_t *parser = rbs_parser_new(string, resolve_encoding(encoding, encoding_length), start_pos, clamp_end_pos(end_pos, length)); + if (parser == NULL) { + allocate_result(0); + return RBS_WASM_INVALID_START_POS; + } + declare_variables(parser, variables, variables_length); rbs_ast_ruby_annotations_t *annotation = NULL; @@ -322,7 +371,7 @@ static int parse_inline_annotation(const char *source, int length, const char *e status = set_error_result(parser); } else if (!success || annotation == NULL) { allocate_result(0); - status = 1; + status = RBS_WASM_OK; } else { status = set_serialized_result(parser, (rbs_node_t *) annotation); } @@ -364,11 +413,18 @@ static void w_lex_u32(rbs_allocator_t *allocator, rbs_buffer_t *buffer, uint32_t * * The final token is always pEOF, mirroring RBS::Parser._lex. * - * @return 1 always (lexing does not report parse errors here). + * @return RBS_WASM_OK, or RBS_WASM_INVALID_RANGE for a negative `end_pos` + * (lexing does not report parse errors here). */ __attribute__((export_name("rbs_wasm_lex"))) int rbs_wasm_lex(const char *source, int length, const char *encoding, int encoding_length, int end_pos) { + if (!range_is_valid(0, end_pos)) { + allocate_result(0); + return RBS_WASM_INVALID_RANGE; + } + rbs_allocator_t *allocator = rbs_allocator_init(); - rbs_lexer_t *lexer = rbs_lexer_new(allocator, rbs_string_new(source, source + length), resolve_encoding(encoding, encoding_length), 0, end_pos); + // Byte 0 is always a position the lexer can start on, so this is never NULL. + rbs_lexer_t *lexer = rbs_lexer_new(allocator, rbs_string_new(source, source + length), resolve_encoding(encoding, encoding_length), 0, clamp_end_pos(end_pos, length)); rbs_buffer_t buffer; rbs_buffer_init(allocator, &buffer); @@ -390,7 +446,7 @@ __attribute__((export_name("rbs_wasm_lex"))) int rbs_wasm_lex(const char *source memcpy(allocate_result(n), bytes.start, n); rbs_allocator_free(allocator); - return 1; + return RBS_WASM_OK; } /**