Spaces:
Runtime error
Runtime error
File size: 6,326 Bytes
f36b499 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 | """Artifact source of truth for OpenRange builder templates.
Computes and verifies SHA256 hashes of template files to ensure they
haven't been modified unexpectedly.
Usage::
python -m open_range.artifact_verify update # regenerate hashes
python -m open_range.artifact_verify verify # check hashes match
python -m open_range.artifact_verify # defaults to verify
"""
from __future__ import annotations
import argparse
import hashlib
import json
import sys
from pathlib import Path
# Default templates directory relative to this file
_DEFAULT_TEMPLATES_DIR = Path(__file__).parent / "builder" / "templates"
_DEFAULT_HASHES_FILE = _DEFAULT_TEMPLATES_DIR / ".hashes.json"
def _hash_file(path: Path) -> str:
"""Compute SHA256 hex digest of a file."""
h = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(8192), b""):
h.update(chunk)
return h.hexdigest()
def _find_templates(templates_dir: Path) -> list[Path]:
"""Find all template files in the templates directory.
Returns sorted list of template file paths (excludes .hashes.json
and hidden files).
"""
if not templates_dir.is_dir():
return []
templates = []
for p in sorted(templates_dir.iterdir()):
if p.is_file() and not p.name.startswith("."):
templates.append(p)
return templates
def compute_hashes(templates_dir: Path | None = None) -> dict[str, str]:
"""Compute SHA256 hashes for all template files.
Args:
templates_dir: Directory containing templates. Defaults to
``src/open_range/builder/templates/``.
Returns:
Dict mapping filename to SHA256 hex digest.
"""
templates_dir = templates_dir or _DEFAULT_TEMPLATES_DIR
hashes: dict[str, str] = {}
for path in _find_templates(templates_dir):
hashes[path.name] = _hash_file(path)
return hashes
def update(
templates_dir: Path | None = None,
hashes_file: Path | None = None,
) -> dict[str, str]:
"""Regenerate the hash manifest file.
Args:
templates_dir: Directory containing templates.
hashes_file: Path to write the JSON hash manifest.
Returns:
The computed hashes dict.
"""
templates_dir = templates_dir or _DEFAULT_TEMPLATES_DIR
hashes_file = hashes_file or (templates_dir / ".hashes.json")
hashes = compute_hashes(templates_dir)
hashes_file.parent.mkdir(parents=True, exist_ok=True)
with open(hashes_file, "w") as f:
json.dump(hashes, f, indent=2, sort_keys=True)
f.write("\n")
return hashes
def verify(
templates_dir: Path | None = None,
hashes_file: Path | None = None,
) -> dict[str, str]:
"""Verify current templates match stored hashes.
Args:
templates_dir: Directory containing templates.
hashes_file: Path to the JSON hash manifest.
Returns:
Dict mapping filename to status string:
- "ok" if hash matches
- "modified" if hash differs
- "missing" if file in manifest but not on disk
- "new" if file on disk but not in manifest
Raises:
FileNotFoundError: If the hashes file does not exist.
"""
templates_dir = templates_dir or _DEFAULT_TEMPLATES_DIR
hashes_file = hashes_file or (templates_dir / ".hashes.json")
if not hashes_file.exists():
raise FileNotFoundError(
f"Hash manifest not found: {hashes_file}. "
f"Run 'python -m open_range.artifact_verify update' first."
)
with open(hashes_file) as f:
stored_hashes: dict[str, str] = json.load(f)
current_hashes = compute_hashes(templates_dir)
results: dict[str, str] = {}
# Check files in manifest
for name, stored_hash in stored_hashes.items():
if name not in current_hashes:
results[name] = "missing"
elif current_hashes[name] != stored_hash:
results[name] = "modified"
else:
results[name] = "ok"
# Check for new files not in manifest
for name in current_hashes:
if name not in stored_hashes:
results[name] = "new"
return results
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
# ANSI color codes
_GREEN = "\033[32m"
_RED = "\033[31m"
_YELLOW = "\033[33m"
_BOLD = "\033[1m"
_RESET = "\033[0m"
def main() -> None:
parser = argparse.ArgumentParser(
description="Verify or update OpenRange template artifact hashes",
)
parser.add_argument(
"action",
nargs="?",
default="verify",
choices=["verify", "update"],
help="Action to perform (default: verify)",
)
parser.add_argument(
"--templates-dir",
type=Path,
default=None,
help="Templates directory (default: src/open_range/builder/templates/)",
)
args = parser.parse_args()
if args.action == "update":
hashes = update(templates_dir=args.templates_dir)
print(f"{_GREEN}Updated hash manifest with {len(hashes)} files:{_RESET}")
for name, h in sorted(hashes.items()):
print(f" {name}: {h[:16]}...")
sys.exit(0)
# verify
try:
results = verify(templates_dir=args.templates_dir)
except FileNotFoundError as exc:
print(f"{_RED}{exc}{_RESET}", file=sys.stderr)
sys.exit(1)
any_issues = False
for name, status in sorted(results.items()):
if status == "ok":
print(f" {_GREEN}OK{_RESET} {name}")
elif status == "modified":
print(f" {_RED}MODIFIED{_RESET} {name}")
any_issues = True
elif status == "missing":
print(f" {_RED}MISSING{_RESET} {name}")
any_issues = True
elif status == "new":
print(f" {_YELLOW}NEW{_RESET} {name}")
any_issues = True
if any_issues:
print(
f"\n{_RED}Verification failed.{_RESET} "
f"Run 'python -m open_range.artifact_verify update' to refresh."
)
sys.exit(1)
else:
print(f"\n{_GREEN}All templates verified.{_RESET}")
sys.exit(0)
if __name__ == "__main__":
main()
|