Skip to content

Commit

Permalink
Ensure script preparation exposes only Jint's exception type (#1927)
Browse files Browse the repository at this point in the history
  • Loading branch information
lahma committed Jul 24, 2024
1 parent 91755e9 commit 3d5c872
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 43 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@

namespace Jint.Tests.Runtime;

public partial class EngineTests
public class ScriptModulePreparationTests
{
[Fact]
public void ScriptPreparationAcceptsReturnOutsideOfFunctions()
{
var preparedScript = Engine.PrepareScript("return 1;");
Assert.IsType<ReturnStatement>(preparedScript.Program.Body[0]);
preparedScript.Program.Body[0].Should().BeOfType<ReturnStatement>();
}

[Fact]
Expand All @@ -21,10 +21,10 @@ public void CanPreCompileRegex()
var script = Engine.PrepareScript("var x = /[cgt]/ig; var y = /[cgt]/ig; 'g'.match(x).length;");
var declaration = Assert.IsType<VariableDeclaration>(script.Program.Body[0]);
var init = Assert.IsType<RegExpLiteral>(declaration.Declarations[0].Init);
Assert.Equal("[cgt]", init.Value.ToString());
Assert.Equal(RegexOptions.Compiled, init.Value.Options & RegexOptions.Compiled);

Assert.Equal(1, _engine.Evaluate(script));
init.Value.ToString().Should().Be("[cgt]");
(init.Value.Options & RegexOptions.Compiled).Should().Be(RegexOptions.Compiled);
new Engine().Evaluate(script).AsNumber().Should().Be(1);
}

[Fact]
Expand All @@ -33,9 +33,9 @@ public void ScriptPreparationFoldsConstants()
var preparedScript = Engine.PrepareScript("return 1 + 2;");
var returnStatement = Assert.IsType<ReturnStatement>(preparedScript.Program.Body[0]);
var constant = Assert.IsType<JintConstantExpression>(returnStatement.Argument?.UserData);
Assert.Equal(3, constant.GetValue(null!));

Assert.Equal(3, _engine.Evaluate(preparedScript));
constant.GetValue(null!).AsNumber().Should().Be(3);
new Engine().Evaluate(preparedScript).AsNumber().Should().Be(3);
}

[Fact]
Expand All @@ -46,8 +46,8 @@ public void ScriptPreparationOptimizesNegatingUnaryExpression()
var unaryExpression = Assert.IsType<NonUpdateUnaryExpression>(expression.Expression);
var constant = Assert.IsType<JintConstantExpression>(unaryExpression.UserData);

Assert.Equal(-1, constant.GetValue(null!));
Assert.Equal(-1, _engine.Evaluate(preparedScript));
constant.GetValue(null!).AsNumber().Should().Be(-1);
new Engine().Evaluate(preparedScript).AsNumber().Should().Be(-1);
}

[Fact]
Expand All @@ -58,20 +58,37 @@ public void ScriptPreparationOptimizesConstantReturn()
var returnStatement = Assert.IsType<ConstantStatement>(statement.UserData);

var builtStatement = JintStatement.Build(statement);
Assert.Same(returnStatement, builtStatement);
returnStatement.Should().BeSameAs(builtStatement);

var result = builtStatement.Execute(new EvaluationContext(_engine)).Value;
Assert.Equal(JsBoolean.False, result);
var result = builtStatement.Execute(new EvaluationContext( new Engine())).Value;
result.Should().Be(JsBoolean.False);
}

[Fact]
public void CompiledRegexShouldProduceSameResultAsNonCompiled()
{
const string Script = """JSON.stringify(/(.*?)a(?!(a+)b\2c)\2(.*)/.exec("baaabaac"))""";

var nonCompiled = _engine.Evaluate(Script);
var compiled = _engine.Evaluate(Engine.PrepareScript(Script));
var engine = new Engine();
var nonCompiledResult = engine.Evaluate(Script);
var compiledResult = engine.Evaluate(Engine.PrepareScript(Script));

Assert.Equal(nonCompiled, compiled);
nonCompiledResult.Should().Be(compiledResult);
}

