59 lines
2.9 KiB
Python
59 lines
2.9 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
|
|
|
|
|
|
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"])
|
|
if dungeons != 84 or manastorms != 0:
|
|
raise ValueError(f"Unexpected bundled content: {dungeons} dungeons, {manastorms} Manastorm packs")
|
|
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,
|
|
"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="")
|