using System.Text.RegularExpressions;
using Jint;
using Mono.Cecil;
using XFESystem.Collections;
namespace XFESystem.JintCleanupPatch.Tests;
public sealed class LiteralRegexReplacementTests
{
[Fact]
public void OnlyTheExactStringReplacementOverloadIsPatched()
{
using var assembly = AssemblyDefinition.ReadAssembly(typeof(Engine).Assembly.Location);
Assert.NotNull(assembly.MainModule.GetType("XFESystem.JintLiteralRegexReplacementV1"));
var method = assembly.MainModule.GetType("Jint.Native.RegExp.RegExpPrototype").Methods.Single(m => m.Name == "Replace");
Assert.Single(method.Body.Instructions, i => i.Operand is MethodReference call && call.DeclaringType.FullName == typeof(LiteralRegexReplacement).FullName);
Assert.Contains(method.Body.Instructions, i => i.Operand is MethodReference call && call.DeclaringType.FullName == typeof(Regex).FullName && call.Name == "Replace" && call.Parameters.Any(p => p.ParameterType.Name == "MatchEvaluator"));
}
[Theory]
[InlineData(@"\.", "listFilterWidget.background", "-", int.MaxValue, RegexOptions.None)]
[InlineData(@"\.", "a.b.c", "", 1, RegexOptions.None)]
[InlineData(@"a\.b", "a.b a.b", "x", 2, RegexOptions.CultureInvariant)]
[InlineData(@"\[x\]", "[x] [X]", "y", int.MaxValue, RegexOptions.None)]
[InlineData(@"\\", @"a\b\c", "/", int.MaxValue, RegexOptions.None)]
[InlineData("literal", "no match", "x", int.MaxValue, RegexOptions.None)]
[InlineData("", "abc", "x", 2, RegexOptions.None)]
[InlineData("a+", "aaab", "x", 1, RegexOptions.None)]
[InlineData(@"\w", "abc", "x", 1, RegexOptions.None)]
[InlineData("ä", "aäb", "x", 1, RegexOptions.None)]
[InlineData("x", "Xx", "y", 1, RegexOptions.IgnoreCase)]
[InlineData("x", "xXx", "y", 1, RegexOptions.RightToLeft)]
[InlineData("a b", "ab a b", "y", 1, RegexOptions.IgnorePatternWhitespace)]
[InlineData(@"\.", "a.b.c", "$&", int.MaxValue, RegexOptions.None)]
[InlineData("a", "aaaa", "x", 0, RegexOptions.None)]
public void BoundedAtomsAndFallbackShapesMatchTheOriginalDotNetEngine(string pattern, string input, string replacement, int count, RegexOptions options)
{
var regex = new Regex(pattern, options, TimeSpan.FromMilliseconds(100));
Assert.Equal(regex.Replace(input, replacement, count), LiteralRegexReplacement.Replace(regex, input, replacement, count));
}
[Fact]
public void SizeBoundsAndErrorsRetainOriginalResults()
{
var regex = new Regex(@"\.", RegexOptions.None, TimeSpan.FromMilliseconds(100));
foreach (var sample in new[] { (new string('a', 131_073) + ".", "-"), ("...", new string('x', 4097)), (new string('.', 300), new string('x', 4096)) })
Assert.Equal(regex.Replace(sample.Item1, sample.Item2), LiteralRegexReplacement.Replace(regex, sample.Item1, sample.Item2, int.MaxValue));
Assert.Throws<ArgumentOutOfRangeException>(() => LiteralRegexReplacement.Replace(regex, "a.b", "-", -2));
}
[Theory]
[InlineData("const r=/\\./g;r.lastIndex=7;const s='listFilterWidget.background'.replace(r,'-');[s,r.lastIndex].join(',')", "listFilterWidget-background,0")]
[InlineData("const r=/\\./;r.lastIndex=7;const s='a.b.c'.replace(r,'-');[s,r.lastIndex].join(',')", "a-b.c,0")]
[InlineData("const r=/\\./g;r.exec=function(){return null};'a.b'.replace(r,'-')", "a.b")]
[InlineData("const r=/\\./g;let reads=0;Object.defineProperty(r,'flags',{get(){reads++;return 'g'}});['a.b.c'.replace(r,'-'),reads].join(',')", "a-b-c,1")]
[InlineData("'a.b'.replace(/\\./g,()=>'$&')", "a$&b")]
[InlineData("'a.b'.replace(/\\./g,'$`')", "aab")]
public void ActualJintKeepsLastIndexExecAccessorsAndReplacementCallbacks(string source, string expected)
{
var engine = new Engine(options => options.Constraints.RegexTimeout = TimeSpan.FromMilliseconds(100));
try { Assert.Equal(expected, engine.Evaluate(source).AsString()); }
finally { ((IWeakCollectionOwner)(object)engine).WeakCollections.Release(); }
}
[Fact]
public void ComplexBacktrackingStillTimesOut()
{
var engine = new Engine(options => options.Constraints.RegexTimeout = TimeSpan.FromMilliseconds(20));
try { Assert.Throws<RegexMatchTimeoutException>(() => engine.Evaluate("('a'.repeat(32)+'!').replace(/(a+)+$/,'x')")); }
finally { ((IWeakCollectionOwner)(object)engine).WeakCollections.Release(); }
}
}
using System.Text.RegularExpressions;
using Jint;
using Mono.Cecil;
using XFESystem.Collections;
namespace XFESystem.JintCleanupPatch.Tests;
public sealed class LiteralRegexReplacementTests
{
[Fact]
public void OnlyTheExactStringReplacementOverloadIsPatched()
{
using var assembly = AssemblyDefinition.ReadAssembly(typeof(Engine).Assembly.Location);
Assert.NotNull(assembly.MainModule.GetType("XFESystem.JintLiteralRegexReplacementV1"));
var method = assembly.MainModule.GetType("Jint.Native.RegExp.RegExpPrototype").Methods.Single(m => m.Name == "Replace");
Assert.Single(method.Body.Instructions, i => i.Operand is MethodReference call && call.DeclaringType.FullName == typeof(LiteralRegexReplacement).FullName);
Assert.Contains(method.Body.Instructions, i => i.Operand is MethodReference call && call.DeclaringType.FullName == typeof(Regex).FullName && call.Name == "Replace" && call.Parameters.Any(p => p.ParameterType.Name == "MatchEvaluator"));
}
[Theory]
[InlineData(@"\.", "listFilterWidget.background", "-", int.MaxValue, RegexOptions.None)]
[InlineData(@"\.", "a.b.c", "", 1, RegexOptions.None)]
[InlineData(@"a\.b", "a.b a.b", "x", 2, RegexOptions.CultureInvariant)]
[InlineData(@"\[x\]", "[x] [X]", "y", int.MaxValue, RegexOptions.None)]
[InlineData(@"\\", @"a\b\c", "/", int.MaxValue, RegexOptions.None)]
[InlineData("literal", "no match", "x", int.MaxValue, RegexOptions.None)]
[InlineData("", "abc", "x", 2, RegexOptions.None)]
[InlineData("a+", "aaab", "x", 1, RegexOptions.None)]
[InlineData(@"\w", "abc", "x", 1, RegexOptions.None)]
[InlineData("ä", "aäb", "x", 1, RegexOptions.None)]
[InlineData("x", "Xx", "y", 1, RegexOptions.IgnoreCase)]
[InlineData("x", "xXx", "y", 1, RegexOptions.RightToLeft)]
[InlineData("a b", "ab a b", "y", 1, RegexOptions.IgnorePatternWhitespace)]
[InlineData(@"\.", "a.b.c", "$&", int.MaxValue, RegexOptions.None)]
[InlineData("a", "aaaa", "x", 0, RegexOptions.None)]
public void BoundedAtomsAndFallbackShapesMatchTheOriginalDotNetEngine(string pattern, string input, string replacement, int count, RegexOptions options)
{
var regex = new Regex(pattern, options, TimeSpan.FromMilliseconds(100));
Assert.Equal(regex.Replace(input, replacement, count), LiteralRegexReplacement.Replace(regex, input, replacement, count));
}
[Fact]
public void SizeBoundsAndErrorsRetainOriginalResults()
{
var regex = new Regex(@"\.", RegexOptions.None, TimeSpan.FromMilliseconds(100));
foreach (var sample in new[] { (new string('a', 131_073) + ".", "-"), ("...", new string('x', 4097)), (new string('.', 300), new string('x', 4096)) })
Assert.Equal(regex.Replace(sample.Item1, sample.Item2), LiteralRegexReplacement.Replace(regex, sample.Item1, sample.Item2, int.MaxValue));
Assert.Throws<ArgumentOutOfRangeException>(() => LiteralRegexReplacement.Replace(regex, "a.b", "-", -2));
}
[Theory]
[InlineData("const r=/\\./g;r.lastIndex=7;const s='listFilterWidget.background'.replace(r,'-');[s,r.lastIndex].join(',')", "listFilterWidget-background,0")]
[InlineData("const r=/\\./;r.lastIndex=7;const s='a.b.c'.replace(r,'-');[s,r.lastIndex].join(',')", "a-b.c,0")]
[InlineData("const r=/\\./g;r.exec=function(){return null};'a.b'.replace(r,'-')", "a.b")]
[InlineData("const r=/\\./g;let reads=0;Object.defineProperty(r,'flags',{get(){reads++;return 'g'}});['a.b.c'.replace(r,'-'),reads].join(',')", "a-b-c,1")]
[InlineData("'a.b'.replace(/\\./g,()=>'$&')", "a$&b")]
[InlineData("'a.b'.replace(/\\./g,'$`')", "aab")]
public void ActualJintKeepsLastIndexExecAccessorsAndReplacementCallbacks(string source, string expected)
{
var engine = new Engine(options => options.Constraints.RegexTimeout = TimeSpan.FromMilliseconds(100));
try { Assert.Equal(expected, engine.Evaluate(source).AsString()); }
finally { ((IWeakCollectionOwner)(object)engine).WeakCollections.Release(); }
}
[Fact]
public void ComplexBacktrackingStillTimesOut()
{
var engine = new Engine(options => options.Constraints.RegexTimeout = TimeSpan.FromMilliseconds(20));
try { Assert.Throws<RegexMatchTimeoutException>(() => engine.Evaluate("('a'.repeat(32)+'!').replace(/(a+)+$/,'x')")); }
finally { ((IWeakCollectionOwner)(object)engine).WeakCollections.Release(); }
}
}