XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFESystem

【Rust】XFE系统,基于Rust和汇编

公开
关注 0 Fork 0 Star 0
UTF-8
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;
}