mirror of
https://github.com/valentderah/totk-pc-recomp.git
synced 2026-11-11 01:41:02 +00:00
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
95 lines
3.4 KiB
Python
95 lines
3.4 KiB
Python
"""Refuse files that look like game content, keys or generated game code.
|
|
|
|
Runs in CI on every push and pull request, and locally as the pre-commit hook
|
|
(.githooks/pre-commit). With no arguments it checks every tracked file; with
|
|
--staged it checks the files staged for the next commit.
|
|
|
|
Standard library only, so it runs before any environment is set up.
|
|
"""
|
|
|
|
import argparse
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
MAX_BYTES = 1024 * 1024
|
|
|
|
# Switch containers, executables and game data formats.
|
|
BLOCKED_SUFFIXES = {
|
|
".xci", ".xcz", ".nsp", ".nsz", ".nca", ".ncz", ".tik", ".cert",
|
|
".nso", ".nro", ".nrr", ".npdm", ".keys",
|
|
".bfres", ".bntx", ".bnsh", ".sarc", ".szs", ".zs", ".pack", ".byml",
|
|
".bgyml", ".msbt", ".bars", ".bwav", ".bfwav", ".bfstm", ".bfttf",
|
|
".bfotf", ".sharcb", ".webm",
|
|
}
|
|
|
|
# ExeFS modules carry no extension.
|
|
BLOCKED_NAMES = {"main", "sdk", "rtld", "subsdk0", "subsdk1", "main.npdm"}
|
|
|
|
# Written by the code generator into every file it emits.
|
|
GENERATED_MARKER = "TOTK_RECOMP_" + "GENERATED_FROM_GAME_CODE"
|
|
|
|
# A prod.keys / title.keys line: name containing key, kek or seed, then hex.
|
|
KEY_LINE = re.compile(
|
|
r"^\s*[A-Za-z0-9_]*(key|kek|seed)[A-Za-z0-9_]*\s*=\s*[0-9A-Fa-f]{32,}\s*$",
|
|
re.MULTILINE,
|
|
)
|
|
|
|
|
|
def list_files(staged: bool) -> list[Path]:
|
|
if staged:
|
|
cmd = ["git", "diff", "--cached", "--name-only", "--diff-filter=ACMR", "-z"]
|
|
else:
|
|
# Tracked files plus untracked ones that are not ignored: everything a commit could pick up.
|
|
cmd = ["git", "ls-files", "-z", "--cached", "--others", "--exclude-standard"]
|
|
out = subprocess.run(cmd, check=True, capture_output=True).stdout
|
|
return [Path(p) for p in out.decode("utf-8").split("\0") if p]
|
|
|
|
|
|
def problems_for(path: Path, allow_large: bool = False) -> list[str]:
|
|
"""allow_large: files over MAX_BYTES (toolchain binaries in a release folder) are checked for the generated marker
|
|
only, not refused."""
|
|
found = []
|
|
if path.suffix.lower() in BLOCKED_SUFFIXES:
|
|
found.append(f"blocked file type {path.suffix}")
|
|
if path.name.lower() in BLOCKED_NAMES:
|
|
found.append("name of a Switch executable module")
|
|
if not path.is_file():
|
|
return found
|
|
size = path.stat().st_size
|
|
if size > MAX_BYTES and allow_large:
|
|
if GENERATED_MARKER.encode() in path.read_bytes():
|
|
found.append("generated from game code")
|
|
return found
|
|
if size > MAX_BYTES:
|
|
found.append(f"{size // 1024} KiB, over the {MAX_BYTES // 1024} KiB limit")
|
|
return found
|
|
data = path.read_bytes()
|
|
if GENERATED_MARKER.encode() in data:
|
|
found.append("generated from game code")
|
|
if KEY_LINE.search(data.decode("utf-8", errors="ignore")):
|
|
found.append("contains a key in prod.keys format")
|
|
return found
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
parser.add_argument("--staged", action="store_true", help="check staged files only")
|
|
args = parser.parse_args()
|
|
|
|
failed = False
|
|
for path in list_files(args.staged):
|
|
for problem in problems_for(path):
|
|
print(f"{path}: {problem}")
|
|
failed = True
|
|
if failed:
|
|
print("\nGame content, keys and generated game code never go into the repository.")
|
|
print("See CONTRIBUTING.md.")
|
|
return 1
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|