[Fact]
public void PrepareScriptShouldNotLeakAcornimaException()
{
var ex = Assert.Throws<ScriptPreparationException>(() => Engine.PrepareScript("class A { } A().#nonexistent = 1;"));
ex.Message.Should().Be("Could not prepare script: Private field '#nonexistent' must be declared in an enclosing class (1:17)");
ex.InnerException.Should().BeOfType<SyntaxErrorException>();
}

[Fact]
public void PrepareModuleShouldNotLeakAcornimaException()
{
var ex = Assert.Throws<ScriptPreparationException>(() => Engine.PrepareModule("class A { } A().#nonexistent = 1;"));
ex.Message.Should().Be("Could not prepare script: Private field '#nonexistent' must be declared in an enclosing class (1:17)");
ex.InnerException.Should().BeOfType<SyntaxErrorException>();
}
}
32 changes: 24 additions & 8 deletions Jint/Engine.Ast.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using Jint.Native;
using Jint.Runtime;
using Jint.Runtime.Interpreter;
Expand All @@ -20,10 +19,20 @@ public partial class Engine
public static Prepared<Script> PrepareScript(string code, string? source = null, bool strict = false, ScriptPreparationOptions? options = null)
{
options ??= ScriptPreparationOptions.Default;

var astAnalyzer = new AstAnalyzer(options);
var parserOptions = options.GetParserOptions();
var preparedScript = new Parser(parserOptions with { OnNode = astAnalyzer.NodeVisitor }).ParseScript(code, source, strict);
return new Prepared<Script>(preparedScript, parserOptions);
var parser = new Parser(parserOptions with { OnNode = astAnalyzer.NodeVisitor });

try
{
var preparedScript = parser.ParseScript(code, source, strict);
return new Prepared<Script>(preparedScript, parserOptions);
}
catch (Exception e)
{
throw new ScriptPreparationException("Could not prepare script: " + e.Message, e);
}
}

/// <summary>
Expand All @@ -35,19 +44,26 @@ public static Prepared<Script> PrepareScript(string code, string? source = null,
public static Prepared<Module> PrepareModule(string code, string? source = null, ModulePreparationOptions? options = null)
{
options ??= ModulePreparationOptions.Default;

var astAnalyzer = new AstAnalyzer(options);
var parserOptions = options.GetParserOptions();
var preparedModule = new Parser(parserOptions with { OnNode = astAnalyzer.NodeVisitor }).ParseModule(code, source);
return new Prepared<Module>(preparedModule, parserOptions);
var parser = new Parser(parserOptions with { OnNode = astAnalyzer.NodeVisitor });

try
{
var preparedModule = parser.ParseModule(code, source);
return new Prepared<Module>(preparedModule, parserOptions);
}
catch (Exception e)
{
throw new ScriptPreparationException("Could not prepare script: " + e.Message, e);
}
}

private sealed class AstAnalyzer
{
private static readonly bool _canCompileNegativeLookaroundAssertions = typeof(Regex).Assembly.GetName().Version?.Major is not (null or 7 or 8);

private readonly IPreparationOptions<IParsingOptions> _preparationOptions;
private readonly Dictionary<string, Environment.BindingName> _bindingNames = new(StringComparer.Ordinal);
private readonly Dictionary<string, Regex> _regexes = new(StringComparer.Ordinal);

public AstAnalyzer(IPreparationOptions<IParsingOptions> preparationOptions)
{
Expand Down
15 changes: 15 additions & 0 deletions Jint/JintException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace Jint;

/// <summary>
/// Base class for exceptions thrown by Jint.
/// </summary>
public abstract class JintException : Exception
{
internal JintException(string? message) : base(message)
{
}

internal JintException(string? message, Exception? innerException) : base(message, innerException)
{
}
}
20 changes: 0 additions & 20 deletions Jint/Runtime/JintException.cs

This file was deleted.

8 changes: 8 additions & 0 deletions Jint/ScriptPreparationException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Jint;

public sealed class ScriptPreparationException : JintException
{
public ScriptPreparationException(string? message, Exception? innerException) : base(message, innerException)
{
}
}

0 comments on commit 3d5c872

Please sign in to comment.