mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
86 lines
5.5 KiB
Python
86 lines
5.5 KiB
Python
"""Exercise AppDir routing with tiny verified stand-in tools, never a game/build.
|
|
|
|
The production provenance pins are asserted; only a PRIVATE script copy uses
|
|
fixture-tool hashes/file URLs. This checks final-byte identity binding after a
|
|
deployment rewrite, mandatory cache verification, and compression failure gates.
|
|
It does not certify a real ELF executable, linuxdeploy, or an AppImage artifact.
|
|
"""
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import subprocess
|
|
import tempfile
|
|
|
|
|
|
def main():
|
|
root = Path(__file__).resolve().parents[2]
|
|
source = (root / "tools/package_appimage.sh").read_text()
|
|
deploy_pin = "c20cd71e3a4e3b80c3483cef793cda3f4e990aca14014d23c544ca3ce1270b4d"
|
|
image_pin = "ed4ce84f0d9caff66f50bcca6ff6f35aae54ce8135408b3fa33abfc3cb384eb0"
|
|
assert deploy_pin in source and image_pin in source
|
|
assert source.index('"$LINUXDEPLOY" --appimage') < source.index('"$PY" "$HELPER" stage-execution') < source.index('ARCH=x86_64 "$APPIMAGETOOL"')
|
|
assert source.index('"$PY" "$HELPER" fetch-pinned') < source.index('"$LINUXDEPLOY" --appimage')
|
|
shared = "a" * 64
|
|
identity = hashlib.sha256(f"psx-execution-v1\nENHANCED\nshared:{shared}\n".encode()).hexdigest()
|
|
manifest = dict(schema=1, profile="ENHANCED", identity=identity,
|
|
shared_contract_sha256=shared, implementations=[], binary_sha256="f" * 64)
|
|
with tempfile.TemporaryDirectory(prefix="psx-appimage-fixture-") as temp:
|
|
work = Path(temp)
|
|
payload, cache, remote, tmp = [work / name for name in ("payload with.space", "cache", "remote", "tmp")]
|
|
for directory in (payload,cache,remote,tmp): directory.mkdir()
|
|
(payload / "assets").mkdir()
|
|
original = b"\x7fELF" + identity.encode() + b"original payload bytes"
|
|
(payload / "Game").write_bytes(original)
|
|
(payload / "Game.execution.json").write_text(json.dumps(manifest))
|
|
(payload / "game.toml").write_text("[game]\nname='Fixture'\n")
|
|
app_run, desktop, icon = [work / name for name in ("AppRun", "io.fixture.Game.desktop", "icon.png")]
|
|
app_run.write_text("#!/bin/sh\nexit 0\n")
|
|
desktop.write_text("[Desktop Entry]\nType=Application\nName=Fixture\nExec=Game\nIcon=io.fixture.Game\nCategories=Game;\n")
|
|
icon.write_bytes(b"\x89PNG\r\n\x1a\nfixture")
|
|
tools = {
|
|
"linuxdeploy-x86_64.AppImage": '#!/bin/bash\nset -eu\nwhile [ "$#" -gt 0 ]; do if [ "$1" = --executable ]; then exe=$2; shift 2; else shift; fi; done\nprintf "deployment rewrite" >> "$exe"\n',
|
|
"appimagetool-x86_64.AppImage": '#!/bin/bash\nset -eu\nout="${@: -1}"\nprintf "fixture squashfs output" > "$out"\n',
|
|
}
|
|
for name,text in tools.items():
|
|
(remote / name).write_text(text)
|
|
# Invalid cached bytes must be replaced through verified fetching.
|
|
(cache / name).write_text("invalid previous cache")
|
|
deploy_url = "https://github.com/linuxdeploy/linuxdeploy/releases/download/1-alpha-20251107-1/linuxdeploy-x86_64.AppImage"
|
|
image_url = "https://github.com/AppImage/appimagetool/releases/download/1.9.1/appimagetool-x86_64.AppImage"
|
|
private = source.replace(deploy_pin,hashlib.sha256((remote / "linuxdeploy-x86_64.AppImage").read_bytes()).hexdigest()).replace(image_pin,hashlib.sha256((remote / "appimagetool-x86_64.AppImage").read_bytes()).hexdigest())
|
|
private = private.replace(deploy_url,(remote / "linuxdeploy-x86_64.AppImage").as_uri()).replace(image_url,(remote / "appimagetool-x86_64.AppImage").as_uri())
|
|
script = work / "package_appimage.sh"
|
|
script.write_text(private)
|
|
environment = dict(os.environ,TMPDIR=str(tmp),SOURCE_DATE_EPOCH="12345")
|
|
def run(output):
|
|
return subprocess.run(["bash",str(script),"--framework",str(root),"--payload",str(payload),"--exe-name","Game","--payload-name","fixture","--app-run",str(app_run),"--desktop-file",str(desktop),"--icon",str(icon),"--tool-cache",str(cache),"--output",str(output),"--jobs","2"],env=environment,text=True,capture_output=True)
|
|
output = work / "candidate.AppImage"
|
|
result = run(output)
|
|
assert result.returncode == 0, result.stdout + result.stderr
|
|
appdir = Path(next(line.split("AppDir: ",1)[1] for line in result.stdout.splitlines() if line.startswith("AppDir: ")))
|
|
final = (appdir / "usr/bin/Game").read_bytes()
|
|
bound = json.loads((appdir / "usr/bin/Game.execution.json").read_text())
|
|
assert final == original + b"deployment rewrite"
|
|
assert bound["binary_sha256"] == hashlib.sha256(final).hexdigest()
|
|
assert bound["binary_sha256"] != manifest["binary_sha256"]
|
|
assert (appdir / "usr/bin/assets").is_symlink()
|
|
assert not (appdir / "usr/share/fixture/Game").exists()
|
|
assert output.is_file() and Path(str(output)+".sha256").is_file()
|
|
for name in tools: assert (cache / name).read_bytes() == (remote / name).read_bytes()
|
|
# A bad build contract must fail before the stand-in compressor runs.
|
|
manifest["identity"] = "0" * 64
|
|
(payload / "Game.execution.json").write_text(json.dumps(manifest))
|
|
failed = work / "failed.AppImage"
|
|
result = run(failed)
|
|
assert result.returncode != 0 and not failed.exists()
|
|
assert "execution staging failed" in result.stderr
|
|
(payload / "wrong.dll").write_bytes(b"Windows cache")
|
|
result = run(failed)
|
|
assert result.returncode != 0 and "Windows binaries/cache" in result.stderr
|
|
print("PASS AppImage routing: verified tools, post-deploy final bytes, no stale hash, fail closed")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|