From 55f7ed24c24d7fc8dac13a844293b1f7f3a77688 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Thu, 5 Nov 2026 23:25:21 +0100 Subject: [PATCH] fix: rank archives by name on a shared crc32 segadimm.zip holds fpr23905.ic36 and fpr23905c.ic36 under the same crc32, and segasp.zip four fpr-*.ic72 images. Ranked by crc32 alone, a copy missing some of them tied with the full set and the index order chose it. A crc32 that two declared members share now also requires the member name. --- install/recalbox.json | 26 ++++++++++++++++++-------- scripts/common.py | 27 +++++++++++++++++++-------- tests/test_archive_choice.py | 16 ++++++++++++++++ 3 files changed, 53 insertions(+), 16 deletions(-) diff --git a/install/recalbox.json b/install/recalbox.json index 470e31098..f6395e260 100644 --- a/install/recalbox.json +++ b/install/recalbox.json @@ -280,9 +280,9 @@ } ], "total_files": 3743, - "total_size": 4682168657, + "total_size": 4685153679, "pack_files": 3745, - "pack_size": 4682168657, + "pack_size": 4685153679, "total_omitted": 62, "omitted_files": [ { @@ -42218,14 +42218,13 @@ }, { "dest": "segasp.zip", - "sha1": "de78a0b8df78d5ffdb1a274f80d877a96ef204aa", - "sha256": "3bca348667d6ff08fd5efe38eec35100ccbe0de3cd1e0b5dd72ae5c6cc201ee9", - "size": 1281424, - "repo_path": "bios/Arcade/Arcade/segasp.zip", + "sha1": "b4c7e46fb510096acf8fb19fb99c8c5a8ab1d9e1", + "sha256": "bda96939992d42410034caf3f5296d8fc63eced1693b5b522536c6ac717ded73", + "size": 4266446, + "repo_path": "bios/Arcade/MAME/segasp.zip", "cores": [ "mame0258", - "mame0278", - "mame2016" + "mame0278" ], "systems": [ "3do", @@ -42238,6 +42237,17 @@ "scv", "stv", "ti994a" + ], + "alternatives": [ + { + "sha1": "de78a0b8df78d5ffdb1a274f80d877a96ef204aa", + "sha256": "3bca348667d6ff08fd5efe38eec35100ccbe0de3cd1e0b5dd72ae5c6cc201ee9", + "size": 1281424, + "repo_path": "bios/Arcade/Arcade/segasp.zip", + "cores": [ + "mame2016" + ] + } ] }, { diff --git a/scripts/common.py b/scripts/common.py index e53889378..c6e0bfe25 100644 --- a/scripts/common.py +++ b/scripts/common.py @@ -1149,19 +1149,30 @@ def contents_members(file_entry: dict) -> list[dict]: A member is its crc32 where the profile declares one, its name where the code opens it by name alone: Flycast reads epr-21329.ic27 in hod2bios.zip by filename, and a copy without it has no Japan BIOS. + A crc32 two members share also keeps the name: fpr23905.ic36 and + fpr23905c.ic36 both have crc32 ffffffff in segadimm.zip, so the crc + alone cannot tell a copy holding one from a copy holding both. """ - return [ - {"crc32": str(m["crc32"]).lower(), "required": True} - if m.get("crc32") else {"name": str(m["name"]), "required": True} - for m in file_entry.get("contents") or [] - if m.get("crc32") or m.get("name") - ] + listed = [m for m in file_entry.get("contents") or [] if m.get("crc32") or m.get("name")] + seen = Counter(str(m["crc32"]).lower() for m in listed if m.get("crc32")) + members = [] + for m in listed: + if not m.get("crc32"): + members.append({"name": str(m["name"]), "required": True}) + continue + crc = str(m["crc32"]).lower() + member = {"crc32": crc, "required": True} + if seen[crc] > 1 and m.get("name"): + member["name"] = str(m["name"]) + members.append(member) + return members def _member_held(member: dict, crcs: frozenset[str], names: frozenset[str]) -> bool: + named = member.get("name", "").rsplit("/", 1)[-1].casefold() in names if member.get("crc32"): - return member["crc32"] in crcs - return member["name"].rsplit("/", 1)[-1].casefold() in names + return member["crc32"] in crcs and ("name" not in member or named) + return named def _holding_members(paths: list[str], members: list[dict]) -> list[str]: diff --git a/tests/test_archive_choice.py b/tests/test_archive_choice.py index 37bac96a1..602dc14ce 100644 --- a/tests/test_archive_choice.py +++ b/tests/test_archive_choice.py @@ -390,6 +390,22 @@ class AnArchiveRankedByWhatItHolds(unittest.TestCase): for name in ("epr-21330.ic27", "epr-21332.ic27")]} self.assertEqual(self._resolve(entry, db), "bios/MAME/hod2bios.zip") + def test_a_shared_crc_keeps_the_member_name(self): + """segadimm.zip holds fpr23905.ic36 and fpr23905c.ic36, both crc32 + ffffffff. Ranked by crc alone, a copy without fpr23905c.ic36 tied with + the full set and came first in the index.""" + self.roms["fpr23905.ic36"] = self.roms["fpr23905c.ic36"] = b"\xff" * 16 + db = self._db([("bios/Arcade/hod2bios.zip", ["epr-21330.ic27", "fpr23905.ic36"]), + ("bios/MAME/hod2bios.zip", + ["epr-21330.ic27", "fpr23905.ic36", "fpr23905c.ic36"])]) + crc = format(zlib.crc32(b"\xff" * 16), "08x") + first = format(zlib.crc32(self.roms["epr-21330.ic27"]), "08x") + entry = {"name": "hod2bios.zip", "contents": [ + {"name": "epr-21330.ic27", "crc32": first}, + {"name": "fpr23905.ic36", "crc32": crc}, + {"name": "fpr23905c.ic36", "crc32": crc}]} + self.assertEqual(self._resolve(entry, db), "bios/MAME/hod2bios.zip") + def test_a_copy_holding_no_declared_member_is_not_the_archive(self): """galgames.zip held only an assembler listing and none of the six ROMs MAME declares: the name step served it as the BIOS set."""