90 lines
3.3 KiB
Python
Executable File
90 lines
3.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Validate the exact three-file GitHub Release inventory before extraction."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import pathlib
|
|
import re
|
|
import sys
|
|
|
|
|
|
ARCHITECTURES = ("arm64", "x86_64")
|
|
|
|
|
|
def sha256(path: pathlib.Path) -> str:
|
|
digest = hashlib.sha256()
|
|
with path.open("rb") as source:
|
|
for chunk in iter(lambda: source.read(1024 * 1024), b""):
|
|
digest.update(chunk)
|
|
return digest.hexdigest()
|
|
|
|
|
|
def expected_names(version: str) -> tuple[list[str], str]:
|
|
archive_names = [
|
|
f"AgentPetCompanion-{version}-macos-{architecture}.zip"
|
|
for architecture in ARCHITECTURES
|
|
]
|
|
return archive_names, f"AgentPetCompanion-{version}-SHA256SUMS.txt"
|
|
|
|
|
|
def validate(directory: pathlib.Path, version: str) -> None:
|
|
archive_names, checksum_name = expected_names(version)
|
|
expected = set(archive_names) | {checksum_name}
|
|
actual: set[str] = set()
|
|
for item in directory.iterdir():
|
|
if item.is_symlink() or not item.is_file():
|
|
raise ValueError(f"artifact inventory contains a non-regular file: {item.name!r}")
|
|
actual.add(item.name)
|
|
if actual != expected:
|
|
missing = sorted(expected - actual)
|
|
extra = sorted(actual - expected)
|
|
raise ValueError(
|
|
f"artifact inventory must contain exactly three files; missing={missing}, extra={extra}"
|
|
)
|
|
|
|
checksum_path = directory / checksum_name
|
|
if checksum_path.stat().st_size > 16 * 1024:
|
|
raise ValueError("checksum file exceeds its size bound")
|
|
lines = checksum_path.read_text(encoding="ascii").splitlines()
|
|
if len(lines) != 2 or any(not line for line in lines):
|
|
raise ValueError("checksum inventory must contain exactly two archive lines")
|
|
parsed: dict[str, str] = {}
|
|
pattern = re.compile(r"^([0-9a-f]{64}) ([A-Za-z0-9._-]+)$")
|
|
for line in lines:
|
|
match = pattern.fullmatch(line)
|
|
if match is None:
|
|
raise ValueError("checksum inventory contains a malformed line")
|
|
digest, name = match.groups()
|
|
if name in parsed:
|
|
raise ValueError("checksum inventory contains a duplicate filename")
|
|
parsed[name] = digest
|
|
if set(parsed) != set(archive_names):
|
|
raise ValueError("checksum inventory must cover only the two App ZIPs")
|
|
for name in archive_names:
|
|
if sha256(directory / name) != parsed[name]:
|
|
raise ValueError(f"checksum mismatch: {name}")
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--directory", required=True, type=pathlib.Path)
|
|
parser.add_argument("--version", required=True)
|
|
arguments = parser.parse_args()
|
|
if not re.fullmatch(r"[0-9]+(?:[.][0-9]+){2}", arguments.version):
|
|
parser.error("--version must be a three-component semantic version")
|
|
if not arguments.directory.is_dir() or arguments.directory.is_symlink():
|
|
parser.error("--directory must be a regular directory")
|
|
try:
|
|
validate(arguments.directory, arguments.version)
|
|
except (OSError, UnicodeError, ValueError) as error:
|
|
print(f"GitHub Release metadata validation failed: {error}", file=sys.stderr)
|
|
return 1
|
|
print("GitHub Release three-file inventory and two-entry checksum metadata ok")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|