diff --git a/core/engine/src/builtins/regexp/mod.rs b/core/engine/src/builtins/regexp/mod.rs index 037eed6eb19..2e6a4eb87e1 100644 --- a/core/engine/src/builtins/regexp/mod.rs +++ b/core/engine/src/builtins/regexp/mod.rs @@ -798,7 +798,17 @@ impl RegExp { } else { let mut s = Vec::with_capacity(src.len()); let mut buf = [0; 2]; + let mut escaped = false; for c in src.code_points() { + // Drop the pattern's own `\` so the code point is not escaped twice. + if escaped + && matches!( + c, + CodePoint::Unicode('/' | '\n' | '\r' | '\u{2028}' | '\u{2029}') + ) + { + s.pop(); + } match c { CodePoint::Unicode('/') => s.extend_from_slice(utf16!(r"\/")), CodePoint::Unicode('\n') => s.extend_from_slice(utf16!(r"\n")), @@ -808,6 +818,7 @@ impl RegExp { CodePoint::Unicode(c) => s.extend_from_slice(c.encode_utf16(&mut buf)), CodePoint::UnpairedSurrogate(surr) => s.push(surr), } + escaped = !escaped && c == CodePoint::Unicode('\\'); } JsValue::new(js_string!(&s[..])) diff --git a/core/engine/src/builtins/regexp/tests.rs b/core/engine/src/builtins/regexp/tests.rs index 0897b682720..0a537884b26 100644 --- a/core/engine/src/builtins/regexp/tests.rs +++ b/core/engine/src/builtins/regexp/tests.rs @@ -175,6 +175,55 @@ fn to_string() { TestAction::assert_eq(r"/,\;/.toString()", js_string!(r"/,\;/")), ]); } +#[test] +fn source_escapes_slash() { + run_test_actions([ + TestAction::assert_eq(r#"new RegExp("/").source"#, js_string!(r"\/")), + TestAction::assert_eq(r"/a\/b/.source", js_string!(r"a\/b")), + TestAction::assert_eq(r#"new RegExp("\\/").source"#, js_string!(r"\/")), + // The backslash is escaped, the `/` is not. + TestAction::assert_eq(r#"new RegExp("\\\\/").source"#, js_string!(r"\\\/")), + ]); +} + +#[test] +fn source_escapes_line_terminators() { + run_test_actions([ + TestAction::assert_eq(r#"new RegExp("\n").source"#, js_string!(r"\n")), + TestAction::assert_eq(r#"new RegExp("\\\n").source"#, js_string!(r"\n")), + TestAction::assert_eq(r#"new RegExp("\\\r").source"#, js_string!(r"\r")), + TestAction::assert_eq(r#"new RegExp("\\\u2028").source"#, js_string!(r"\u2028")), + TestAction::assert_eq(r#"new RegExp("\\\u2029").source"#, js_string!(r"\u2029")), + // The backslash is escaped, the line terminator is not. + TestAction::assert_eq(r#"new RegExp("\\\\\n").source"#, js_string!(r"\\\n")), + ]); +} + +#[test] +fn source_round_trips() { + run_test_actions([ + TestAction::run(indoc! {r#" + function roundTrips(re, inputs) { + var copies = [ + new RegExp(re.source, re.flags), + eval("/" + re.source + "/" + re.flags), + ]; + return copies.every(function (copy) { + return inputs.every(function (input) { + return copy.test(input) === re.test(input); + }); + }); + } + "#}), + TestAction::assert(r#"roundTrips(/a\/b/, ["a/b", "a\\/b"])"#), + TestAction::assert(r#"roundTrips(new RegExp("/"), ["/", "\\/"])"#), + TestAction::assert(r#"roundTrips(new RegExp("\\\\/"), ["/", "\\/", "\\"])"#), + TestAction::assert(r#"roundTrips(new RegExp("[/]"),["/", "\\"])"#), + TestAction::assert(r#"roundTrips(new RegExp("\\\n"), ["\n", "\\n"])"#), + TestAction::assert(r#"roundTrips(new RegExp("\\\u2028"), ["\u2028", "\\u2028"])"#), + ]); +} + #[test] fn search() { const ERROR: &str = "RegExp.prototype[Symbol.search] method called on incompatible value";