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

XFESystem

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

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Globalization;
using System.Text;
using AngleSharp.Dom;
using AngleSharp.Html.Dom;

namespace XFESystem.Browser;

public sealed partial class BrowserService
{
    private string? remoteEvidenceAddress, remoteBodySha256;
    private bool remoteCertificateValidated;
    private void RecordRemoteResponseEvidence(string? address, string? bodyHash, bool certificateValidated)
    { remoteEvidenceAddress = address; remoteBodySha256 = bodyHash; remoteCertificateValidated = certificateValidated; }

    /// <summary>Read actual DOM, loaded resources and existing layout commands on the owning worker.
    /// No script, resource load or screenshot enters this readback.</summary>
    public string ReadRemotePageProof(long snapshotSequence = 0) => WithGate(() =>
    {
        if (document is null || displayList is null || currentAddress?.Scheme is not ("http" or "https") ||
            remoteEvidenceAddress != currentAddress.AbsoluteUri || string.IsNullOrEmpty(remoteBodySha256)) return "{}";
        var visible = new BrowserRect(0, 0, viewport.Width, viewport.Height);
        var json = new StringBuilder("{\"Kind\":\"ActualRemotePageDomReadback\",\"Address\":").Append(JavaScriptString(currentAddress.AbsoluteUri))
            .Append(",\"Title\":").Append(JavaScriptString(document.Title ?? "")).Append(",\"BodySha256\":").Append(JavaScriptString(remoteBodySha256))
            .Append(",\"CertificateValidated\":").Append(remoteCertificateValidated ? "true" : "false")
            .Append(",\"SnapshotSequence\":").Append(snapshotSequence)
            .Append(",\"DocumentGeneration\":").Append(DocumentGeneration)
            .Append(",\"ViewportWidth\":").Append(Number(viewport.Width)).Append(",\"ViewportHeight\":").Append(Number(viewport.Height))
            .Append(",\"DocumentHeight\":").Append(Number(displayList.DocumentHeight))
            .Append(",\"ScrollY\":").Append(Number(viewport.ScrollY)).Append(",\"TextRuns\":[");
        bool comma = false; int count = 0;
        foreach (var command in displayList.Commands)
        {
            if (command.Kind != BrowserDrawKind.Text || command.Opacity <= 0 || string.IsNullOrWhiteSpace(command.Text)) continue;
            var bounds = command.ViewportPinned ? command.Bounds : (command.Transform ?? BrowserAffineTransform.Identity).Bounds(command.Bounds).Translate(-viewport.ScrollX, -viewport.ScrollY);
            if (bounds.Intersect(visible) is not { Width: > 0, Height: > 0 }) continue;
            if (comma) json.Append(','); comma = true;
            json.Append("{\"Text\":").Append(JavaScriptString(command.Text.Length > 256 ? command.Text[..256] : command.Text))
                .Append(",\"Bounds\":").Append(Rect(bounds)).Append(",\"Color\":").Append(JavaScriptString("#" + command.Color.ToString("X6")))
                .Append(",\"Ancestors\":").Append(JavaScriptString(AncestorTags(command.Node)))
                .Append(",\"ListItemId\":").Append(ListItemIdentity(command.Node)).Append('}');
            if (++count == 256) break;
        }
        json.Append("],\"SearchInputs\":["); comma = false;
        foreach (var element in document.QuerySelectorAll("input[name],textarea[name]").Take(16))
        {
            if (comma) json.Append(','); comma = true;
            json.Append("{\"Name\":").Append(JavaScriptString(element.GetAttribute("name") ?? ""))
                .Append(",\"Value\":").Append(JavaScriptString(element.GetAttribute("name") == "q" ? element is IHtmlInputElement { Type: "text" or "search" } input ? input.Value ?? "" : element is IHtmlTextAreaElement area ? area.Value ?? "" : "" : ""))
                .Append(",\"Bounds\":").Append(Rect(displayList.GetBoundingClientRect(element))).Append('}');
        }
        json.Append("],\"LoadedImages\":["); comma = false; count = 0;
        foreach (var command in displayList.Commands)
        {
            if (command.Kind != BrowserDrawKind.Image || command.Opacity <= 0 || FindImage(command.ImageSource) is not { } image) continue;
            var bounds = (command.Transform ?? BrowserAffineTransform.Identity).Bounds(command.Bounds).Translate(-viewport.ScrollX, -viewport.ScrollY);
            if (bounds.Intersect(visible) is not { Width: > 0, Height: > 0 }) continue;
            if (comma) json.Append(','); comma = true;
            string source = command.ImageSource.StartsWith("data:", StringComparison.OrdinalIgnoreCase) ? "data:image" : ImageKey(command.ImageSource);
            json.Append("{\"Source\":").Append(JavaScriptString(source)).Append(",\"Bounds\":").Append(Rect(bounds))
                .Append(",\"Format\":").Append(JavaScriptString(imageFormats.GetValueOrDefault(ImageKey(command.ImageSource), "")))
                .Append(",\"Width\":").Append(image.Width).Append(",\"Height\":").Append(image.Height)
                .Append(",\"Opacity\":").Append(Number(command.Opacity)).Append(",\"Samples\":[");
            bool sampleComma = false;
            // Source-nearest samples qualify only the existing axis-aligned
            // image path; affine images carry geometry/decoding evidence here.
            for (int row = 0; command.Transform is null && row < 6; row++) for (int column = 0; column < 8; column++)
            {
                int x = (int)Math.Floor(bounds.X + (column + .5) * bounds.Width / 8), y = (int)Math.Floor(bounds.Y + (row + .5) * bounds.Height / 6);
                if (!visible.Contains(x, y)) continue;
                int sx = Math.Clamp((int)((x - bounds.X) * image.Width / bounds.Width), 0, image.Width - 1);
                int sy = Math.Clamp((int)((y - bounds.Y) * image.Height / bounds.Height), 0, image.Height - 1);
                if (sampleComma) json.Append(','); sampleComma = true;
                json.Append("{\"X\":").Append(x).Append(",\"Y\":").Append(y).Append(",\"Argb\":")
                    .Append(JavaScriptString(image.Pixel(sx, sy).ToString("X8", CultureInfo.InvariantCulture))).Append('}');
            }
            json.Append("]}"); if (++count == 16) break;
        }
        json.Append("],\"Links\":["); comma = false; count = 0;
        foreach (var element in document.QuerySelectorAll("a[href]"))
        {
            var bounds = displayList.GetBoundingClientRect(element);
            if (bounds.Intersect(visible) is not { Width: > 0, Height: > 0 } || displayList.GetComputedStyle(element)?.Visibility == "hidden" || string.IsNullOrWhiteSpace(element.TextContent)) continue;
            if (!Uri.TryCreate(currentAddress, element.GetAttribute("href"), out var target)) continue;
            if (comma) json.Append(','); comma = true;
            json.Append("{\"Address\":").Append(JavaScriptString(target.AbsoluteUri)).Append(",\"Text\":")
                .Append(JavaScriptString(element.TextContent[..Math.Min(element.TextContent.Length, 256)]))
                .Append(",\"Bounds\":").Append(Rect(bounds)).Append('}');
            if (++count == 64) break;
        }
        json.Append("],\"LayoutSamples\":["); comma = false; count = 0;
        foreach (var box in displayList.Boxes)
        {
            if (box.Node is not IElement element || box.GeneratedPseudo is not null || !box.Visible ||
                element.LocalName is not ("cite" or "h2") && box.Transform.IsIdentity) continue;
            var bounds = displayList.GetBoundingClientRect(element);
            if (bounds.Intersect(visible) is not { Width: > 0, Height: > 0 }) continue;
            if (comma) json.Append(','); comma = true;
            json.Append("{\"Tag\":").Append(JavaScriptString(element.LocalName)).Append(",\"Class\":").Append(JavaScriptString(element.ClassName ?? ""))
                .Append(",\"Bounds\":").Append(Rect(bounds)).Append(",\"ListItemId\":").Append(ListItemIdentity(element))
                .Append(",\"Transform\":[").Append(string.Join(',', new[] { box.Transform.A, box.Transform.B, box.Transform.C, box.Transform.D, box.Transform.E, box.Transform.F }.Select(Number)))
                .Append("],\"CssTransform\":").Append(JavaScriptString(box.Style.Get("transform", "none"))).Append('}');
            if (++count == 64) break;
        }
        json.Append("],\"MissingVisibleImages\":["); comma = false; count = 0;
        var imageClip = new BrowserRect(viewport.ScrollX, viewport.ScrollY, viewport.Width, viewport.Height); var imageClips = new Stack<BrowserRect>();
        var paintedLinks = new HashSet<INode>(ReferenceEqualityComparer.Instance);
        foreach (var command in displayList.Commands)
        {
            if (command.Kind == BrowserDrawKind.PushClip) { imageClips.Push(imageClip); imageClip = imageClip.Intersect(command.Bounds); continue; }
            if (command.Kind == BrowserDrawKind.PopClip) { if (imageClips.TryPop(out var prior)) imageClip = prior; continue; }
            if (command.Kind == BrowserDrawKind.Text && command.Opacity > 0 && !string.IsNullOrWhiteSpace(command.Text) &&
                (command.Transform ?? BrowserAffineTransform.Identity).Bounds(command.Bounds).Intersect(imageClip) is { Width: > 0, Height: > 0 })
                for (var element = command.Node as IElement ?? command.Node?.ParentElement; element is not null; element = element.ParentElement)
                    if (element.LocalName == "a") { paintedLinks.Add(element); break; }
            if (command.Kind != BrowserDrawKind.Image || command.Opacity <= 0 || command.ImageSource.Length == 0 || FindImage(command.ImageSource) is not null) continue;
            var bounds = (command.Transform ?? BrowserAffineTransform.Identity).Bounds(command.Bounds).Intersect(imageClip).Translate(-viewport.ScrollX, -viewport.ScrollY);
            if (bounds.Intersect(visible) is not { Width: > 0, Height: > 0 }) continue;
            if (comma) json.Append(','); comma = true;
            json.Append("{\"Source\":").Append(JavaScriptString(command.ImageSource.StartsWith("data:", StringComparison.OrdinalIgnoreCase) ? "data:image" : ImageKey(command.ImageSource)))
                .Append(",\"Bounds\":").Append(Rect(bounds)).Append('}');
            if (++count == 64) break;
        }
        json.Append("],\"NavigationLinks\":["); comma = false; count = 0;
        foreach (var element in document.QuerySelectorAll("nav a[href],[role=navigation] a[href]"))
        {
            if (string.IsNullOrWhiteSpace(element.TextContent) || HiddenNavigationAncestor(element)) continue;
            var menu = element.Closest("nav,[role=navigation]")!;
            var menuBounds = displayList.GetBoundingClientRect(menu);
            if (menuBounds.Intersect(visible) is not { Width: > 0, Height: > 0 }) continue;
            if (comma) json.Append(','); comma = true;
            json.Append("{\"MenuId\":").Append(displayList.GetNodeIdentity(menu)).Append(",\"ContainerBounds\":").Append(Rect(menuBounds))
                .Append(",\"Text\":").Append(JavaScriptString(element.TextContent.Length > 128 ? element.TextContent[..128] : element.TextContent))
                .Append(",\"Bounds\":").Append(Rect(displayList.GetBoundingClientRect(element)))
                .Append(",\"Painted\":").Append(paintedLinks.Contains(element) ? "true" : "false").Append('}');
            if (++count == 64) break;
        }
        json.Append("],\"LoadedStyles\":["); comma = false;
        foreach (var receipt in LoadedResources.Where(item => item.Kind == "style").Take(128))
        {
            if (comma) json.Append(','); comma = true;
            json.Append("{\"Address\":").Append(JavaScriptString(receipt.Address))
                .Append(",\"FinalAddress\":").Append(JavaScriptString(receipt.Address))
                .Append(",\"RequestedAddress\":").Append(JavaScriptString(receipt.RequestedAddress ?? receipt.Address))
                .Append(",\"Characters\":").Append(receipt.Characters).Append('}');
        }
        json.Append("],\"DeclaredStyleLinks\":["); comma = false;
        foreach (var link in document.QuerySelectorAll("link[rel~=stylesheet][href]").Take(128))
        {
            if (!Uri.TryCreate(currentAddress, link.GetAttribute("href"), out var uri)) continue;
            if (comma) json.Append(','); comma = true; json.Append(JavaScriptString(uri.AbsoluteUri));
        }
        json.Append("],\"InlineStyleCount\":").Append(document.QuerySelectorAll("style").Length)
            .Append(",\"DecodedImageFormats\":[").Append(string.Join(',', imageFormats.Values.Distinct().Select(JavaScriptString))).Append(']')
            .Append(",\"DocumentInitializationComplete\":").Append(!documentEnhancementsPending && !awaitingDocumentPresentation ? "true" : "false")
            .Append(",\"ScriptBudgetExceeded\":").Append(scriptBudgetExhausted ? "true" : "false")
            .Append(",\"PendingResources\":").Append(pendingResources.Count + (dom?.NetworkRequestCount ?? 0) + (documentEnhancementsPending ? Math.Max(1, pendingDocumentScripts.Count) : 0))
            .Append(",\"DomBudgetValidations\":").Append(dom?.BudgetValidationCount ?? 0)
            .Append(",\"DomBudgetNodeVisits\":").Append(dom?.BudgetNodeVisits ?? 0)
            .Append(",\"DeclaredCsp\":").Append(cspDeclared ? "true" : "false")
            .Append(",\"AuthorStylesBlockedByCsp\":").Append(cspBlocksAuthorStyles ? "true" : "false")
            .Append(",\"BaseHrefCount\":").Append(document.QuerySelectorAll("base[href]").Length)
            .Append(",\"Diagnostics\":").Append(JavaScriptString(pageDiagnostics?.ToString() ?? ""))
            .Append(",\"ReadOnly\":true,\"NoHostScreenshotSubstitution\":true}");
        return json.ToString();
    });

