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

XFESystem

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

公开
关注 0 Fork 0 Star 0
UTF-8
using AngleSharp.Dom;

namespace XFESystem.Browser;

public sealed partial class BrowserService
{
    private readonly HashSet<IElement> executedScripts = new(ReferenceEqualityComparer.Instance);
    private bool drainingResources;
    private bool scriptBudgetExhausted;
    private readonly Dictionary<IElement, string> styleAddresses = new(ReferenceEqualityComparer.Instance);
    private int pageScriptSequence;

    private BrowserDomHost CreateDomHost() => new()
    {
        Mutated = InvalidateDocument,
        AttributeChanged = (_, name) => { if (layoutDirty || layoutEngine.AttributeAffectsLayout(name)) InvalidateDocument(); },
        FocusChanged = _ => InvalidateDocument(),
        SubmitForm = (form, submitter) => SubmitForm(form, submitter, fireEvent: false),
        Navigate = reference => pendingNavigation = ResourceAddress(reference).AbsoluteUri,
        ResourceRequested = element =>
        {
            if (scheduledResources.Add(element)) pendingResources.Enqueue(element);
        },
        Measure = node => EnsureLayout().GetBoundingClientRect(node),
        MeasureRange = (start, startOffset, end, endOffset) => EnsureLayout().GetRangeRects(start,startOffset,end,endOffset),
        HitTest = (x,y) => EnsureLayout().HitTest(x,y)?.Node,
        ComputedStyle = (node, property) => EnsureLayout().GetComputedStyle(node)?.GetPropertyValue(property),
        MeasureText = (text, font) => textMetrics.Measure(text, font),
        RasterizeText = (text, font) => metricCanvas?.RasterizeWebText(text, font.Size, font.Family, font.Weight)
            ?? throw new NotSupportedException("The platform did not supply a real Canvas text rasterizer."),
        ImageInfo = element =>
        {
            string source = element.GetAttribute("src") ?? "";
            var image = FindImage(source);
            bool complete = source.Length == 0;
            if (!complete)
            {
                try { complete = images.ContainsKey(ImageKey(source)); }
                catch (Exception error) when (error is UriFormatException or NotSupportedException) { complete = true; }
            }
            return (complete, image?.Width ?? 0, image?.Height ?? 0);
        },
        ReadCookies = () => transport is IWebCookieStore store ? store.ReadDocumentCookies(currentAddress!) : pageCookies.ReadDocumentCookies(currentAddress!),
        WriteCookie = value => { if (transport is IWebCookieStore store) store.WriteDocumentCookie(currentAddress!, value); else pageCookies.WriteDocumentCookie(currentAddress!, value); },
        GetResourceWithHeaders = (reference, headers) =>
        {
            if (scriptPolicy?.BlocksAllScripts == true) throw new InvalidOperationException("The declared connection policy cannot be enforced.");
            Uri target = ResourceAddress(reference);
            if (target.GetLeftPart(UriPartial.Authority) != currentAddress!.GetLeftPart(UriPartial.Authority))
                throw new NotSupportedException("Cross-origin script requests require CORS support.");
            var response = resourceLoader!.Get(target, "fetch", headers);
            if (response.Address.GetLeftPart(UriPartial.Authority) != currentAddress.GetLeftPart(UriPartial.Authority))
                throw new NotSupportedException("A script request redirected outside its origin.");
            return response;
        },
        FillRandom = transport is IBrowserRandomSource random ? random.FillRandom : null,
        Viewport = () => viewport
    };

    private Uri ResourceAddress(string reference)
    {
        Uri basis = currentAddress!;
        string? declared = document?.QuerySelector("base[href]")?.GetAttribute("href");
        if (!string.IsNullOrEmpty(declared) && Uri.TryCreate(basis, declared, out var candidate))
        { ValidateWebAddress(candidate); if (scriptPolicy?.AllowsBase(currentAddress!, candidate) != false) basis = candidate; }
        var address = new Uri(basis, reference);
        ValidateWebAddress(address); return address;
    }

