fix: read crc32 past a copier header in slots

Geargrafx drops a 512-byte header before its crc32 (src/media.cpp:754-773
at 7c447b0b), and its profile lists both sizes. slots compared the crc32
of the whole file, so EmuELEC's headered syscard3.pce read as contested.
This commit is contained in:
Abdessamad Derraz committed 2026-11-06 15:04:26 +01:00
1 parent 6bf62d5746
commit 74a4a036ea
2 files changed
+64

No files matched your search

+28
View File
@@ -20,6 +20,7 @@ feeds the report this module prints.
from __future__ import annotations
import os
import zlib
from dataclasses import dataclass, field
from typing import TYPE_CHECKING
@@ -892,10 +893,37 @@ def _accepted_by_all(path: str, entries: list[dict], db: dict) -> bool:
for field_name in ("md5", "sha1", "sha256", "crc32"):
declared = _declared_hashes(entry, field_name)
if declared and str(record.get(field_name, "")).lower() not in declared:
if field_name == "crc32" and _headerless_crc32(path, record, entry) in declared:
continue
return False
return True
COPIER_HEADER = 512
def _headerless_crc32(path: str, record: dict, entry: dict) -> str | None:
"""The crc32 of a file past its 512-byte copier header, when the entry
declares both the file's size and the size without the header.
Geargrafx drops the header when the file is 512 bytes over the expected
size and computes the crc32 on the rest (src/media.cpp:754-773). The
profile lists both sizes. EmuELEC's headered syscard3.pce (crc32
64f78e3c) is the 6d9a73ef image the profile names.
"""
sizes = entry.get("size")
declared = sizes if isinstance(sizes, list) else [sizes]
size = record.get("size")
if not isinstance(size, int) or not {size, size - COPIER_HEADER} <= set(declared):
return None
try:
with open(path, "rb") as f:
f.seek(COPIER_HEADER)
return f"{zlib.crc32(f.read()):08x}"
except OSError:
return None
def _same_file_family(paths: list[str]) -> bool:
"""Whether the paths are one file and its own pinned variants.
+36
View File
@@ -992,6 +992,42 @@ class TwoPathsHoldingOneContentAgree(unittest.TestCase):
self.assertFalse(slots._satisfies(different, platform, db))
class ACopierHeaderIsNotAnotherImage(unittest.TestCase):
"""Geargrafx drops a 512-byte header before the crc32 (src/media.cpp:
754-773). EmuELEC's headered syscard3.pce was reported contested, though
the core reads it as the 6d9a73ef image its profile lists."""
def test_headered_file_satisfies_the_profile(self):
import zlib # noqa: PLC0415
image = b"S" * 1024
with tempfile.TemporaryDirectory() as tmp:
headered = Path(tmp, "headered.pce")
headered.write_bytes(b"\0" * 512 + image)
plain = Path(tmp, "plain.pce")
plain.write_bytes(image)
crc = f"{zlib.crc32(image):08x}"
db = {"files": {
"a" * 40: {"path": str(headered), "size": 1536,
"crc32": f"{zlib.crc32(headered.read_bytes()):08x}"},
"b" * 40: {"path": str(plain), "size": 1024, "crc32": crc},
}}
platform = Claim("platform", "syscard3.pce", "syscard3.pce",
local_path=str(headered), status="md5_exact")
profile = Claim(
"profile", "syscard3.pce", "syscard3.pce", emulator="geargrafx",
local_path=str(plain), status="crc32_exact",
entry={"name": "syscard3.pce", "crc32": [crc], "size": [1024, 1536]},
)
self.assertTrue(slots._satisfies(profile, platform, db))
one_size = Claim(
"profile", "syscard3.pce", "syscard3.pce", emulator="geargrafx",
local_path=str(plain), status="crc32_exact",
entry={"name": "syscard3.pce", "crc32": [crc], "size": 1024},
)
self.assertFalse(slots._satisfies(one_size, platform, db))
class TheSlotsSummaryClosesTheRun(unittest.TestCase):
"""The totals came before the collision section and ignored it, and the
help said the default run fails on conflicts the pack should settle,