diff --git a/Jint.Repl/Program.cs b/Jint.Repl/Program.cs index bd1fe16e1c..b44fc3c013 100644 --- a/Jint.Repl/Program.cs +++ b/Jint.Repl/Program.cs @@ -221,10 +221,17 @@ JsValue str = result; if (!result.IsPrimitive() && result is not IJsPrimitive) { - str = serializer.Serialize(result, JsValue.Undefined, " "); - if (str == JsValue.Undefined) + if (result is JsRegExp jsRegExp) { - str = result; + str = result.ToString(); + } + else + { + str = serializer.Serialize(result, JsValue.Undefined, " "); + if (str == JsValue.Undefined) + { + str = result; + } } } else if (result.IsString()) diff --git a/Jint.Tests/Runtime/RegExpTests.cs b/Jint.Tests/Runtime/RegExpTests.cs index 7a4b704bbd..892fe5c5a0 100644 --- a/Jint.Tests/Runtime/RegExpTests.cs +++ b/Jint.Tests/Runtime/RegExpTests.cs @@ -77,6 +77,28 @@ public void ToStringWithRealRegExpInstance() Assert.Equal("/test/g", result); } + [Fact] + public void ToStringPreserversOriginalPatternOfLiteral() + { + var engine = new Engine(); + var result = engine.Evaluate("/^x\\/\\\\r\\n\\u2028\\u2029\\0\0|[x/\\\\r\\n\\u2028\\u2029\\0\0]$/"); + + var jsRegExp = Assert.IsType(result); + Assert.Equal("^x\\/\\\\r\\n\\u2028\\u2029\\0\0|[x/\\\\r\\n\\u2028\\u2029\\0\0]$", jsRegExp.Source); + Assert.Equal("/^x\\/\\\\r\\n\\u2028\\u2029\\0\0|[x/\\\\r\\n\\u2028\\u2029\\0\0]$/", jsRegExp.ToString()); + } + + [Fact] + public void ToStringCorrectlyEscapesProblematicCharacters() + { + var engine = new Engine(); + var result = engine.Evaluate(@"new RegExp('^x/\\\r\n\u2028\u2029\\0\0|[x/\\\r\n\u2028\u2029\\0\0]$')"); + + var jsRegExp = Assert.IsType(result); + Assert.Equal("^x\\/\\r\\n\\u2028\\u2029\\0\0|[x/\\r\\n\\u2028\\u2029\\0\0]$", jsRegExp.Source); + Assert.Equal("/^x\\/\\r\\n\\u2028\\u2029\\0\0|[x/\\r\\n\\u2028\\u2029\\0\0]$/", jsRegExp.ToString()); + } + [Fact] public void ShouldNotThrowErrorOnIncompatibleRegex() { diff --git a/Jint/Native/RegExp/RegExpConstructor.cs b/Jint/Native/RegExp/RegExpConstructor.cs index 321da4fdef..d60b672162 100644 --- a/Jint/Native/RegExp/RegExpConstructor.cs +++ b/Jint/Native/RegExp/RegExpConstructor.cs @@ -449,10 +449,13 @@ public override ObjectInstance Construct(JsCallArguments arguments, JsValue newT internal JsRegExp RegExpInitialize(JsRegExp r, JsValue pattern, JsValue flags, bool throwOnLastIndex = false) { + // This method is called when a RegExp object is created at run-time, e.g., + // `new RegExp("abc", "i")`, `RegExp("abc", "i")`, `/x/.compile("abc", "i")`, `"x".match("abc")`, etc. + var p = pattern.IsUndefined() ? "" : TypeConverter.ToString(pattern); if (string.IsNullOrEmpty(p)) { - p = "(?:)"; + p = JsRegExp.regExpForMatchingAllCharacters; } var f = flags.IsUndefined() ? "" : TypeConverter.ToString(flags); @@ -496,8 +499,10 @@ internal JsRegExp RegExpInitialize(JsRegExp r, JsValue pattern, JsValue flags, b r.ParseResult = regExpParseResult; } + // The pattern may contain characters that are not valid in a regexp literal ('/' and new line characters), + // so such cases needs to be escaped. + r.Source = EscapeRegExpSource(p); r.Flags = f; - r.Source = p; if (throwOnLastIndex) { @@ -884,6 +889,67 @@ private static int HexDigitValue(char c) return -1; } + private static string EscapeRegExpSource(string source) + { + StringBuilder? sb = null; + var inClass = false; + var chunkStart = 0; + for (var i = 0; i < source.Length; i++) + { + var ch = source[i]; + switch (ch) + { + case '[': + inClass = true; + break; + case ']' when inClass: + inClass = false; + break; + case '\\': + if (++i >= source.Length) + { + break; + } + ch = source[i]; + // Backslash is omitted when a new line character follows. + if (ch is '\n' or '\r' or '\u2028' or '\u2029') + { + (sb ??= new()).Append(source, chunkStart, (i - 1) - chunkStart); + goto AppendEscapedNewLine; + } + break; + case '/' when !inClass: + (sb ??= new()).Append(source, chunkStart, i - chunkStart); + sb.Append('\\').Append(ch); + chunkStart = i + 1; + break; + case '\n': + case '\r': + case '\u2028': + case '\u2029': + (sb ??= new()).Append(source, chunkStart, i - chunkStart); +AppendEscapedNewLine: + sb.Append(ch switch + { + '\n' => @"\n", + '\r' => @"\r", + '\u2028' => @"\u2028", + _ => @"\u2029" + }); + chunkStart = i + 1; + break; + } + } + + if (sb is not null) + { + sb.Append(source, chunkStart, source.Length - chunkStart); + source = sb.ToString(); + } + + return source; + } + /// /// Attempt to compile the regex pattern using the custom Jint regex engine. /// @@ -955,19 +1021,31 @@ private JsRegExp RegExpAlloc(JsValue newTarget) internal JsRegExp Construct(RegExpParseResult parseResult, string source, string flags) { + // This method is called to create a JsRegExp object from a regexp literal, e.g., `/abc/i`. + var r = RegExpAlloc(this); r.Value = parseResult.Regex ?? DummyRegex; + + // Source is already a valid, properly escaped JS regexp pattern. r.Source = source; r.Flags = flags; + r.ParseResult = parseResult; RegExpInitialize(r); return r; } - public JsRegExp Construct(Regex regExp, string source, string flags) + public JsRegExp Construct(Regex regExp, string source = "?[native regex]", string flags = "") { + // This method is called to create a JsRegExp object from a .NET Regex directly. + var r = RegExpAlloc(this); r.Value = regExp; + + // We shouldn't return the .NET pattern as if it were a JS pattern since that would be + // incorrect and misleading. We could try to convert the .NET pattern to a JS one, + // but that would be a huge (if not impossible) task. So let's use an invalid pattern + // giving a hint about the situation. r.Source = source; r.Flags = flags; diff --git a/Jint/Native/RegExp/RegExpPrototype.cs b/Jint/Native/RegExp/RegExpPrototype.cs index 03c10eb7a4..3b7c1e220b 100644 --- a/Jint/Native/RegExp/RegExpPrototype.cs +++ b/Jint/Native/RegExp/RegExpPrototype.cs @@ -164,11 +164,7 @@ private JsValue Source(JsValue thisObject) return JsRegExp.regExpForMatchingAllCharacters; } - - return r.Source - .Replace("\\/", "/") // ensure forward-slashes - .Replace("/", "\\/") // then escape again - .Replace("\n", "\\n"); + return r.Source; } /// diff --git a/Jint/Runtime/Interop/DefaultObjectConverter.cs b/Jint/Runtime/Interop/DefaultObjectConverter.cs index c06d412f4a..f02eef2400 100644 --- a/Jint/Runtime/Interop/DefaultObjectConverter.cs +++ b/Jint/Runtime/Interop/DefaultObjectConverter.cs @@ -31,7 +31,7 @@ internal static class DefaultObjectConverter { typeof(ulong), (engine, v) => JsNumber.Create((ulong)v) }, { typeof(System.Text.RegularExpressions.Regex), - (engine, v) => engine.Realm.Intrinsics.RegExp.Construct((System.Text.RegularExpressions.Regex)v, ((System.Text.RegularExpressions.Regex)v).ToString(), "") + (engine, v) => engine.Realm.Intrinsics.RegExp.Construct((System.Text.RegularExpressions.Regex)v) } };