fix: pack each zip with the collection's bytes

This commit is contained in:
Abdessamad Derraz committed 2026-10-23 07:47:35 +02:00
1 parent 34f9195860
commit fed03c3db7
2 files changed
+56 -52

No files matched your search

+28 -47
View File
@@ -80,7 +80,7 @@ import slot as slot_mod
import slots import slots
import split_pack import split_pack
from gamedata import GAME_DATA, GameDataCut, is_game_data, platform_game_data, without_game_data from gamedata import GAME_DATA, GameDataCut, is_game_data, platform_game_data, without_game_data
from deterministic_zip import _FIXED_DATE_TIME, rebuild_zip_deterministic from deterministic_zip import _FIXED_DATE_TIME
from largefiles import asset_name, load_registered_paths, published_asset_name from largefiles import asset_name, load_registered_paths, published_asset_name
from nativemode import ( from nativemode import (
digest_algorithm, digest_algorithm,
@@ -1259,64 +1259,45 @@ def _add_zip_to_pack(
target_zf: zipfile.ZipFile, target_zf: zipfile.ZipFile,
file_entry: dict | None = None, file_entry: dict | None = None,
) -> None: ) -> None:
"""Write a source ZIP into the pack. """Write a source ZIP into the pack, byte for byte.
A declared outer hash (md5/sha1/crc32) means the platform or profile The pack carries the collection's own bytes, so every member hashes to
verifies the ZIP bytes themselves (Batocera md5sum, System.dat), so the sha1 the install list and the site publish. Rebuilt ZIPs gave about
the resolved file is copied unchanged: a rebuild would change the 1,500 archives another hash than the list: an extracted pack then failed
outer hash and fail the native check. Entries with zipped_file `install.py --check`. The collection's bytes are fixed in git, which keeps
declare the hash of a ROM inside the ZIP (checkInsideZip), which a the pack reproducible without a rebuild.
content-preserving rebuild does not affect.
""" """
if isinstance(target_zf, PackRecord): if isinstance(target_zf, PackRecord):
target_zf.members.append(dest_path) target_zf.members.append(dest_path)
return return
if ( _check_zip_members(source_zip)
file_entry
and not file_entry.get("zipped_file")
and (
file_entry.get("md5")
or file_entry.get("sha1")
or file_entry.get("crc32")
)
):
_add_pack_member(target_zf, source_zip, dest_path) _add_pack_member(target_zf, source_zip, dest_path)
return
_normalize_zip_for_pack(source_zip, dest_path, target_zf)
def _check_zip_members(source_zip: str) -> None:
"""Refuse an archive whose members fail their own CRC.
Copied as-is "for verify to flag", a damaged archive went into the pack
and nothing flagged it.
"""
try:
with zipfile.ZipFile(source_zip) as zf:
bad = zf.testzip()
except zipfile.BadZipFile as exc:
raise DamagedArchiveError(
f"{source_zip}: {exc}; restore it from git or the release before packing"
) from exc
if bad is not None:
raise DamagedArchiveError(
f"{source_zip}: member {bad} fails its CRC; "
"restore it from git or the release before packing"
)
class DamagedArchiveError(OSError): class DamagedArchiveError(OSError):
"""An archive of the collection whose members fail their own CRC.""" """An archive of the collection whose members fail their own CRC."""
def _normalize_zip_for_pack(
source_zip: str, dest_path: str, target_zf: zipfile.ZipFile
) -> None:
"""Add a MAME BIOS ZIP to the pack, rebuilt from its ROMs.
The same ROMs give the same archive bytes in every build, whatever
metadata the source ZIP carried.
"""
import tempfile as _tmp
# tmp/ is gitignored, so a fresh clone does not carry it. WSL keeps /tmp
# on a 4 GB tmpfs that MAME sets overflow, hence the repo-local scratch.
os.makedirs("tmp", exist_ok=True)
tmp_fd, tmp_path = _tmp.mkstemp(suffix=".zip", dir="tmp")
os.close(tmp_fd)
try:
rebuild_zip_deterministic(source_zip, tmp_path)
_add_pack_member(target_zf, tmp_path, dest_path)
except zipfile.BadZipFile as exc:
# The rebuild checks every member's CRC. Copied as-is "for verify to
# flag", a damaged archive went into the pack and nothing flagged it.
raise DamagedArchiveError(
f"{source_zip}: {exc}; restore it from git or the release before packing"
) from exc
finally:
os.unlink(tmp_path)
# Emulator/system mode pack generation # Emulator/system mode pack generation
+28 -5
View File
@@ -626,14 +626,14 @@ class StandaloneCopiesNameTheirSource(unittest.TestCase):
class ADamagedArchiveIsNotPacked(unittest.TestCase): class ADamagedArchiveIsNotPacked(unittest.TestCase):
"""The deterministic rebuild raises on a member whose bytes fail their """A member whose bytes fail their CRC stops the build. The builder once
CRC. The builder caught it and copied the damaged archive into the pack copied the damaged archive into the pack "for verify to flag", which
"for verify to flag", which nothing did.""" nothing did."""
def test_the_rebuild_refusal_reaches_the_run(self): def test_the_rebuild_refusal_reaches_the_run(self):
import zipfile import zipfile
from generate_pack import DamagedArchiveError, _normalize_zip_for_pack from generate_pack import DamagedArchiveError, _add_zip_to_pack
with tempfile.TemporaryDirectory(dir=TMP_ROOT) as directory: with tempfile.TemporaryDirectory(dir=TMP_ROOT) as directory:
source = Path(directory) / "set.zip" source = Path(directory) / "set.zip"
@@ -645,12 +645,35 @@ class ADamagedArchiveIsNotPacked(unittest.TestCase):
pack = Path(directory) / "pack.zip" pack = Path(directory) / "pack.zip"
with zipfile.ZipFile(pack, "w") as target: with zipfile.ZipFile(pack, "w") as target:
with self.assertRaises(DamagedArchiveError) as caught: with self.assertRaises(DamagedArchiveError) as caught:
_normalize_zip_for_pack(str(source), "set.zip", target) _add_zip_to_pack(str(source), "set.zip", target)
self.assertNotIn("set.zip", target.namelist()) self.assertNotIn("set.zip", target.namelist())
self.assertIsInstance(caught.exception, OSError) self.assertIsInstance(caught.exception, OSError)
self.assertIn("set.zip", str(caught.exception)) self.assertIn("set.zip", str(caught.exception))
class APackedArchiveKeepsItsBytes(unittest.TestCase):
"""Packs rebuilt about 1,500 ZIPs in a fixed byte order while the install
list kept the collection's hash: an extracted pack failed --check on each."""
def test_the_member_is_the_source_byte_for_byte(self):
import hashlib
import zipfile
from generate_pack import _add_zip_to_pack
with tempfile.TemporaryDirectory(dir=TMP_ROOT) as directory:
source = Path(directory) / "set.zip"
with zipfile.ZipFile(source, "w", zipfile.ZIP_DEFLATED) as zf:
zf.writestr(zipfile.ZipInfo("rom.bin", (1999, 1, 2, 3, 4, 5)), b"rom" * 40)
pack = Path(directory) / "pack.zip"
with zipfile.ZipFile(pack, "w") as target:
_add_zip_to_pack(str(source), "set.zip", target)
with zipfile.ZipFile(pack) as packed:
inner = packed.read("set.zip")
self.assertEqual(hashlib.sha1(inner).hexdigest(),
hashlib.sha1(source.read_bytes()).hexdigest())
class PackReadmesSayWhatTheInstallerDoes(unittest.TestCase): class PackReadmesSayWhatTheInstallerDoes(unittest.TestCase):
"""The EmuDeck README promised keys set up automatically (they need """The EmuDeck README promised keys set up automatically (they need
--standalone-copies) and gave a Citra path the installer never uses, and the --standalone-copies) and gave a Citra path the installer never uses, and the