feat(release): build verified full-registry catalog candidates
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
"""Strict source identities for immutable, registry-backed catalog entries."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
import re
|
||||
from urllib.parse import unquote, urlsplit
|
||||
|
||||
|
||||
_SHA256 = re.compile(r"[0-9a-f]{64}\Z")
|
||||
_COMMIT = re.compile(r"[0-9a-f]{40}(?:[0-9a-f]{24})?\Z")
|
||||
_REPO = re.compile(r"govoplan-[a-z0-9-]+\Z")
|
||||
_VERSION = re.compile(r"\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?\Z")
|
||||
_PYTHON = re.compile(
|
||||
r"(?P<name>govoplan-[a-z0-9-]+)(?:\[[a-z0-9_,.-]+\])? @ "
|
||||
r"(?P<url>https://\S+)#sha256=(?P<digest>[0-9a-f]{64})\Z"
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RegistrySource:
|
||||
repository: str
|
||||
version: str
|
||||
commit: str
|
||||
tag_object: str
|
||||
|
||||
|
||||
def registry_artifact_conflicts(entries: list[object]) -> tuple[str, ...]:
|
||||
"""Allow repeated module projections only when package artifacts agree."""
|
||||
observed: dict[tuple[str, str], str] = {}
|
||||
issues = []
|
||||
for entry in entries:
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
try:
|
||||
source = registry_entry_source(entry)
|
||||
except ValueError as exc:
|
||||
issues.append(str(exc))
|
||||
continue
|
||||
if source is None:
|
||||
continue
|
||||
for kind, identity in entry["artifact_integrity"].items():
|
||||
if kind not in {"python", "webui"}:
|
||||
continue
|
||||
key = (source.repository, kind)
|
||||
encoded = json.dumps(identity, sort_keys=True, separators=(",", ":"))
|
||||
if observed.setdefault(key, encoded) != encoded:
|
||||
issues.append(f"conflicting {kind} registry artifacts for {source.repository}")
|
||||
return tuple(issues)
|
||||
|
||||
|
||||
def registry_entry_source(entry: dict[str, object]) -> RegistrySource | None:
|
||||
"""Admit registry refs only with a complete matching artifact/source binding.
|
||||
|
||||
Git-backed catalogs keep their existing validation path. An HTTPS Python
|
||||
requirement cannot masquerade as a source-only/non-Python entry when its
|
||||
registry provenance is missing or inconsistent.
|
||||
"""
|
||||
|
||||
ref = entry.get("python_ref")
|
||||
if not isinstance(ref, str) or " @ https://" not in ref:
|
||||
return None
|
||||
match = _PYTHON.fullmatch(ref)
|
||||
source = entry.get("source")
|
||||
version = entry.get("version")
|
||||
package = entry.get("python_package")
|
||||
integrity = entry.get("artifact_integrity")
|
||||
if (
|
||||
match is None
|
||||
or not isinstance(source, dict)
|
||||
or not isinstance(integrity, dict)
|
||||
or not isinstance(version, str)
|
||||
or _VERSION.fullmatch(version) is None
|
||||
or package != match.group("name")
|
||||
):
|
||||
raise ValueError("registry entry has no complete package/source identity")
|
||||
repo = source.get("repository")
|
||||
commit = source.get("commit")
|
||||
tag_object = source.get("tag_object_sha")
|
||||
if (
|
||||
not isinstance(repo, str)
|
||||
or _REPO.fullmatch(repo) is None
|
||||
or repo != package
|
||||
or source.get("tag") != f"v{version}"
|
||||
or not isinstance(commit, str)
|
||||
or _COMMIT.fullmatch(commit) is None
|
||||
or not isinstance(tag_object, str)
|
||||
or _COMMIT.fullmatch(tag_object) is None
|
||||
):
|
||||
raise ValueError("registry entry has invalid immutable tag provenance")
|
||||
python = _artifact(
|
||||
integrity.get("python"), ref=ref, package=package, version=version,
|
||||
commit=commit,
|
||||
)
|
||||
if python["url"] != match.group("url") or python["sha256"] != match.group("digest"):
|
||||
raise ValueError("registry Python ref differs from its artifact identity")
|
||||
webui_package = entry.get("webui_package")
|
||||
webui_ref = entry.get("webui_ref")
|
||||
if bool(webui_package) != bool(webui_ref):
|
||||
raise ValueError("registry WebUI package and ref must be declared together")
|
||||
if webui_package:
|
||||
if not isinstance(webui_package, str) or re.fullmatch(
|
||||
r"@govoplan/[a-z0-9-]+-webui", webui_package
|
||||
) is None or not isinstance(webui_ref, str):
|
||||
raise ValueError("registry WebUI package identity is malformed")
|
||||
if webui_package != f"@govoplan/{repo.removeprefix('govoplan-')}-webui":
|
||||
raise ValueError("registry WebUI package belongs to another source repository")
|
||||
webui = _artifact(
|
||||
integrity.get("webui"), ref=webui_ref, package=webui_package,
|
||||
version=version, commit=commit,
|
||||
)
|
||||
if webui_ref != webui["url"]:
|
||||
raise ValueError("registry WebUI ref differs from its artifact identity")
|
||||
sri = webui.get("integrity")
|
||||
try:
|
||||
valid_sri = isinstance(sri, str) and sri.startswith("sha512-") and len(
|
||||
base64.b64decode(sri[7:], validate=True)
|
||||
) == 64
|
||||
except ValueError:
|
||||
valid_sri = False
|
||||
if not valid_sri:
|
||||
raise ValueError("registry WebUI artifact needs a SHA-512 integrity identity")
|
||||
elif "webui" in integrity:
|
||||
raise ValueError("registry entry carries an unexpected WebUI artifact")
|
||||
return RegistrySource(repo, version, commit, tag_object)
|
||||
|
||||
|
||||
def _artifact(
|
||||
value: object, *, ref: str, package: str, version: str, commit: str,
|
||||
) -> dict[str, object]:
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError("registry entry is missing artifact integrity")
|
||||
url = value.get("url")
|
||||
filename = value.get("filename")
|
||||
digest = value.get("sha256")
|
||||
size = value.get("size")
|
||||
parsed = urlsplit(url) if isinstance(url, str) else None
|
||||
url_filename = unquote(parsed.path.rsplit("/", 1)[-1]) if parsed else ""
|
||||
decoded_parts = unquote(parsed.path).split("/") if parsed else []
|
||||
expected_filenames = {url_filename}
|
||||
expected_path = [package, version, url_filename]
|
||||
if package.startswith("@govoplan/"):
|
||||
# npm pack includes the scope in its local filename; the registry's
|
||||
# immutable download URL uses the unscoped package basename.
|
||||
expected_filenames = {
|
||||
f"govoplan-{package.split('/', 1)[1]}-{version}.tgz",
|
||||
} if url_filename == f"{package.split('/', 1)[1]}-{version}.tgz" else set()
|
||||
expected_path = [*package.split("/"), "-", version, url_filename]
|
||||
if (
|
||||
parsed is None
|
||||
or parsed.scheme != "https"
|
||||
or not parsed.netloc
|
||||
or parsed.username is not None
|
||||
or parsed.password is not None
|
||||
or parsed.fragment
|
||||
or parsed.query
|
||||
or any(part in {".", ".."} for part in unquote(parsed.path).split("/"))
|
||||
or "%" in unquote(parsed.path)
|
||||
or "\\" in unquote(parsed.path)
|
||||
or any(ord(character) < 32 for character in url)
|
||||
or decoded_parts[-len(expected_path):] != expected_path
|
||||
or not isinstance(filename, str)
|
||||
or re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._+-]{0,254}", filename) is None
|
||||
or filename not in expected_filenames
|
||||
or not isinstance(digest, str)
|
||||
or _SHA256.fullmatch(digest) is None
|
||||
or not isinstance(size, int)
|
||||
or isinstance(size, bool)
|
||||
or not 0 < size <= 512 * 1024 * 1024
|
||||
or value.get("ref") != ref
|
||||
or value.get("registry_identity") != f"{package}@{version}"
|
||||
or value.get("git_ref") != f"v{version}"
|
||||
or value.get("source_commit") != commit
|
||||
):
|
||||
raise ValueError("registry artifact ref, version, bytes, or source binding is inconsistent")
|
||||
return value
|
||||
Reference in New Issue
Block a user