Add runtime validation gate and known issues baseline
This commit is contained in:
243
scripts/headless_e2e.py
Executable file
243
scripts/headless_e2e.py
Executable file
@@ -0,0 +1,243 @@
|
||||
#!/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)
|
||||
Reference in New Issue
Block a user