244 lines
7.2 KiB
Python
Executable File
244 lines
7.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
from __future__ import annotations
|
|
|
|
import filecmp
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
DEFAULT_BINARY = REPO_ROOT / "build" / "m2pack"
|
|
ENV_BINARY = "M2PACK_BINARY"
|
|
|
|
|
|
def resolve_binary() -> Path:
|
|
env_value = os.environ.get(ENV_BINARY)
|
|
if env_value:
|
|
candidate = Path(env_value).expanduser()
|
|
if candidate.is_file():
|
|
return candidate
|
|
|
|
if DEFAULT_BINARY.is_file():
|
|
return DEFAULT_BINARY
|
|
|
|
raise FileNotFoundError(
|
|
f"m2pack binary not found. Build {DEFAULT_BINARY} or set {ENV_BINARY}."
|
|
)
|
|
|
|
|
|
def run_json(binary: Path, *args: str) -> dict:
|
|
proc = subprocess.run(
|
|
[str(binary), *args, "--json"],
|
|
cwd=REPO_ROOT,
|
|
text=True,
|
|
capture_output=True,
|
|
check=False,
|
|
)
|
|
if proc.returncode != 0:
|
|
detail = proc.stderr.strip() or proc.stdout.strip() or f"exit code {proc.returncode}"
|
|
raise RuntimeError(f"command failed: {' '.join(args)}\n{detail}")
|
|
|
|
stdout = proc.stdout.strip()
|
|
if not stdout:
|
|
return {"ok": True}
|
|
return json.loads(stdout)
|
|
|
|
|
|
def write_asset_tree(root: Path) -> None:
|
|
(root / "locale" / "de").mkdir(parents=True, exist_ok=True)
|
|
(root / "icon").mkdir(parents=True, exist_ok=True)
|
|
(root / "ui").mkdir(parents=True, exist_ok=True)
|
|
|
|
(root / "locale" / "de" / "welcome.txt").write_text(
|
|
"metin2 secure pack test\n",
|
|
encoding="utf-8",
|
|
)
|
|
(root / "ui" / "layout.json").write_text(
|
|
json.dumps(
|
|
{
|
|
"window": "inventory",
|
|
"slots": 90,
|
|
"theme": "headless-e2e",
|
|
},
|
|
indent=2,
|
|
)
|
|
+ "\n",
|
|
encoding="utf-8",
|
|
)
|
|
(root / "icon" / "item.bin").write_bytes(bytes((i * 13) % 256 for i in range(2048)))
|
|
|
|
|
|
def compare_trees(left: Path, right: Path) -> None:
|
|
comparison = filecmp.dircmp(left, right)
|
|
if comparison.left_only or comparison.right_only or comparison.diff_files or comparison.funny_files:
|
|
raise RuntimeError(
|
|
"tree mismatch: "
|
|
f"left_only={comparison.left_only}, "
|
|
f"right_only={comparison.right_only}, "
|
|
f"diff_files={comparison.diff_files}, "
|
|
f"funny_files={comparison.funny_files}"
|
|
)
|
|
|
|
for child in comparison.common_dirs:
|
|
compare_trees(left / child, right / child)
|
|
|
|
|
|
def main() -> int:
|
|
binary = resolve_binary()
|
|
|
|
with tempfile.TemporaryDirectory(prefix="m2pack-headless-") as tmp_dir:
|
|
tmp = Path(tmp_dir)
|
|
assets = tmp / "assets"
|
|
keys = tmp / "keys"
|
|
out_dir = tmp / "out"
|
|
extracted = tmp / "extracted"
|
|
client_header = tmp / "M2PackKeys.h"
|
|
runtime_json = tmp / "runtime-key.json"
|
|
runtime_blob = tmp / "runtime-key.bin"
|
|
archive = out_dir / "client.m2p"
|
|
|
|
assets.mkdir(parents=True, exist_ok=True)
|
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
write_asset_tree(assets)
|
|
|
|
keygen = run_json(binary, "keygen", "--out-dir", str(keys))
|
|
build = run_json(
|
|
binary,
|
|
"build",
|
|
"--input",
|
|
str(assets),
|
|
"--output",
|
|
str(archive),
|
|
"--key",
|
|
str(keys / "master.key"),
|
|
"--sign-secret-key",
|
|
str(keys / "signing.key"),
|
|
"--key-id",
|
|
"7",
|
|
)
|
|
listed = run_json(binary, "list", "--archive", str(archive))
|
|
verify = run_json(
|
|
binary,
|
|
"verify",
|
|
"--archive",
|
|
str(archive),
|
|
"--public-key",
|
|
str(keys / "signing.pub"),
|
|
"--key",
|
|
str(keys / "master.key"),
|
|
)
|
|
diff = run_json(
|
|
binary,
|
|
"diff",
|
|
"--left",
|
|
str(assets),
|
|
"--right",
|
|
str(archive),
|
|
)
|
|
export_client = run_json(
|
|
binary,
|
|
"export-client-config",
|
|
"--key",
|
|
str(keys / "master.key"),
|
|
"--public-key",
|
|
str(keys / "signing.pub"),
|
|
"--key-id",
|
|
"7",
|
|
"--output",
|
|
str(client_header),
|
|
)
|
|
export_runtime_json = run_json(
|
|
binary,
|
|
"export-runtime-key",
|
|
"--key",
|
|
str(keys / "master.key"),
|
|
"--public-key",
|
|
str(keys / "signing.pub"),
|
|
"--key-id",
|
|
"7",
|
|
"--format",
|
|
"json",
|
|
"--output",
|
|
str(runtime_json),
|
|
)
|
|
export_runtime_blob = run_json(
|
|
binary,
|
|
"export-runtime-key",
|
|
"--key",
|
|
str(keys / "master.key"),
|
|
"--public-key",
|
|
str(keys / "signing.pub"),
|
|
"--key-id",
|
|
"7",
|
|
"--format",
|
|
"blob",
|
|
"--output",
|
|
str(runtime_blob),
|
|
)
|
|
extract = run_json(
|
|
binary,
|
|
"extract",
|
|
"--archive",
|
|
str(archive),
|
|
"--output",
|
|
str(extracted),
|
|
"--key",
|
|
str(keys / "master.key"),
|
|
)
|
|
|
|
compare_trees(assets, extracted)
|
|
|
|
runtime_obj = json.loads(runtime_json.read_text(encoding="utf-8"))
|
|
if runtime_obj["key_id"] != 7 or runtime_obj["mapping_name"] != "Local\\M2PackSharedKeys":
|
|
raise RuntimeError("runtime json payload mismatch")
|
|
|
|
if runtime_blob.stat().st_size != 84:
|
|
raise RuntimeError(f"runtime blob size mismatch: {runtime_blob.stat().st_size}")
|
|
|
|
header_text = client_header.read_text(encoding="utf-8")
|
|
if "M2PACK_RUNTIME_MASTER_KEY_REQUIRED = true" not in header_text:
|
|
raise RuntimeError("client header missing runtime enforcement flag")
|
|
if "M2PACK_SIGN_KEY_IDS = { 7 }" not in header_text:
|
|
raise RuntimeError("client header missing key id slot")
|
|
|
|
summary = {
|
|
"ok": True,
|
|
"binary": str(binary),
|
|
"file_count": build["file_count"],
|
|
"listed_entries": len(listed["entries"]),
|
|
"verify_ok": verify["ok"],
|
|
"diff_changed": len(diff["changed"]),
|
|
"diff_added": len(diff["added"]),
|
|
"diff_removed": len(diff["removed"]),
|
|
"extract_entry_count": extract["entry_count"],
|
|
"runtime_key_id": runtime_obj["key_id"],
|
|
"runtime_blob_size": runtime_blob.stat().st_size,
|
|
"artifacts_root": str(tmp),
|
|
"steps": {
|
|
"keygen": keygen["ok"],
|
|
"build": build["ok"],
|
|
"list": listed["ok"],
|
|
"verify": verify["ok"],
|
|
"diff": diff["ok"],
|
|
"export_client_config": export_client["ok"],
|
|
"export_runtime_json": export_runtime_json["ok"],
|
|
"export_runtime_blob": export_runtime_blob["ok"],
|
|
"extract": extract["ok"],
|
|
},
|
|
}
|
|
print(json.dumps(summary, indent=2))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
raise SystemExit(main())
|
|
except Exception as exc:
|
|
print(str(exc), file=sys.stderr)
|
|
raise SystemExit(1)
|