@@ -839,6 +839,7 @@ fn teq(vm: &mut Executor, globals: &mut Globals, lfp: Lfp, _: BytecodePtr) -> Re
839839 } else {
840840 return Ok ( Value :: bool ( false ) ) ;
841841 } ;
842+ check_subject_match_encoding ( & globals. store , & regex, subject) ?;
842843 // Stash a UTF-8-valid String subject so the MatchData snapshot is
843844 // zero-copy and carries the subject's encoding into $&/$1..$N;
844845 // non-UTF-8 subjects fall back to the owned lossy copy as before.
@@ -860,6 +861,71 @@ fn teq(vm: &mut Executor, globals: &mut Globals, lfp: Lfp, _: BytecodePtr) -> Re
860861 Ok ( res)
861862}
862863
864+ /// CRuby's `rb_reg_prepare_enc`: verify that `regex` can be matched
865+ /// against a subject of encoding `str_enc` (with `str_ascii_only`
866+ /// telling whether its content is entirely 7-bit), raising
867+ /// `Encoding::CompatibilityError` when it cannot. No-op for
868+ /// Symbol subjects and for compatible String/Regexp encoding pairs.
869+ ///
870+ /// The rule:
871+ /// - An **ASCII-incompatible** subject (UTF-16/32) is matchable only
872+ /// by a regexp of the *same* encoding.
873+ /// - A **fixed-encoding** regexp on an ASCII-compatible subject must
874+ /// share the subject's encoding, unless the regexp's own encoding is
875+ /// ASCII-compatible *and* the subject is entirely 7-bit.
876+ /// - Otherwise (a non-fixed, ASCII-compatible regexp on an
877+ /// ASCII-compatible subject) any content is fine.
878+ fn check_match_encoding (
879+ store : & Store ,
880+ regex : & RegexpInner ,
881+ str_enc : crate :: value:: Encoding ,
882+ str_ascii_only : bool ,
883+ ) -> Result < ( ) > {
884+ let reg_enc = regex. declared_encoding ( ) ;
885+ if !str_enc. is_ascii_compatible ( ) {
886+ if reg_enc != str_enc {
887+ return Err ( regexp_encoding_mismatch ( store, reg_enc, str_enc) ) ;
888+ }
889+ } else if regex. fixed_encoding ( )
890+ && reg_enc != str_enc
891+ && ( !reg_enc. is_ascii_compatible ( ) || !str_ascii_only)
892+ {
893+ return Err ( regexp_encoding_mismatch ( store, reg_enc, str_enc) ) ;
894+ }
895+ Ok ( ( ) )
896+ }
897+
898+ /// `Encoding::CompatibilityError` for a regexp/subject encoding clash,
899+ /// worded as CRuby's `reg_enc_error`.
900+ fn regexp_encoding_mismatch (
901+ store : & Store ,
902+ reg_enc : crate :: value:: Encoding ,
903+ str_enc : crate :: value:: Encoding ,
904+ ) -> MonorubyErr {
905+ MonorubyErr :: encoding_compatibility_error_with_store (
906+ store,
907+ format ! (
908+ "incompatible encoding regexp match ({} regexp with {} string)" ,
909+ reg_enc. name( ) ,
910+ str_enc. name( )
911+ ) ,
912+ )
913+ }
914+
915+ /// Run [`check_match_encoding`] for a subject `Value` — only Strings
916+ /// carry an encoding that can clash; Symbols are always compatible.
917+ fn check_subject_match_encoding (
918+ store : & Store ,
919+ regex : & RegexpInner ,
920+ subject : Value ,
921+ ) -> Result < ( ) > {
922+ if subject. is_rstring ( ) . is_some ( ) {
923+ let inner = subject. as_rstring_inner ( ) ;
924+ check_match_encoding ( store, regex, inner. encoding ( ) , inner. is_ascii_only ( ) ) ?;
925+ }
926+ Ok ( ( ) )
927+ }
928+
863929///
864930/// ### Regexp#=~
865931/// - self =~ string -> Integer | nil
@@ -886,6 +952,7 @@ fn regexp_match(
886952 // encoding to $&/$`/$'/$1..$N) when it is a UTF-8-valid String;
887953 // Symbols and non-UTF-8 subjects fall back to the owned conversion.
888954 let arg0 = lfp. arg ( 0 ) ;
955+ check_subject_match_encoding ( & globals. store , & regex, arg0) ?;
889956 let given_owned;
890957 let given: & str = match arg0. is_rstring ( ) {
891958 Some ( rs) if std:: str:: from_utf8 ( rs. as_bytes ( ) ) . is_ok ( ) => {
@@ -980,6 +1047,7 @@ fn match_(vm: &mut Executor, globals: &mut Globals, lfp: Lfp, _: BytecodePtr) ->
9801047 if arg0. is_nil ( ) {
9811048 return Ok ( Value :: bool ( false ) ) ;
9821049 }
1050+ check_subject_match_encoding ( & globals. store , & regex, arg0) ?;
9831051 let given = arg0. expect_symbol_or_string ( globals) ?. to_string ( ) ;
9841052 let char_pos = if let Some ( pos) = lfp. try_arg ( 1 ) {
9851053 match conv_index ( pos. coerce_to_int_i64 ( vm, globals) ?, given. chars ( ) . count ( ) ) {
@@ -1029,6 +1097,7 @@ fn rmatch(vm: &mut Executor, globals: &mut Globals, lfp: Lfp, _: BytecodePtr) ->
10291097 ) ) ) ;
10301098 }
10311099 }
1100+ check_subject_match_encoding ( & globals. store , & regex, arg0) ?;
10321101 // Borrow the subject's bytes directly (and stash the Value for
10331102 // the zero-copy MatchData snapshot) when it is a UTF-8 String;
10341103 // Symbols and non-UTF-8 subjects fall back to the owned
@@ -2186,6 +2255,39 @@ mod tests {
21862255 run_test ( r#"/a/.match("xay".b).nil?"# ) ;
21872256 }
21882257
2258+ #[ test]
2259+ fn regexp_match_encoding_compatibility ( ) {
2260+ // CRuby `rb_reg_prepare_enc`: a regexp/subject encoding clash
2261+ // raises Encoding::CompatibilityError across `match` / `match?` /
2262+ // `=~` / `===`.
2263+ // (1) ASCII-incompatible subject (UTF-16LE) vs an ASCII regexp.
2264+ run_test (
2265+ r#"(/\A[[:space:]]*\z/.match(" ".encode("UTF-16LE")); nil) rescue $!.class.name"# ,
2266+ ) ;
2267+ run_test (
2268+ r#"(/\A[[:space:]]*\z/.match?(" ".encode("UTF-16LE")); nil) rescue $!.class.name"# ,
2269+ ) ;
2270+ run_test ( r#"(/\A[[:space:]]*\z/ =~ " ".encode("UTF-16LE"); nil) rescue $!.class.name"# ) ;
2271+ run_test ( r#"(/x/ === "y".encode("UTF-16LE"); nil) rescue $!.class.name"# ) ;
2272+ // (2) A fixed-encoding regexp whose encoding differs from the
2273+ // (ASCII-compatible) subject's.
2274+ run_test (
2275+ r#"(Regexp.new("".dup.force_encoding("UTF-16LE"), Regexp::FIXEDENCODING) =~ " ".encode("UTF-8"); nil) rescue $!.class.name"# ,
2276+ ) ;
2277+ // (3) A fixed US-ASCII regexp vs a UTF-8 subject with non-ASCII
2278+ // content.
2279+ run_test (
2280+ r#"(Regexp.new("".dup.force_encoding("US-ASCII"), Regexp::FIXEDENCODING) =~ "\303\251".dup.force_encoding("UTF-8"); nil) rescue $!.class.name"# ,
2281+ ) ;
2282+ // Compatible pairs (ASCII subject, or same encoding) still match.
2283+ run_test ( r#"[ /abc/ =~ "xabc", /\d+/.match("a12b")[0], ("héllo" =~ /é/) ]"# ) ;
2284+ // A subject broken in its own encoding is still ArgumentError,
2285+ // not CompatibilityError.
2286+ run_test (
2287+ r#"("\x80".dup.force_encoding("UTF-8") =~ /./; nil) rescue [$!.class.name, $!.message]"# ,
2288+ ) ;
2289+ }
2290+
21892291 #[ test]
21902292 fn regexp_encoding_binary_noencoding ( ) {
21912293 // `/.../n` with a high `\xHH` escape is BINARY; pure-ASCII /n is
0 commit comments