diff --git a/lib/parser/lexer-strings.rl b/lib/parser/lexer-strings.rl index 23e206d28..764940c41 100644 --- a/lib/parser/lexer-strings.rl +++ b/lib/parser/lexer-strings.rl @@ -445,7 +445,11 @@ class Parser::LexerStrings end def read_post_meta_or_ctrl_char(p) - @escape = source_buffer.slice(p - 1, 1).chr + # \c and \M- escapes operate on the raw byte value of their target, not on + # a Unicode codepoint, so read it as a raw byte here: the source buffer's + # own encoding (eg. UTF-8) would otherwise make `.ord` below raise on a + # byte that isn't valid on its own in that encoding (eg. \c\xFF). + @escape = source_buffer.slice(p - 1, 1).chr.b if @version >= 27 && ((0..8).include?(@escape.ord) || (14..31).include?(@escape.ord)) diagnostic :fatal, :invalid_escape @@ -477,11 +481,16 @@ class Parser::LexerStrings end def slash_c_char - @escape = encode_escape(@escape[0].ord & 0x9f) + # @escape may already carry the source encoding (eg. via encode_escape, + # which force_encodes a raw byte without validating it), so read its + # value as a raw byte here rather than as a character in that encoding: + # a \c/\M control/meta escape operates on bytes, and .ord on a string + # that isn't valid in its tagged encoding raises ArgumentError. + @escape = encode_escape(@escape.b[0].ord & 0x9f) end def slash_m_char - @escape = encode_escape(@escape[0].ord | 0x80) + @escape = encode_escape(@escape.b[0].ord | 0x80) end def emit_character_constant diff --git a/test/test_parser.rb b/test/test_parser.rb index e9d95ef8b..9c898471d 100644 --- a/test/test_parser.rb +++ b/test/test_parser.rb @@ -11070,6 +11070,56 @@ def test_control_meta_escape_chars_in_regexp__since_31 SINCE_3_1) end + # Same escapes as test_control_meta_escape_chars_in_regexp__since_31, but + # from a UTF-8 source (the common case, and what these literals actually + # have by default unless the source is forced to ascii-8bit as above). + # \x9F on its own isn't valid UTF-8, so this can't successfully parse to a + # :str node the way the ascii-8bit case does; it must instead raise a + # graceful diagnostic rather than crash with an unhandled ArgumentError. + def test_control_meta_escape_chars_in_regexp_from_utf8_source + assert_diagnoses( + [:error, :invalid_encoding], + %q{/\c\xFF/}, + %q{}, + SINCE_3_1) + + assert_diagnoses( + [:error, :invalid_encoding], + %q{/\c\M-\xFF/}, + %q{}, + SINCE_3_1) + + assert_diagnoses( + [:error, :invalid_encoding], + %q{/\C-\xFF/}, + %q{}, + SINCE_3_1) + + assert_diagnoses( + [:error, :invalid_encoding], + %q{/\C-\M-\xFF/}, + %q{}, + SINCE_3_1) + + assert_diagnoses( + [:error, :invalid_encoding], + %q{/\M-\xFF/}, + %q{}, + SINCE_3_1) + + assert_diagnoses( + [:error, :invalid_encoding], + %q{/\M-\C-\xFF/}, + %q{}, + SINCE_3_1) + + assert_diagnoses( + [:error, :invalid_encoding], + %q{/\M-\c\xFF/}, + %q{}, + SINCE_3_1) + end + def test_forward_arg_with_open_args assert_diagnoses_many( [