    private bool RunScript(IElement script)
    {
        CheckOperationCancellation();
        if (scriptBudgetExhausted) return false;
        if (dom!.IsInertParsedScript(script)) return true;
        if (scriptPolicy?.Allows(script, dom!.IsDynamicScript(script)) == false || !executedScripts.Add(script)) return true;
        string type = (script.GetAttribute("type") ?? string.Empty).Trim().ToLowerInvariant();
        if (type == "module") { Diagnostic("Module scripts are not supported by the classic-script interpreter."); return true; }
        if (type is not ("" or "text/javascript" or "application/javascript" or "text/ecmascript" or "application/ecmascript")) return true;
        try
        {
            bool external = script.HasAttribute("src");
            string source = external
                ? resourceLoader!.Get(ResourceAddress(script.GetAttribute("src")!), "script").Body : script.TextContent;
            CheckOperationCancellation();
            Trace?.Invoke("XFE_BROWSER_SCRIPT_BEGIN|" + (script.GetAttribute("src") ?? "Inline") + "|Characters|" + source.Length + "\n");
            CheckOperationCancellation();
            long previousRevision = Revision;
            long previousLayouts = LayoutPassCount, previousBudgetChecks = dom!.BudgetValidationCount;
            dom!.CurrentScript = script;
            executingPageScript = true;
            long scriptStarted = System.Diagnostics.Stopwatch.GetTimestamp();
            var result = external && resourceLoader!.LastResponsePackaged
                ? runtime!.EvaluatePackaged(source, ResourceAddress(script.GetAttribute("src")!).AbsoluteUri)
                : runtime!.EvaluateNamed(source, external ? ResourceAddress(script.GetAttribute("src")!).AbsoluteUri : currentAddress!.AbsoluteUri + "#inline-" + (++pageScriptSequence));
            dom.CurrentScript = null;
            executingPageScript = false;
            Trace?.Invoke("XFE_BROWSER_SCRIPT_END|" + (script.GetAttribute("src") ?? "Inline") + "|Success|" + result.Success + "\n");
            Trace?.Invoke("XFE_BROWSER_SCRIPT_TIMING|" + (script.GetAttribute("src") ?? "Inline") +
                "|Ticks|" + (System.Diagnostics.Stopwatch.GetTimestamp() - scriptStarted) +
                "|PreparationTicks|" + runtime!.LastScriptTiming.PreparationTicks +
                "|AdmissionTicks|" + runtime.LastScriptTiming.AdmissionTicks +
                "|ExecutionTicks|" + runtime.LastScriptTiming.ExecutionTicks +
                "|CacheHit|" + runtime.LastScriptTiming.CacheHit +
                "|LayoutPasses|" + (LayoutPassCount - previousLayouts) +
                "|DomBudgetValidations|" + (dom.BudgetValidationCount - previousBudgetChecks) +
                "|Frequency|" + System.Diagnostics.Stopwatch.Frequency + "\n");
            CheckOperationCancellation();
            if (result.ConsoleOutput.Length != 0) Diagnostic(result.ConsoleOutput);
            if (!result.Success)
            {
                firstScriptFailure ??= result.Error;
                Diagnostic("JavaScript error: " + result.Error);
            }
            if (script.HasAttribute("src")) dom!.DispatchEvent(script, result.Success ? "load" : "error", new() { Bubbles = false, Cancelable = false });
            scriptBudgetExhausted |= result.BudgetExceeded;
            // DOM mutation and event callbacks already invalidate their real
            // changes. Loading a pure library need not re-rasterize the page.
            if (!documentEnhancementsPending || Revision != previousRevision) InvalidateDocument();
            return !result.BudgetExceeded;
        }
        catch (OperationCanceledException) when (OperationCancelled) { throw; }
        catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException)
        {
            executingPageScript = false;
            dom!.CurrentScript = null;
            if (error is BrowserLimitException)
                Trace?.Invoke("XFE_BROWSER_SCRIPT_REJECTED|" + BoundedText.Safe(script.GetAttribute("src") ?? "Inline", 256) + "|Reason|ResourceBudget\n");
            Diagnostic("External script failed: " + error.Message);
            firstScriptFailure ??= error.Message;
            dom!.DispatchEvent(script, "error", new() { Bubbles = false, Cancelable = false });
            return true;
        }
    }

    private void LoadStyle(IElement link)
    {
        CheckOperationCancellation();
        if (cspBlocksAuthorStyles || !(link.GetAttribute("rel") ?? "").Split(' ', StringSplitOptions.RemoveEmptyEntries).Contains("stylesheet", StringComparer.OrdinalIgnoreCase)) return;
        try
        {
            string address = ResourceAddress(link.GetAttribute("href") ?? "").AbsoluteUri;
            if (styleAddresses.GetValueOrDefault(link) == address) return;
            linkedStyles.RemoveAll(item => !IsConnected(item.Owner));
            if (linkedStyles.Count(item => !ReferenceEquals(item.Owner, link)) >= activeLimits.MaximumStyleSheets) throw new BrowserLimitException("The stylesheet count limit was reached.");
            var response = resourceLoader!.Get(ResourceAddress(link.GetAttribute("href") ?? ""), "style");
            CheckOperationCancellation();
            Trace?.Invoke("XFE_BROWSER_STYLE|" + response.Address + "|Characters|" + response.Body.Length + "\n");
            linkedStyles.RemoveAll(item => ReferenceEquals(item.Owner, link));
            linkedStyles.Add((response.Body, link)); InvalidateDocument();
            styleAddresses[link] = address;
            dom?.DispatchEvent(link, "load", new() { Bubbles = false, Cancelable = false });
        }
        catch (OperationCanceledException) when (OperationCancelled) { throw; }
        catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException)
        { Diagnostic("Stylesheet failed: " + error.Message); dom?.DispatchEvent(link, "error", new() { Bubbles = false, Cancelable = false }); }
    }

    private void DrainResources(int maximum = int.MaxValue)
    {
        if (drainingResources || dom is null) return;
        if (documentEnhancementsPending) maximum = Math.Min(maximum, 1);
        maximum = Math.Min(maximum, activeLimits.MaximumEventCallbacksPerPump);
        drainingResources = true;
        // Explicit success/catch cleanup is needed by the current native runtime.
        try
        {
            int count = 0;
            while (count < maximum && (pendingResources.Count != 0 || dom.HasNetworkRequests))
            {
                CheckOperationCancellation();
                if (pendingResources.Count == 0)
                {
                    count++;
                    try { dom.ProcessNetworkRequest(); CaptureEventOutput(); }
                    catch (OperationCanceledException) when (OperationCancelled) { throw; }
                    catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException)
                    { Diagnostic("Network callback failed: " + error.Message); }
                    continue;
                }
                var element = pendingResources.Dequeue();
                scheduledResources.Remove(element);
                if (++count > activeLimits.MaximumExternalResources) throw new BrowserLimitException("The dynamic resource queue limit was reached.");
                if (element.Owner != document || !IsConnected(element) && element.LocalName != "img") continue;
                if (element.LocalName == "script") RunScript(element); else if (element.LocalName == "link") LoadStyle(element); else if (element.LocalName == "img") LoadImage(element);
            }
        }
        catch { drainingResources = false; throw; }
        drainingResources = false;
    }

    private static bool IsConnected(INode node)
    { while (node.Parent is not null) node = node.Parent; return node.NodeType == NodeType.Document; }
    private void Diagnostic(string message) => pageDiagnostics?.Append(BoundedText.Safe(message, 2_048) + "\n");
}