From 7a6e494597cb9b08b6232b6a876154bd59f3d4cb Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Sat, 17 Oct 2026 10:28:11 +0200 Subject: [PATCH] fix: name custom and system packs by their selection Two --from-md5 runs with different hash lists wrote the same file, the second replacing the first, and a pack limited to thirty systems built a name longer than a file name may be. The custom pack now carries a digest of its hash list, and a long system list keeps its first system, a count and a digest, as the emulator pack does. --- scripts/generate_pack.py | 31 ++++++++++++++++++++++++++++--- tests/test_audit_regressions.py | 16 ++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/scripts/generate_pack.py b/scripts/generate_pack.py index a5aae92e..e1ddb796 100644 --- a/scripts/generate_pack.py +++ b/scripts/generate_pack.py @@ -203,7 +203,25 @@ def _system_tag(system_filter: list[str] | None) -> str: s = s[len(prefix) :] break display_parts.append("_".join(p.title() for p in s.split("-") if p)) - return "_" + "_".join(display_parts) + return "_" + _bounded_join(display_parts, system_filter) + + +# A system list long enough to push a pack name past this falls back to the +# first system, a count and a digest of the list. +MAX_SYSTEM_TAG_BYTES = 120 + + +def _bounded_join(parts: list[str], keys: list[str]) -> str: + """Parts joined while they fit, else the first, a count and a digest. + + Thirty systems joined made a 261-byte file name and the ZIP could not + be opened; the digest keeps two different selections apart. + """ + joined = "_".join(parts) + if len(joined.encode("utf-8")) <= MAX_SYSTEM_TAG_BYTES: + return joined + digest = hashlib.sha1("\n".join(sorted(keys)).encode("utf-8")).hexdigest()[:8] + return f"{parts[0]}_and_{len(parts) - 1}_more_{digest}" _UNSAFE_IN_NAME = re.compile(r'[/\\:*?"<>|]') @@ -1634,7 +1652,9 @@ def generate_system_pack( # Named like the platform's --system tag: one spelling of a system is # one file name, and one selection. - sys_display = "_".join(_system_display_name(sid) for sid in system_ids) + sys_display = _bounded_join( + [_system_display_name(sid) for sid in system_ids], list(system_ids) + ) # Built in a scratch directory: under its emulator name in output_dir it # overwrote, then carried away, an --emulator pack already there. os.makedirs(output_dir, exist_ok=True) @@ -1925,8 +1945,13 @@ def generate_md5_pack( "full", None, None, False, False, standalone=standalone ) ) + # The hash list is what chooses the content: two lists under one name + # overwrote each other in the same output directory. + selection = hashlib.sha1( + "\n".join(sorted(f"{kind}:{value.lower()}" for kind, value in hashes)).encode() + ).hexdigest()[:8] zip_name = ( - f"{_name_part(context_name, '_')}_Custom{custom_tags}_BIOS_Pack.zip" + f"{_name_part(context_name, '_')}_Custom_{selection}{custom_tags}_BIOS_Pack.zip" ) zip_path = os.path.join(output_dir, zip_name) os.makedirs(output_dir, exist_ok=True) diff --git a/tests/test_audit_regressions.py b/tests/test_audit_regressions.py index b9854954..f0240a69 100644 --- a/tests/test_audit_regressions.py +++ b/tests/test_audit_regressions.py @@ -2238,6 +2238,22 @@ class APackNameFitsAFileName(unittest.TestCase): self.assertNotEqual(long_name, other) self.assertEqual(_bounded_pack_name(["A", "B"], ["a", "b"], ".zip"), "A_B.zip") + def test_a_long_system_list_is_bounded(self): + """Thirty systems joined made a 261-byte platform pack name.""" + from scripts.generate_pack import MAX_SYSTEM_TAG_BYTES, _system_tag + + systems = [f"vendor-console-number-{i}" for i in range(30)] + tag = _system_tag(systems) + self.assertLessEqual(len(tag.encode()), MAX_SYSTEM_TAG_BYTES + 1) + self.assertNotEqual(tag, _system_tag(systems[:-1] + ["other-console"])) + self.assertEqual(_system_tag(["sony-playstation"]), "_Playstation") + + def test_two_hash_lists_get_two_names(self): + """--from-md5 named its pack after the context only: a second list + overwrote the first pack in the same directory.""" + source = (ROOT / "scripts" / "generate_pack.py").read_text(encoding="utf-8") + self.assertIn('_Custom_{selection}', source) + def test_the_system_pack_names_itself(self): source = (ROOT / "scripts" / "generate_pack.py").read_text(encoding="utf-8") self.assertIn("pack_name=final_name", source)