Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# 1.1.3
Comment thread
FlorianRappl marked this conversation as resolved.
Resolved

Released on Friday, October 30 2026.

- Fixed prepared regular-expression literals ignoring the timeout configured during engine creation

# 1.1.2

Released on Sunday, October 25 2026.
Expand Down
46 changes: 46 additions & 0 deletions src/AngleSharp.Js.Tests/EngineConfigurationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ namespace AngleSharp.Js.Tests
using AngleSharp.Js.Tests.Mocks;
using AngleSharp.Scripting;
using Jint;
using Jint.Native;
using Jint.Runtime;
using NUnit.Framework;
using System;
Expand All @@ -14,6 +15,51 @@ namespace AngleSharp.Js.Tests

public class EngineConfigurationTests
{
[TestCase(false)]
[TestCase(true)]
public async Task RegexTimeoutAppliesToInlineAndExplicitScripts(Boolean inline)
{
var timeout = TimeSpan.FromMilliseconds(37);
var config = Configuration.Default
.With(new EngineCreator((window, options) => new Engine(options.RegexTimeoutInterval(timeout))))
.WithJs()
.WithEventLoop();

using (var context = BrowsingContext.New(config))
{
var document = await context.OpenAsync(request => request.Content(inline
? "<script>var configuredRegex = /inlineEngineTimeout/;</script>"
: "")).ConfigureAwait(false);
var expression = (JsRegExp)document.ExecuteScript(inline ? "configuredRegex" : "/explicitEngineTimeout/");

Assert.AreEqual(timeout, expression.Value.MatchTimeout);
}
}

[TestCase(23, 71)]
[TestCase(71, 23)]
public async Task CachedScriptsKeepEachEngineRegexTimeout(Int32 firstMilliseconds, Int32 secondMilliseconds)
{
var source = "/cachedEngineTimeout" + firstMilliseconds + "/";

foreach (var milliseconds in new[] { firstMilliseconds, secondMilliseconds, firstMilliseconds })
{
var timeout = TimeSpan.FromMilliseconds(milliseconds);
var config = Configuration.Default
.With(new EngineCreator((window, options) => new Engine(options.RegexTimeoutInterval(timeout))))
.WithJs()
.WithEventLoop();

using (var context = BrowsingContext.New(config))
{
var document = await context.OpenNewAsync().ConfigureAwait(false);
var expression = (JsRegExp)document.ExecuteScript(source);

Assert.AreEqual(timeout, expression.Value.MatchTimeout);
}
}
}

[Test]
public async Task ConstraintsAreInstalledBeforeInlineScriptsRun()
{
Expand Down
40 changes: 32 additions & 8 deletions src/AngleSharp.Js/Cache/ScriptCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ namespace AngleSharp.Js.Cache
/// <summary>
/// Caches the parsed and analyzed form of scripts. A prepared script is
/// documented by Jint as reusable and thread-safe, so the same one can serve
/// every document that runs the same source - a page loading a library ends up
/// parsing it once instead of once per document.
/// every document that runs the same source with the same regex timeout - a
/// page loading a library ends up parsing it once instead of once per document.
/// </summary>
static class ScriptCache
{
Expand All @@ -19,24 +19,29 @@ static class ScriptCache
// seen first: libraries are referenced early and by many documents.
private const Int32 Capacity = 32;

private static readonly ConcurrentDictionary<String, Prepared<Script>> _scripts =
new ConcurrentDictionary<String, Prepared<Script>>(StringComparer.Ordinal);
private static readonly ConcurrentDictionary<ScriptKey, Prepared<Script>> _scripts =
new ConcurrentDictionary<ScriptKey, Prepared<Script>>();

/// <summary>
/// Gets the prepared form of the given source. An invalid result means the
/// source could not be prepared and should be handed to the engine as text,
/// so that the engine reports the syntax error itself.
/// </summary>
public static Prepared<Script> GetOrCreate(String source)
public static Prepared<Script> GetOrCreate(String source, TimeSpan regexTimeout)
{
if (_scripts.TryGetValue(source, out var prepared))
var key = new ScriptKey(source, regexTimeout);

if (_scripts.TryGetValue(key, out var prepared))
{
return prepared;
}

try
{
prepared = Engine.PrepareScript(source);
prepared = Engine.PrepareScript(source, options: new ScriptPreparationOptions
{
ParsingOptions = new ScriptParsingOptions { RegexTimeout = regexTimeout },
});
}
catch (ScriptPreparationException)
{
Expand All @@ -45,10 +50,29 @@ public static Prepared<Script> GetOrCreate(String source)

if (_scripts.Count < Capacity)
{
_scripts.TryAdd(source, prepared);
_scripts.TryAdd(key, prepared);
}

return prepared;
}

private readonly struct ScriptKey : IEquatable<ScriptKey>
{
private readonly String _source;
private readonly TimeSpan _regexTimeout;

public ScriptKey(String source, TimeSpan regexTimeout)
{
_source = source ?? throw new ArgumentNullException(nameof(source));
_regexTimeout = regexTimeout;
}

public Boolean Equals(ScriptKey other) =>
String.Equals(_source, other._source, StringComparison.Ordinal) && _regexTimeout == other._regexTimeout;

public override Boolean Equals(Object obj) => obj is ScriptKey other && Equals(other);

public override Int32 GetHashCode() => (_source.GetHashCode() * 397) ^ _regexTimeout.GetHashCode();
}
}
}
4 changes: 3 additions & 1 deletion src/AngleSharp.Js/EngineInstance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ sealed class EngineInstance
private const Int32 StackGuardDisabled = -1;

private readonly Engine _engine;
private readonly TimeSpan _regexTimeout;
private readonly PrototypeCache _prototypes;
private readonly ReferenceCache _references;
private readonly ConditionalWeakTable<Object, SameObjectCache> _sameObjects;
Expand All @@ -49,6 +50,7 @@ public EngineInstance(IWindow window, IDictionary<String, Object> assignments, I
var creator = window.Document.Context.GetService<EngineCreator>();
_engine = (creator is null ? new Engine(engineOptions) : creator.Invoke(window, engineOptions))
?? throw new InvalidOperationException("The engine creator must return a Jint engine.");
_regexTimeout = engineOptions.Constraints.RegexTimeout;
_libs = new LibrarySet(libs);
_prototypes = new PrototypeCache(_engine, _libs);
_references = new ReferenceCache();
Expand Down Expand Up @@ -159,7 +161,7 @@ public JsValue RunScript(String source, String type, String sourceUrl)
{
if (MimeTypeNames.IsJavaScript(type))
{
var prepared = ScriptCache.GetOrCreate(source);
var prepared = ScriptCache.GetOrCreate(source, _regexTimeout);
// An invalid result means the source did not parse; hand it to the
// engine as text so the syntax error is reported as usual.
return prepared.IsValid ? _engine.Evaluate(prepared) : _engine.Evaluate(source);
Expand Down
Loading