Skip to content

Commit e162b6c

Browse files
authored
Regexp match raises Encoding::CompatibilityError on encoding clashes (#909)
1 parent 65b720e commit e162b6c

1 file changed

Lines changed: 102 additions & 0 deletions

File tree

monoruby/src/builtins/regexp.rs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)