Files
AnyPS5/tools/validate_patch_catalog.py
T

80 lines
3.4 KiB
Python

import argparse
import json
import sys
import tomllib
from pathlib import Path
import suggest_patches
def unique_json_keys(pairs):
result = {}
for key, value in pairs:
if key in result:
raise ValueError(f"duplicate JSON key {key!r}")
result[key] = value
return result
def validate(directory):
try:
entries = suggest_patches.read_catalog(directory, object_pairs_hook=unique_json_keys)
except RecursionError as error:
raise ValueError("index.json is nested too deeply") from error
seen_ids = {}
errors = 0
for number, entry in enumerate(entries, 1):
patch_name = entry.get("patch") if isinstance(entry, dict) else None
label = f"Catalog entry {number} ({patch_name!r})" if isinstance(patch_name, str) else f"Catalog entry {number}"
try:
if not isinstance(entry, dict):
raise ValueError("entry must be an object")
for field in ("titleId", "contentVersion"):
suggest_patches.single_line(entry.get(field), field)
patch, targets, metadata = suggest_patches.validate_entry(directory, entry)
patch_data = tomllib.loads(patch.read_text(encoding="utf-8"))
if type(patch_data.get("format")) is not int or patch_data["format"] != 1:
raise ValueError("patch file must have format 1")
for name in targets:
suggest_patches.relative_path(name)
identifier = metadata.get("id")
if identifier is not None:
key = (entry["titleId"], entry["contentVersion"], identifier)
for previous, previous_targets in seen_ids.get(key, []):
if not any(name in previous_targets and previous_targets[name] != digest for name, digest in targets.items()):
raise ValueError(f"duplicate id {identifier!r} for {key[0]} {key[1]} may match entry {previous}")
seen_ids.setdefault(key, []).append((number, targets))
except (OSError, ValueError, tomllib.TOMLDecodeError) as error:
print(f"{label}: invalid: {error}", file=sys.stderr)
errors += 1
except RecursionError:
print(f"{label}: invalid: entry or patch file is nested too deeply", file=sys.stderr)
errors += 1
if errors:
print(f"Catalog validation: {errors} invalid of {len(entries)} entries.", file=sys.stderr)
return 2
print(f"Catalog validation: {len(entries)} entries valid.")
return 0
def main(argv=None):
for stream in (sys.stdout, sys.stderr):
if hasattr(stream, "reconfigure"):
stream.reconfigure(encoding="utf-8", errors="backslashreplace")
parser = argparse.ArgumentParser(description="Validate every entry in a local patch catalog without game files")
parser.add_argument("--catalog", required=True, type=Path, help="directory containing index.json and patch files")
args = parser.parse_args(argv)
try:
try:
catalog = args.catalog.resolve()
except RuntimeError as error:
raise ValueError(f"catalog path cannot be resolved: {error}") from error
return validate(catalog)
except (OSError, ValueError, json.JSONDecodeError, tomllib.TOMLDecodeError) as error:
print(f"Catalog validation: {error}", file=sys.stderr)
return 2
if __name__ == "__main__":
raise SystemExit(main())