Files
m2pack-secure/scripts/headless_e2e.py

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)