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

XFESystem

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

公开
关注 0 Fork 0 Star 0
UTF-8
"""Collect locked C# dependency notices without modifying NuGet packages.

Use an already restored cache. --Fetch permits only the pinned, hashed official
GitHub license URLs in LicenseSources.json; otherwise collection is offline.
Existing output must be byte-identical, so reruns cannot silently replace notices.
"""
from __future__ import annotations

import argparse
import base64
import hashlib
import json
import re
import urllib.request
import xml.etree.ElementTree as ET
import zipfile
from pathlib import Path


ROOT = Path(__file__).resolve().parents[2]
OUTPUT = ROOT / "Tools/CSharpBoot/Licenses/Collected"
SOURCES = ROOT / "Tools/CSharpBoot/LicenseSources.json"
KERNEL_LOCK = ROOT / "Source/XFESystem.Kernel/Packages.lock.json"


def sha(data: bytes) -> str:
    return hashlib.sha256(data).hexdigest()


def save(relative: str, data: bytes) -> dict:
    target = (OUTPUT / relative).resolve()
    if not target.is_relative_to(OUTPUT.resolve()):
        raise ValueError("License output escaped its dedicated directory")
    target.parent.mkdir(parents=True, exist_ok=True)
    if target.exists():
        if target.read_bytes() != data:
            raise ValueError(f"Existing notice differs: {target}")
    else:
        with target.open("xb") as stream:
            stream.write(data)
    return {"Path": relative, "Sha256": sha(data), "Bytes": len(data)}


def read_json(path: Path):
    return json.loads(path.read_text(encoding="utf-8-sig"))


