Add verified multipart Linux and Windows GitHub releases

This commit is contained in:
raptorreplica committed 2026-10-20 21:37:49 +11:00
1 parent 443d77852b
commit d1f5b7e5a2
8 files changed
+1152 -1

No files matched your search

+323
View File
@@ -0,0 +1,323 @@
"""Small real-ZIP release fixtures; no production archives or game build."""
from pathlib import Path
import json
import os
import tempfile
import unittest
from unittest.mock import patch
import zipfile
import export_release as release
COMMIT = "a" * 40
class ReleaseExportTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
self.package = self.root / "package"
self.package.mkdir()
fixtures = {"data/INDEX": b"10000 4 misc/pic\n", "data/misc/pic": b"base",
"data/hd/jobs/reviewed/derived/picture.png": b"accepted artwork",
"data/hd/jobs/reviewed/proofs/actual-proof.json": b"{}",
"bin/raptor-rust": b"Linux exe", "bin/raptor-rust.exe": b"Windows exe",
"bin/SDL2.dll": b"Windows SDL", "lib/libSDL2-2.0.so.0": b"Linux SDL",
"run.sh": b"#!/bin/sh\n", "run.cmd": b"@echo off\n",
"mods/example/mod.toml": b'id = "example"\n',
"mods/example/art/sd.png": b"SD", "mods/example/art/hd.png": b"HD",
"mods/example/sounds/fire.wav": b"sound", "mods/example/music/track.wav": b"music",
"mods/example/art/source/draft.png": b"private source art",
"src/main.rs": b"fn main() {}", "docs/modding.md": b"API docs",
"target/cache": b"build cache", ".reference/private": b"private review"}
for name, value in fixtures.items():
self.put(name, value)
(self.package / "run.sh").chmod(0o755)
self.put_json("data/hd/hd.json", {"sequences": [{"frames": [{"file": "jobs/reviewed/derived/picture.png"}]}]})
data = {p.relative_to(self.package).as_posix(): release.digest(p)
for p in (self.package / "data").rglob("*") if p.is_file()}
self.put_json("runtime-manifest.json", {"files": data, "approved_hd_frames": 1,
"binary_sha256": release.digest(self.package / "bin/raptor-rust"),
"data_files": len(data), "data_bytes": sum((self.package / n).stat().st_size for n in data),
"catalog_entries": 1})
self.put_json("runtime-windows-manifest.json", {"files": {
n: release.digest(self.package / n) for n in ["bin/raptor-rust.exe", "bin/SDL2.dll", "run.cmd"]},
"approved_hd_frames": 1})
patch.object(release, "require_clean_workspace").start()
self.mock_commit = patch.object(release, "git_commit", return_value=COMMIT).start()
self.addCleanup(patch.stopall)
def put(self, name, value):
path = self.package / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(value)
def put_json(self, name, value):
self.put(name, (json.dumps(value) + "\n").encode())
def plan(self):
value = release.build_plan(self.package, COMMIT)
path = self.root / "plan.json"
release.write_new_json(path, value)
return path, value
def test_complete_data_mods_audio_and_platform_split(self):
_, plan = self.plan()
data, linux, windows = plan["assets"]
self.assertIn("data/hd/jobs/reviewed/derived/picture.png", {r["name"] for r in data["files"]})
self.assertIn("data/hd/jobs/reviewed/proofs/actual-proof.json", {r["name"] for r in data["files"]})
for asset in (linux, windows):
names = {r["name"] for r in asset["files"]}
self.assertTrue({"mods/example/mod.toml", "mods/example/art/sd.png", "mods/example/art/hd.png",
"mods/example/sounds/fire.wav", "mods/example/music/track.wav", "src/main.rs"} <= names)
self.assertFalse({"target/cache", ".reference/private", "mods/example/art/source/draft.png"} & names)
self.assertIn("bin/raptor-rust", {r["name"] for r in linux["files"]})
self.assertNotIn("bin/raptor-rust.exe", {r["name"] for r in linux["files"]})
self.assertNotIn("lib/libSDL2-2.0.so.0", {r["name"] for r in windows["files"]})
self.assertEqual(plan["bundled_mods"], 1)
def test_partition_covers_every_file_once_under_budget(self):
rows = [{"name": str(i), "bytes": i} for i in range(1, 7)]
groups = release.partition(rows, 7)
self.assertEqual([r for group in groups for r in group], rows)
self.assertTrue(all(sum(r["bytes"] for r in group) <= 7 for group in groups))
with self.assertRaises(ValueError):
release.partition(rows, 5)
def test_real_zip_relocation_complete_hashes_and_linux_permissions(self):
path, plan = self.plan()
relocated = self.root / "Portable Ω folder"
for asset in plan["assets"]:
archive = self.root / asset["name"]
receipt = release.write_asset(path, asset["name"], archive)
self.assertEqual(receipt["status"], "ZIP_CRC_AND_COMPLETE_PAYLOAD_SHA256_PASS")
with zipfile.ZipFile(archive) as packed:
self.assertIsNone(packed.testzip())
packed.extractall(relocated)
if asset["kind"] == "linux":
self.assertEqual((packed.getinfo("RustRaptor/run.sh").external_attr >> 16) & 0o777, 0o755)
for row in asset["files"]:
self.assertEqual(release.digest(relocated / "RustRaptor" / row["name"]), row["sha256"])
def test_modified_same_size_data_is_rejected(self):
path, plan = self.plan()
self.put("data/misc/pic", b"evil")
with self.assertRaisesRegex(ValueError, "differs from plan"):
release.write_asset(path, plan["assets"][0]["name"], self.root / plan["assets"][0]["name"])
def test_manifest_or_commit_change_rejected(self):
path, plan = self.plan()
self.mock_commit.return_value = "b" * 40
with self.assertRaisesRegex(ValueError, "changed after"):
release.write_asset(path, plan["assets"][0]["name"], self.root / plan["assets"][0]["name"])
def test_no_overwrite_or_unplanned_output_name(self):
path, plan = self.plan()
asset = plan["assets"][0]["name"]
with self.assertRaisesRegex(ValueError, "filename"):
release.write_asset(path, asset, self.root / "wrong.zip")
output = self.root / asset
output.write_bytes(b"existing export")
with self.assertRaisesRegex(ValueError, "already exists"):
release.write_asset(path, asset, output)
self.assertEqual(output.read_bytes(), b"existing export")
def test_symlink_and_fifo_sources_are_rejected_without_blocking(self):
outside = self.root / "outside"
outside.write_bytes(b"base")
source = self.package / "data/misc/pic"
source.unlink()
source.symlink_to(outside)
with self.assertRaises(OSError):
with release.opened_source(self.package, "data/misc/pic", 4):
self.fail("symlink source was accepted")
source.unlink()
os.mkfifo(source)
with self.assertRaisesRegex(ValueError, "regular file"):
with release.opened_source(self.package, "data/misc/pic", 0):
self.fail("FIFO source was accepted")
def test_open_descriptor_growth_is_rejected(self):
with self.assertRaisesRegex(ValueError, "changed while"):
with release.opened_source(self.package, "data/misc/pic", 4) as (stream, _):
with (self.package / "data/misc/pic").open("ab") as growing:
growing.write(b"extra")
self.assertEqual(stream.read(5), b"basee")
def test_checksums_require_complete_matching_actual_receipts(self):
path, plan = self.plan()
receipts = self.root / "receipts"
receipts.mkdir()
with self.assertRaises(FileNotFoundError):
release.checksums(path, receipts, self.root / "SHA256SUMS")
for asset in plan["assets"]:
result = release.write_asset(path, asset["name"], self.root / asset["name"])
release.write_new_json(receipts / (asset["name"] + ".json"), result)
release.checksums(path, receipts, self.root / "SHA256SUMS")
self.assertEqual(len((self.root / "SHA256SUMS").read_text().splitlines()), len(plan["assets"]))
record = receipts / (plan["assets"][0]["name"] + ".json")
value = release.read_json(record)
value["payload_sha256"] = {}
record.write_text(json.dumps(value))
with self.assertRaisesRegex(ValueError, "mismatched"):
release.checksums(path, receipts, self.root / "bad-sums")
def test_active_hd_missing_from_manifest_rejected(self):
self.put_json("data/hd/hd.json", {"sequences": [{"frames": [{"file": "missing.png"}]}]})
manifest = release.read_json(self.package / "runtime-manifest.json")
manifest["files"]["data/hd/hd.json"] = release.digest(self.package / "data/hd/hd.json")
manifest["data_bytes"] = sum((self.package / name).stat().st_size for name in manifest["files"])
self.put_json("runtime-manifest.json", manifest)
with self.assertRaises(FileNotFoundError):
release.build_plan(self.package, COMMIT)
def test_complete_physical_data_must_match_manifest(self):
self.put("data/forgotten.wav", b"required sound")
with self.assertRaisesRegex(ValueError, "complete physical"):
self.plan()
def test_nonportable_paths_rejected(self):
for name in ("../outside", "RustRaptor/a/../b", "RustRaptor/C:evil", "RustRaptor/a\\b", "RustRaptor/CON.txt", "RustRaptor/a."):
with self.subTest(name=name), self.assertRaises(ValueError):
release.safe_member(name)
def test_release_manifest_binds_all_payload_and_snapshot(self):
path, plan = self.plan()
receipts = self.root / "receipts"
receipts.mkdir()
for asset in plan["assets"]:
result = release.write_asset(path, asset["name"], self.root / asset["name"])
release.write_new_json(receipts / (asset["name"] + ".json"), result)
manifest = self.root / "release-manifest.json"
release.checksums(path, receipts, self.root / "SHA256SUMS", manifest, "v0.1.0-preview.test")
actual = release.read_json(manifest)
self.assertEqual(actual["checkpoint"], plan["snapshot"])
self.assertEqual(actual["tag"], "v0.1.0-preview.test")
self.assertEqual([a["files"] for a in actual["assets"]], [a["files"] for a in plan["assets"]])
def test_data_directory_symlink_is_rejected(self):
outside = self.root / "outside-data"
outside.mkdir()
(outside / "required.png").write_bytes(b"required")
(self.package / "data/linked").symlink_to(outside, target_is_directory=True)
with self.assertRaisesRegex(ValueError, "symlink"):
self.plan()
def test_mod_directory_symlink_is_rejected(self):
outside = self.root / "outside-mod"
outside.mkdir()
(outside / "mod.toml").write_text('id="linked"')
(self.package / "mods/linked").symlink_to(outside, target_is_directory=True)
with self.assertRaisesRegex(ValueError, "symlink"):
self.plan()
def test_unmanifested_data_fifo_is_rejected_without_blocking(self):
os.mkfifo(self.package / "data/fifo")
with self.assertRaisesRegex(ValueError, "non-regular"):
self.plan()
def test_skipped_private_symlinks_and_fifo_do_not_enter_release(self):
(self.package / "target/link").symlink_to(self.root, target_is_directory=True)
os.mkfifo(self.package / ".reference/fifo")
_, plan = self.plan()
self.assertFalse(any("target/" in row["name"] or ".reference/" in row["name"]
for asset in plan["assets"] for row in asset["files"]))
def test_selected_core_symlink_and_fifo_are_rejected(self):
for kind in ("symlink", "fifo"):
with self.subTest(kind=kind):
path = self.package / "mods/example/bad"
if kind == "symlink":
path.symlink_to(self.package / "src/main.rs")
else:
os.mkfifo(path)
try:
with self.assertRaisesRegex(ValueError, "symlink|non-regular"):
self.plan()
finally:
path.unlink()
def test_checksum_receipt_identity_and_invalid_tag_rejected_before_output(self):
path, plan = self.plan()
receipts = self.root / "receipts"
receipts.mkdir()
for asset in plan["assets"]:
result = release.write_asset(path, asset["name"], self.root / asset["name"])
release.write_new_json(receipts / (asset["name"] + ".json"), result)
record = receipts / (plan["assets"][0]["name"] + ".json")
original = release.read_json(record)
for field, bad in (("checkpoint", {}), ("kind", "wrong"), ("payload_files", -1)):
value = dict(original)
value[field] = bad
record.write_text(json.dumps(value))
output = self.root / (field + "-SHA256SUMS")
with self.assertRaisesRegex(ValueError, "mismatched"):
release.checksums(path, receipts, output)
self.assertFalse(output.exists())
record.write_text(json.dumps(original))
output = self.root / "invalid-tag-SHA256SUMS"
with self.assertRaisesRegex(ValueError, "release tag"):
release.checksums(path, receipts, output, self.root / "invalid-manifest.json", "../bad")
self.assertFalse(output.exists())
self.assertFalse((self.root / "invalid-manifest.json").exists())
class LinuxInstallerTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
script = Path(__file__).parents[1] / "release/Install-Linux.sh"
source = script.read_text().split("# BEGIN INSTALLER PYTHON\n", 1)[1].split("# END INSTALLER PYTHON", 1)[0]
cls.api = {"__name__": "installer_test"}
exec(compile(source, str(script), "exec"), cls.api)
def test_real_long_path_payload_hash_and_executable_mode(self):
with tempfile.TemporaryDirectory() as temp:
root = Path(temp)
archive = root / "one.zip"
name = "data/" + ("long-component/" * 25) + "picture.png"
body = b"complete accepted payload"
with zipfile.ZipFile(archive, "w") as packed:
info = zipfile.ZipInfo("RustRaptor/" + name)
info.external_attr = 0o100755 << 16
packed.writestr(info, body)
row = {"name": name, "bytes": len(body), "sha256": release.hashlib.sha256(body).hexdigest()}
self.api["extract"](archive, root, [row])
actual = root / "RustRaptor" / name
self.assertEqual(actual.read_bytes(), body)
self.assertEqual(actual.stat().st_mode & 0o777, 0o755)
with self.assertRaises(FileExistsError):
self.api["extract"](archive, root, [row])
def test_unexpected_member_symlink_and_bad_hash_rejected(self):
for case in ("unexpected", "symlink", "hash", "traversal"):
with self.subTest(case=case), tempfile.TemporaryDirectory() as temp:
root = Path(temp)
archive = root / "one.zip"
name = "RustRaptor/file" if case != "traversal" else "RustRaptor/../escape"
with zipfile.ZipFile(archive, "w") as packed:
info = zipfile.ZipInfo(name)
info.external_attr = (0o120777 if case == "symlink" else 0o100644) << 16
packed.writestr(info, b"body")
row = {"name": "other" if case == "unexpected" else name[len("RustRaptor/"):],
"bytes": 4, "sha256": "0" * 64 if case == "hash" else release.hashlib.sha256(b"body").hexdigest()}
with self.assertRaises(ValueError):
self.api["extract"](archive, root, [row])
def test_existing_symlink_parent_is_rejected(self):
with tempfile.TemporaryDirectory() as temp:
root = Path(temp)
outside = root / "outside"
outside.mkdir()
(root / "RustRaptor").symlink_to(outside, target_is_directory=True)
archive = root / "one.zip"
with zipfile.ZipFile(archive, "w") as packed:
packed.writestr("RustRaptor/file", b"body")
row = {"name": "file", "bytes": 4, "sha256": release.hashlib.sha256(b"body").hexdigest()}
with self.assertRaisesRegex(ValueError, "parent"):
self.api["extract"](archive, root, [row])
self.assertFalse((outside / "file").exists())
if __name__ == "__main__":
unittest.main()