"""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"(?Pgovoplan-[a-z0-9-]+)(?:\[[a-z0-9_,.-]+\])? @ " r"(?Phttps://\S+)#sha256=(?P[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