def package_license_key(identifier: str) -> str:
    identifier = identifier.lower()
    if "cosmos." in identifier:
        return "Cosmos"
    if identifier.startswith("microsoft.codeanalysis.analyzers"):
        return "RoslynAnalyzers"
    if identifier.startswith("microsoft.codeanalysis"):
        return "Roslyn"
    return {
        "acornima": "Acornima", "anglesharp": "AngleSharp",
        "anglesharp.css": "AngleSharpCss", "htmlsanitizer": "HtmlSanitizer",
        "humanizer.core": "Humanizer", "jint": "Jint", "markdig": "Markdig",
        "mono.cecil": "MonoCecil",
    }.get(identifier, "DotNetRuntime" if identifier.startswith(("microsoft.", "runtime.", "system.")) else "")


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--NuGetCache", type=Path, default=Path.home() / ".nuget/packages")
    parser.add_argument("--Fetch", action="store_true")
    args = parser.parse_args()
    specifications = read_json(SOURCES)
    official = {}
    files = []
    for entry in specifications["OfficialSources"]:
        relative = f"Upstream/{entry['Name']}/License.txt"
        target = OUTPUT / relative
        if target.exists():
            content = target.read_bytes()
        elif args.Fetch:
            url = entry["Url"]
            if not url.startswith("https://raw.githubusercontent.com/"):
                raise ValueError("Only fixed official GitHub raw sources may be fetched")
            request = urllib.request.Request(url, headers={"User-Agent": "XFESystem-LicenseCollector"})
            with urllib.request.urlopen(request, timeout=30) as response:
                if not response.url.startswith("https://raw.githubusercontent.com/"):
                    raise ValueError("Unexpected license download redirect")
                content = response.read(1_048_577)
        else:
            raise FileNotFoundError(f"Missing {relative}; use --Fetch for the pinned official source")
        if sha(content) != entry["Sha256"]:
            raise ValueError(f"Official license hash mismatch: {entry['Name']}")
        record = save(relative, content) | {"Source": entry["Url"], "Note": entry.get("Note", "")}
        files.append(record)
        official[entry["Name"]] = record["Path"]

    for entry in specifications["RepositoryFiles"]:
        source_root = args.NuGetCache.resolve() if "NuGetFile" in entry else ROOT
        source_path = (source_root / (entry["NuGetFile"] if "NuGetFile" in entry else entry["Source"])).resolve()
        if not source_path.is_relative_to(source_root):
            raise ValueError("Local license source escaped its repository or NuGet cache")
        source_description = "NuGetCache:" + entry["NuGetFile"] if "NuGetFile" in entry else entry["Source"]
        content = source_path.read_bytes()
        if sha(content) != entry["Sha256"]:
            raise ValueError(f"Local license changed: {source_description}")
        record = save(entry["Destination"], content) | {"Source": source_description}
        files.append(record)
        if "Key" in entry:
            official[entry["Key"]] = record["Path"]

    lock = read_json(KERNEL_LOCK)
    dependencies = {}
    for framework in lock["dependencies"].values():
        for identifier, dependency in framework.items():
            if dependency["type"] != "Project":
                old = dependencies.get(identifier)
                current = (dependency["resolved"], dependency["contentHash"])
                if old is not None and old != current:
                    raise ValueError(f"Conflicting locked package: {identifier}")
                dependencies[identifier] = current
    for item in specifications["ImplicitRuntimePackages"]:
        dependencies[item["Id"]] = (item["Version"], None)

    packages = []
    for identifier, (version, expected_hash) in sorted(dependencies.items()):
        if not re.fullmatch(r"[A-Za-z0-9_.-]+", identifier) or not re.fullmatch(r"[A-Za-z0-9_.+-]+", version):
            raise ValueError("Invalid locked package identifier or version")
        cache = args.NuGetCache / identifier.lower() / version
        archive = cache / f"{identifier.lower()}.{version}.nupkg"
        package_bytes = archive.read_bytes()
        zip_sha512 = base64.b64encode(hashlib.sha512(package_bytes).digest()).decode("ascii")
        sidecar = archive.with_suffix(".nupkg.sha512").read_text(encoding="utf-8-sig").strip()
        if sidecar != zip_sha512:
            raise ValueError(f"NuGet archive differs from its SHA512 sidecar: {identifier}")
        metadata = read_json(cache / ".nupkg.metadata")
        if expected_hash is not None and metadata["contentHash"] != expected_hash:
            raise ValueError(f"NuGet metadata differs from the locked content hash: {identifier}")
        prefix = f"Packages/{identifier}-{version}"
        notices = []
        with zipfile.ZipFile(archive) as package:
            nuspec_name = next(name for name in package.namelist() if name.lower().endswith(".nuspec"))
            nuspec = package.read(nuspec_name)
            xml = ET.fromstring(nuspec)
            tags = {element.tag.rsplit("}", 1)[-1]: element for element in xml.iter()}
            license_tag = tags.get("license")
            license_value = license_tag.text if license_tag is not None else None
            license_type = license_tag.get("type") if license_tag is not None else None
            record = save(f"{prefix}/Metadata.nuspec", nuspec) | {"Source": f"nuget:{identifier}/{version}/{nuspec_name}"}
            files.append(record)
            for name in package.namelist():
                basename = Path(name).name.lower()
                is_notice = any(term in basename for term in ("license", "notice", "copying"))
                is_declared = license_type == "file" and name.lower() == (license_value or "").lower()
                if name.endswith("/") or not (is_notice or is_declared):
                    continue
                if Path(basename).suffix not in ("", ".txt", ".md", ".rtf", ".html"):
                    continue
                safe_name = name.replace("/", "-").replace("\\", "-")
                safe_name = safe_name[0].upper() + safe_name[1:]
                record = save(f"{prefix}/{safe_name}", package.read(name)) | {"Source": f"nuget:{identifier}/{version}/{name}"}
                files.append(record)
                notices.append(record["Path"])
        key = package_license_key(identifier)
        if key:
            notices.append(official[key])
        if not notices:
            raise ValueError(f"No complete license text was collected for {identifier}")
        repository = tags.get("repository")
        packages.append({
            "Id": identifier, "Version": version,
            "Scope": "LockedKernelGraphIncludingBuildTools" if expected_hash else "ImplicitNativeAotRuntimePack",
            "NuGetSource": metadata.get("source"),
            "LockedContentHash": expected_hash,
            "NuGetContentHashFromMetadata": metadata["contentHash"],
            "ContentHashNote": "NuGet normalized package content hash; archive SHA256/SHA512 are recorded separately",
            "NupkgSha256": sha(package_bytes), "NupkgSha512": zip_sha512,
            "LicenseDeclaration": {"Type": license_type, "Value": license_value},
            "Repository": repository.attrib if repository is not None else None,
            "LicenseFiles": sorted(set(notices)),
        })
    manifest = {
        "Schema": "XFESystem.CSharpLicenses.V1",
        "KernelLockSha256": sha(KERNEL_LOCK.read_bytes()),
        "SourcesLockSha256": sha(SOURCES.read_bytes()),
        "PackageCount": len(packages), "Packages": packages,
        "Files": sorted(files, key=lambda entry: entry["Path"]),
        "ScopeNote": "Build tools are included conservatively; listing a dependency does not imply its code survives NativeAOT trimming or is executed in the guest",
    }
    save("Manifest.json", (json.dumps(manifest, ensure_ascii=False, indent=2) + "\n").encode("utf-8"))
    print(json.dumps({"Packages": len(packages), "Files": len(files), "Output": str(OUTPUT)}, ensure_ascii=False))


if __name__ == "__main__":
    main()