Files
healer-man/scripts/content/verify-android-package.py
T
2026-09-12 11:07:25 -04:00

70 lines
3.5 KiB
Python

"""Verify the packaged bytes, including Android asset-name/compression behavior."""
import argparse
import gzip
import hashlib
import json
from pathlib import Path
from zipfile import ZIP_STORED, ZipFile
CONTENT_POLICY = json.loads(
(Path(__file__).resolve().parents[2] / "shared" / "content-policy.json").read_text(encoding="utf-8")
)
def verify_package(apk: Path) -> dict:
with ZipFile(apk) as archive:
manifest = json.loads(archive.read("assets/public/content/bootstrap-manifest.json"))
checked = set()
object_bytes = 0
for logical_path, entry in manifest["files"].items():
stored = entry.get("download", entry)
if stored["url"] in checked:
continue
checked.add(stored["url"])
name = "assets/public" + stored["url"]
data = archive.read(name)
if len(data) != stored["bytes"] or hashlib.sha256(data).hexdigest() != stored["sha256"]:
raise ValueError(f"Packaged object failed verification: {logical_path}")
object_bytes += len(data)
if "download" in entry:
if archive.getinfo(name).compress_type != ZIP_STORED:
raise ValueError(f"Android recompressed an already compressed object: {name}")
data = gzip.decompress(data)
if len(data) != entry["bytes"] or hashlib.sha256(data).hexdigest() != entry["sha256"]:
raise ValueError(f"Expanded object failed verification: {logical_path}")
dungeons = sum(pack["id"].startswith("dungeon:") and pack["bundledOnAndroid"] for pack in manifest["packs"])
expected_objects = {"assets/public" + url for url in checked}
actual_objects = {name for name in archive.namelist() if name.startswith("assets/public/content/bundled/") and not name.endswith("/")}
if actual_objects != expected_objects:
raise ValueError("APK includes missing or unreferenced bundled objects.")
manastorms = sum(pack["id"].startswith("manastorm:") and pack["bundledOnAndroid"] for pack in manifest["packs"])
arenas = sum(pack["id"] == "core:arenas" for pack in manifest["packs"])
pack_ids = sorted(pack["id"] for pack in manifest["packs"])
expected_pack_ids = sorted(CONTENT_POLICY["bootstrapPackIds"])
if pack_ids != expected_pack_ids or dungeons != 1 or manastorms != 0 or arenas != 0:
raise ValueError(
f"Unexpected bundled content: {dungeons} dungeons, {manastorms} Manastorm packs, {arenas} arena packs"
)
if object_bytes > CONTENT_POLICY["maxBootstrapStoredBytes"]:
raise ValueError("APK bootstrap content exceeds the stored-byte budget.")
return {
"apk": str(apk.resolve()), "apkBytes": apk.stat().st_size,
"manifestVersion": manifest["version"], "logicalFiles": len(manifest["files"]),
"verifiedUniqueObjects": len(checked), "storedObjectBytes": object_bytes,
"bundledDungeons": dungeons, "bundledManastormPacks": manastorms, "bundledArenaPacks": arenas,
"allOriginalHashesMatch": True,
}
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("apk", type=Path)
parser.add_argument("--report", type=Path)
args = parser.parse_args()
result = json.dumps(verify_package(args.apk), indent=2) + "\n"
if args.report:
args.report.parent.mkdir(parents=True, exist_ok=True)
args.report.write_text(result, encoding="utf-8")
print(result, end="")