using Jint; using Jint.Native; using Jint.Native.Object; using Jint.Runtime.Descriptors; using Jint.Runtime.Interop; using Jint.Runtime; namespace XFESystem.Browser; /// /// 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. /// 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; /// Trusted host data readback, kept separate from terminal-safe display output. 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; /// Collect console output from DOM events dispatched outside Evaluate/Pump. 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); /// /// 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. /// 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; }