From 2e52f068964cae87ebcfed2f89de772fb437cbbd Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Sat, 17 Oct 2026 18:11:38 +0200 Subject: [PATCH] fix: accept every version a profile lists by name A profile lists each version its code takes under one name. The core extra carries the first; a held copy matching another entry of the same profile is no longer reported as a hash discrepancy. --- scripts/generate_pack.py | 31 ++++++++++++++++++++++- tests/test_audit_regressions.py | 45 +++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/scripts/generate_pack.py b/scripts/generate_pack.py index 693026e0..b4846267 100644 --- a/scripts/generate_pack.py +++ b/scripts/generate_pack.py @@ -538,6 +538,33 @@ def _platform_pack_name( return f"{stem}{tags}_BIOS_Pack{_system_tag(system_filter)}.zip" +def _matches_an_alternative(fe: dict, local_path: str, profiles: dict) -> bool: + """Whether the copy is another version the same profile accepts there. + + A profile lists every version its code takes under one name (each GRP + of Raze's table, eduke32's demo and retail NWINTER.GRP); the extra + carries the first, and the held copy matching a sibling is no mismatch. + """ + profile = profiles.get(fe.get("source_profile") or "", {}) + name = fe.get("source_name") or fe.get("name") + path = fe.get("source_path") or name + siblings = [ + entry for entry in profile.get("files") or [] + if isinstance(entry, dict) and entry.get("name") == name + and (entry.get("path") or name) == path + ] + if len(siblings) < 2: + return False + actual = compute_hashes(local_path, frozenset({"sha1", "md5", "crc32"})) + for entry in siblings: + for algorithm in ("sha1", "md5", "crc32"): + declared = entry.get(algorithm) + values = declared if isinstance(declared, list) else [declared] + if any(v and str(v).lower() == actual.get(algorithm, "") for v in values): + return True + return False + + def generate_pack( platform_name: str, platforms_dir: str, @@ -996,7 +1023,9 @@ def generate_pack( ) if disagreement: core_discrepancies.append(f"{full_dest} -{disagreement}") - if status == "hash_mismatch": + if status == "hash_mismatch" and not _matches_an_alternative( + fe, local_path, emu_profiles or {} + ): # The core's declared hash comes from its source, the local dump # is what the collection holds. Shipping it keeps the emulator # working when the code never reads the hash; the divergence is diff --git a/tests/test_audit_regressions.py b/tests/test_audit_regressions.py index b43aeb1a..3e0b5bf6 100644 --- a/tests/test_audit_regressions.py +++ b/tests/test_audit_regressions.py @@ -15,6 +15,7 @@ import sys import tempfile import unittest import zipfile +import zlib from pathlib import Path from unittest import mock @@ -2861,3 +2862,47 @@ class ADuplicateYamlKeyIsRefused(unittest.TestCase): if __name__ == "__main__": unittest.main() + + +class AnAcceptedVersionIsNoCoreDiscrepancy(unittest.TestCase): + """eduke32 lists the retail and demo NWINTER.GRP under one name: the extra + carries the first, and a held demo copy is a version the code takes.""" + + PROFILE = {"files": [ + {"name": "NWINTER.GRP", "size": 9, "crc32": "00000000", + "validation": ["size", "crc32"]}, + {"name": "NWINTER.GRP", "size": 4, "crc32": "", + "validation": ["size", "crc32"]}, + ]} + + def _extra(self) -> dict: + # The shape find_undeclared_files gives a core extra: source_path + # defaults to the name when the profile declares no path. + return {"name": "NWINTER.GRP", "source_name": "NWINTER.GRP", + "source_path": "NWINTER.GRP", "source_profile": "eduke32"} + + def test_a_sibling_version_matches(self): + from scripts.generate_pack import _matches_an_alternative + + with tempfile.TemporaryDirectory(dir=TMP_ROOT) as tmp: + held = Path(tmp) / "NWINTER.GRP" + held.write_bytes(b"demo") + profile = json.loads(json.dumps(self.PROFILE)) + profile["files"][1]["crc32"] = f"{zlib.crc32(b'demo'):08x}" + self.assertTrue(_matches_an_alternative( + self._extra(), str(held), {"eduke32": profile})) + + def test_a_copy_no_entry_declares_still_disagrees(self): + from scripts.generate_pack import _matches_an_alternative + + with tempfile.TemporaryDirectory(dir=TMP_ROOT) as tmp: + held = Path(tmp) / "NWINTER.GRP" + held.write_bytes(b"other") + profile = json.loads(json.dumps(self.PROFILE)) + profile["files"][1]["crc32"] = f"{zlib.crc32(b'demo'):08x}" + self.assertFalse(_matches_an_alternative( + self._extra(), str(held), {"eduke32": profile})) + + def test_the_pack_consults_the_alternatives(self): + source = (ROOT / "scripts" / "generate_pack.py").read_text() + self.assertRegex(source, r'status == "hash_mismatch" and not _matches_an_alternative\(')