using Jint;
using Jint.Native;
using Jint.Native.Object;
using Jint.Runtime.Descriptors;
using Jint.Runtime.Interop;
using Jint.Runtime;
namespace XFESystem.Browser;
/// <summary>
/// A real JavaScript interpreter with explicit callbacks. No .NET objects,
/// reflection, files or sockets are exposed to script code. Timers run only when explicitly pumped.
/// These cooperative limits are not a process-level security boundary.
/// </summary>
public sealed class JavaScriptRuntime
{
private readonly object gate = new();
private readonly BrowserLimits limits;
private readonly BoundedText output;
private readonly Engine engine;
private readonly BrowserEventLoop eventLoop;
private readonly PreparedScriptCache packagedScripts;
internal long PackagedScriptCacheHits { get; private set; }
internal long PackagedScriptPreparations { get; private set; }
public JavaScriptRuntime(BrowserLimits? limits = null) : this(limits, PreparedScriptCache.Shared) { }
internal JavaScriptRuntime(BrowserLimits? limits, PreparedScriptCache packagedScripts)
{
this.packagedScripts = packagedScripts;
this.limits = limits ?? new BrowserLimits();
this.limits.Validate();
output = new(this.limits.MaximumOutputCharacters);
engine = new Engine(options =>
{
options.MaxStatements(this.limits.MaximumStatements);
options.LimitMemory(this.limits.MaximumScriptMemoryBytes);
options.TimeoutInterval(this.limits.ScriptTimeout);
options.LimitRecursion(this.limits.MaximumRecursionDepth);
options.DisableStringCompilation();
options.Interop.Enabled = false;
options.Interop.AllowGetType = false;
options.Interop.AllowSystemReflection = false;
options.Interop.AllowWrite = false;
options.AgentCanSuspend = false;
options.Constraints.RegexTimeout = TimeSpan.FromMilliseconds(100);
options.Constraints.MaxAtomicsPauseIterations = 100;
options.Constraints.PromiseTimeout = this.limits.ScriptTimeout;
options.Constraints.MaxArraySize = (uint)Math.Min(this.limits.MaximumOutputCharacters, 131_072);
});
var console = new BrowserHostObject(engine);
foreach (var name in new[] { "log", "info", "warn", "error", "debug" })
{
var callback = new ClrFunction(engine, name, (_, arguments) =>
{
for (var index = 0; index < arguments.Length; index++)
{
if (index != 0) output.Append(" ");
output.Append(PrimitiveText(arguments[index]));
}
output.Append("\n");
return JsValue.Undefined;
});
console.DefineOwnProperty(name, new PropertyDescriptor(callback, false, true, false));
}
engine.SetValue("console", console);
eventLoop = new BrowserEventLoop(engine, this.limits);
}
internal Engine Engine => engine;
/// <summary>Trusted host data readback, kept separate from terminal-safe display output.</summary>
internal string? ReadString(string expression)
{
Monitor.Enter(gate);
string? text;
try
{
var value = engine.Evaluate(expression);
if (value.IsNull() || value.IsUndefined()) text = null;
else
{
if (!value.IsString()) throw new IOException("The editor returned a non-string value.");
text = value.AsString();
if (text.Length > limits.MaximumOutputCharacters) throw new IOException("The editor result exceeds its readback limit.");
}
}
catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException)
{
Monitor.Exit(gate);
throw new IOException("Editor data readback failed: " + BoundedText.Safe(error.Message, 512), error);
}
Monitor.Exit(gate);
return text;
}
public long MonotonicMilliseconds => eventLoop.Now;
public int PendingTaskCount => eventLoop.PendingCount;
/// <summary>Collect console output from DOM events dispatched outside Evaluate/Pump.</summary>
public string DrainConsoleOutput()
{
Monitor.Enter(gate);
var result = BoundedText.Safe(output.ToString().TrimEnd('\n'), limits.MaximumOutputCharacters);
output.Clear();
Monitor.Exit(gate);
return result;
}
public JavaScriptResult Pump(long monotonicMs, int maximumCallbacks = 64)
{
Monitor.Enter(gate);
output.Clear();
JavaScriptResult result;
try
{
var count = eventLoop.Pump(monotonicMs, maximumCallbacks);
result = new(true, count.ToString(System.Globalization.CultureInfo.InvariantCulture),
BoundedText.Safe(output.ToString().TrimEnd('\n'), limits.MaximumOutputCharacters), string.Empty);
}
catch (Exception exception) when (exception is not OutOfMemoryException and not StackOverflowException)
{
result = new(false, string.Empty, BoundedText.Safe(output.ToString().TrimEnd('\n'), limits.MaximumOutputCharacters),
BoundedText.Safe(exception.Message, 1_024)) { BudgetExceeded = IsBudgetException(exception) };
}
Monitor.Exit(gate);
return result;
}
public JavaScriptResult Evaluate(string source) => EvaluateCore(source, null);
/// <summary>
/// Called only after the resource loader confirms immutable packaged provenance.
/// Only parsed syntax is reusable: the source still executes in this fresh
/// engine, with its own globals and execution constraints, on every call.
/// </summary>
internal JavaScriptResult EvaluatePackaged(string source, string resourceKey) => EvaluateCore(source, resourceKey);
private JavaScriptResult EvaluateCore(string source, string? resourceKey)
{
ArgumentNullException.ThrowIfNull(source);
if (source.Length > limits.MaximumScriptCharacters)
return new(false, string.Empty, string.Empty, "The script exceeds its character limit.") { BudgetExceeded = true };
Monitor.Enter(gate);
output.Clear();
JavaScriptResult evaluation;
try
{
JsValue result;
if (resourceKey is not null && source.Length >= PreparedScriptCache.MinimumSourceCharacters)
{
var prepared = packagedScripts.GetOrPrepare(source, resourceKey, out bool reused);
if (reused) PackagedScriptCacheHits++; else PackagedScriptPreparations++;
result = engine.Evaluate(prepared);
}
else result = engine.Evaluate(source);
var value = PrimitiveText(result);
if (value.Length > limits.MaximumOutputCharacters - output.Length)
throw new BrowserLimitException("The JavaScript result exceeds its character limit.");
evaluation = new(true, BoundedText.Safe(value, limits.MaximumOutputCharacters),
BoundedText.Safe(output.ToString().TrimEnd('\n'), limits.MaximumOutputCharacters), string.Empty);
}
catch (Exception exception) when (exception is not OutOfMemoryException and not StackOverflowException)
{
evaluation = new(false, string.Empty, BoundedText.Safe(output.ToString().TrimEnd('\n'),
limits.MaximumOutputCharacters), BoundedText.Safe(exception.Message, 1_024)) { BudgetExceeded = IsBudgetException(exception) };
}
Monitor.Exit(gate);
return evaluation;
}
private static string PrimitiveText(JsValue value)
{
if (value.IsObject()) return "[object Object]";
return value.ToString();
}
private static bool IsBudgetException(Exception exception) => exception is BrowserLimitException or
StatementsCountOverflowException or MemoryLimitExceededException or RecursionDepthOverflowException or
TimeoutException or ExecutionCanceledException or OperationCanceledException;
}
using Jint;
using Jint.Native;
using Jint.Native.Object;
using Jint.Runtime.Descriptors;
using Jint.Runtime.Interop;
using Jint.Runtime;
namespace XFESystem.Browser;
/// <summary>
/// A real JavaScript interpreter with explicit callbacks. No .NET objects,
/// reflection, files or sockets are exposed to script code. Timers run only when explicitly pumped.
/// These cooperative limits are not a process-level security boundary.
/// </summary>
public sealed class JavaScriptRuntime
{
private readonly object gate = new();
private readonly BrowserLimits limits;
private readonly BoundedText output;
private readonly Engine engine;
private readonly BrowserEventLoop eventLoop;
private readonly PreparedScriptCache packagedScripts;
internal long PackagedScriptCacheHits { get; private set; }
internal long PackagedScriptPreparations { get; private set; }
public JavaScriptRuntime(BrowserLimits? limits = null) : this(limits, PreparedScriptCache.Shared) { }
internal JavaScriptRuntime(BrowserLimits? limits, PreparedScriptCache packagedScripts)
{
this.packagedScripts = packagedScripts;
this.limits = limits ?? new BrowserLimits();
this.limits.Validate();
output = new(this.limits.MaximumOutputCharacters);
engine = new Engine(options =>
{
options.MaxStatements(this.limits.MaximumStatements);
options.LimitMemory(this.limits.MaximumScriptMemoryBytes);
options.TimeoutInterval(this.limits.ScriptTimeout);
options.LimitRecursion(this.limits.MaximumRecursionDepth);
options.DisableStringCompilation();
options.Interop.Enabled = false;
options.Interop.AllowGetType = false;
options.Interop.AllowSystemReflection = false;
options.Interop.AllowWrite = false;
options.AgentCanSuspend = false;
options.Constraints.RegexTimeout = TimeSpan.FromMilliseconds(100);
options.Constraints.MaxAtomicsPauseIterations = 100;
options.Constraints.PromiseTimeout = this.limits.ScriptTimeout;
options.Constraints.MaxArraySize = (uint)Math.Min(this.limits.MaximumOutputCharacters, 131_072);
});
var console = new BrowserHostObject(engine);
foreach (var name in new[] { "log", "info", "warn", "error", "debug" })
{
var callback = new ClrFunction(engine, name, (_, arguments) =>
{
for (var index = 0; index < arguments.Length; index++)
{
if (index != 0) output.Append(" ");
output.Append(PrimitiveText(arguments[index]));
}
output.Append("\n");
return JsValue.Undefined;
});
console.DefineOwnProperty(name, new PropertyDescriptor(callback, false, true, false));
}
engine.SetValue("console", console);
eventLoop = new BrowserEventLoop(engine, this.limits);
}
internal Engine Engine => engine;
/// <summary>Trusted host data readback, kept separate from terminal-safe display output.</summary>
internal string? ReadString(string expression)
{
Monitor.Enter(gate);
string? text;
try
{
var value = engine.Evaluate(expression);
if (value.IsNull() || value.IsUndefined()) text = null;
else
{
if (!value.IsString()) throw new IOException("The editor returned a non-string value.");
text = value.AsString();
if (text.Length > limits.MaximumOutputCharacters) throw new IOException("The editor result exceeds its readback limit.");
}
}
catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException)
{
Monitor.Exit(gate);
throw new IOException("Editor data readback failed: " + BoundedText.Safe(error.Message, 512), error);
}
Monitor.Exit(gate);
return text;
}
public long MonotonicMilliseconds => eventLoop.Now;
public int PendingTaskCount => eventLoop.PendingCount;
/// <summary>Collect console output from DOM events dispatched outside Evaluate/Pump.</summary>
public string DrainConsoleOutput()
{
Monitor.Enter(gate);
var result = BoundedText.Safe(output.ToString().TrimEnd('\n'), limits.MaximumOutputCharacters);
output.Clear();
Monitor.Exit(gate);
return result;
}
public JavaScriptResult Pump(long monotonicMs, int maximumCallbacks = 64)
{
Monitor.Enter(gate);
output.Clear();
JavaScriptResult result;
try
{
var count = eventLoop.Pump(monotonicMs, maximumCallbacks);
result = new(true, count.ToString(System.Globalization.CultureInfo.InvariantCulture),
BoundedText.Safe(output.ToString().TrimEnd('\n'), limits.MaximumOutputCharacters), string.Empty);
}
catch (Exception exception) when (exception is not OutOfMemoryException and not StackOverflowException)
{
result = new(false, string.Empty, BoundedText.Safe(output.ToString().TrimEnd('\n'), limits.MaximumOutputCharacters),
BoundedText.Safe(exception.Message, 1_024)) { BudgetExceeded = IsBudgetException(exception) };
}
Monitor.Exit(gate);
return result;
}
public JavaScriptResult Evaluate(string source) => EvaluateCore(source, null);
/// <summary>
/// Called only after the resource loader confirms immutable packaged provenance.
/// Only parsed syntax is reusable: the source still executes in this fresh
/// engine, with its own globals and execution constraints, on every call.
/// </summary>
internal JavaScriptResult EvaluatePackaged(string source, string resourceKey) => EvaluateCore(source, resourceKey);
private JavaScriptResult EvaluateCore(string source, string? resourceKey)
{
ArgumentNullException.ThrowIfNull(source);
if (source.Length > limits.MaximumScriptCharacters)
return new(false, string.Empty, string.Empty, "The script exceeds its character limit.") { BudgetExceeded = true };
Monitor.Enter(gate);
output.Clear();
JavaScriptResult evaluation;
try
{
JsValue result;
if (resourceKey is not null && source.Length >= PreparedScriptCache.MinimumSourceCharacters)
{
var prepared = packagedScripts.GetOrPrepare(source, resourceKey, out bool reused);
if (reused) PackagedScriptCacheHits++; else PackagedScriptPreparations++;
result = engine.Evaluate(prepared);
}
else result = engine.Evaluate(source);
var value = PrimitiveText(result);
if (value.Length > limits.MaximumOutputCharacters - output.Length)
throw new BrowserLimitException("The JavaScript result exceeds its character limit.");
evaluation = new(true, BoundedText.Safe(value, limits.MaximumOutputCharacters),
BoundedText.Safe(output.ToString().TrimEnd('\n'), limits.MaximumOutputCharacters), string.Empty);
}
catch (Exception exception) when (exception is not OutOfMemoryException and not StackOverflowException)
{
evaluation = new(false, string.Empty, BoundedText.Safe(output.ToString().TrimEnd('\n'),
limits.MaximumOutputCharacters), BoundedText.Safe(exception.Message, 1_024)) { BudgetExceeded = IsBudgetException(exception) };
}
Monitor.Exit(gate);
return evaluation;
}
private static string PrimitiveText(JsValue value)
{
if (value.IsObject()) return "[object Object]";
return value.ToString();
}
private static bool IsBudgetException(Exception exception) => exception is BrowserLimitException or
StatementsCountOverflowException or MemoryLimitExceededException or RecursionDepthOverflowException or
TimeoutException or ExecutionCanceledException or OperationCanceledException;
}