    private int ListItemIdentity(INode? node)
    {
        for (var item = node as IElement ?? node?.ParentElement; item is not null; item = item.ParentElement)
            if (displayList!.GetComputedStyle(item)?.Display == "list-item")
                return displayList.GetNodeIdentity(item);
        return 0;
    }
    private bool HiddenNavigationAncestor(IElement element)
    {
        for (IElement? item = element; item is not null; item = item.ParentElement)
            if (displayList!.GetComputedStyle(item) is { } style && (style.Display == "none" || style.Visibility == "hidden")) return true;
        return false;
    }
    private static string AncestorTags(INode? node)
    {
        var tags = new StringBuilder(); int count = 0;
        for (var element = node as IElement ?? node?.ParentElement; element is not null && count++ < 12; element = element.ParentElement)
        { if (tags.Length > 0) tags.Append('|'); tags.Append(element.LocalName); }
        return tags.ToString();
    }
    private static string Number(double value) => double.IsFinite(value) ? value.ToString("0.###", CultureInfo.InvariantCulture) : "0";
    private static string Rect(BrowserRect value) => "{\"X\":" + Number(value.X) + ",\"Y\":" + Number(value.Y) +
        ",\"Width\":" + Number(value.Width) + ",\"Height\":" + Number(value.Height) + "}";
}