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

XFESystem

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

公开
关注 0 Fork 0 Star 0
UTF-8
namespace XFESystem.Documents;

public static class WebsiteAssets
{
    public const string BaseUri = "https://xfe-resources.invalid/Documents/";
    public static IReadOnlyList<string> StyleNames { get; } = Array.AsReadOnly(new[]
        { "CodeStyle.css", "MarkdownStyle.css", "Log.css", "Terminal.css", "Katex.min.css" });
    public static IReadOnlyList<string> ScriptNames { get; } = Array.AsReadOnly(new[]
        { "Highlight.min.js", "Xaml.js", "CSharp.js", "Bash.js", "PowerShell.js", "Dos.js", "Log.js", "PlainText.js",
          "Katex.min.js", "AutoRender.min.js", "CodeBlockTools.js", "DocUtils.js" });

    public static Stream Open(string name)
    {
        ValidateName(name);
        return typeof(WebsiteAssets).Assembly.GetManifestResourceStream("Documents/Website/" + name)
            ?? throw new FileNotFoundException("The requested bundled document resource does not exist.", name);
    }

    public static string ReadText(string name)
    {
        ValidateName(name);
        if (ImmutableTextResources.Values.TryGetValue(name, out var text)) return text;
        // Keep the legacy decoding behavior for unusual callers of ReadText on
        // binary assets, without retaining decoded font garbage for the boot.
        using var reader = new StreamReader(Open(name));
        return reader.ReadToEnd();
    }

    // Decode each finite embedded resource once, before native heap fragmentation
    // makes multi-megabyte strings difficult to allocate. Static initialization
    // publishes the completed read-only catalog atomically across threads; reads
    // never mutate it, evict entries, hold an engine lock or decode again.
    private static class ImmutableTextResources
    {
        internal static readonly IReadOnlyDictionary<string, string> Values = Load();
        private static IReadOnlyDictionary<string, string> Load()
        {
            const string prefix = "Documents/Website/";
            const int maximumEntries = 512, maximumResourceBytes = 8_388_608, maximumCharacters = 16_777_216;
            var values = new Dictionary<string, string>(StringComparer.Ordinal);
            long characters = 0;
            foreach (string resource in typeof(WebsiteAssets).Assembly.GetManifestResourceNames().OrderBy(name => name, StringComparer.Ordinal))
            {
                if (!resource.StartsWith(prefix, StringComparison.Ordinal)) continue;
                string extension = Path.GetExtension(resource).ToLowerInvariant();
                if (extension is not (".js" or ".css" or ".json" or ".md" or ".txt" or ".html" or ".htm")) continue;
                if (values.Count == maximumEntries) throw new InvalidDataException("The bundled document text catalog exceeds its entry limit.");
                using var stream = typeof(WebsiteAssets).Assembly.GetManifestResourceStream(resource)!;
                if (stream.Length > maximumResourceBytes) throw new InvalidDataException("A bundled document resource exceeds its byte limit.");
                // Keep the original ReadText UTF-8/BOM semantics exactly. Open
                // remains the byte-exact API, including for binary font assets.
                using var reader = new StreamReader(stream);
                string text = reader.ReadToEnd();
                if (text.Length > maximumCharacters - characters) throw new InvalidDataException("The bundled document text catalog exceeds its character limit.");
                values.Add(resource[prefix.Length..], text); characters += text.Length;
            }
            return new System.Collections.ObjectModel.ReadOnlyDictionary<string, string>(values);
        }
    }

    public static int PrewarmTextResources() => ImmutableTextResources.Values.Count;

    /// <summary>Original website URL paths routed to immutable embedded bytes.</summary>
    public static IReadOnlyList<WebsiteWebResource> WebResources => WebsiteWebResourceCatalog.Resources;

    public static bool TryOpenWebPath(string path, out Stream? stream, out string contentType)
    {
        stream = null;
        contentType = string.Empty;
        if (string.IsNullOrEmpty(path) || path.Contains('\\') || path.Contains('?') || path.Contains('#') ||
            path.Split('/').Skip(1).Any(segment => segment is "" or "." or "..")) return false;
        foreach (var resource in WebResources)
        {
            if (!string.Equals(path, resource.UrlPath, StringComparison.Ordinal)) continue;
            stream = Open(resource.AssetName);
            contentType = resource.ContentType;
            return true;
        }
        return false;
    }

    public static bool TryReadWebText(string path, out string text, out string contentType)
    {
        text = contentType = string.Empty;
        if (string.IsNullOrEmpty(path) || path.Contains('\\') || path.Contains('?') || path.Contains('#') ||
            path.Split('/').Skip(1).Any(segment => segment is "" or "." or "..")) return false;
        foreach (var resource in WebResources)
        {
            if (!string.Equals(path, resource.UrlPath, StringComparison.Ordinal)) continue;
            text = ReadText(resource.AssetName); contentType = resource.ContentType; return true;
        }
        return false;
    }

    private static void ValidateName(string name)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(name);
        if (name.Contains('\\') || name.StartsWith('/') || name.Split('/').Any(segment => segment is "" or "." or ".."))
            throw new ArgumentException("Invalid resource name.", nameof(name));
    }
}