From 53625156d6b6e7c763f11e23427095e7f5bb7bf5 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Sat, 17 Oct 2026 08:52:21 +0200 Subject: [PATCH] fix: key the zip member index on all its inputs --- scripts/ziptools.py | 10 ++++++++-- tests/test_audit_regressions.py | 23 +++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/scripts/ziptools.py b/scripts/ziptools.py index fb848988..c5a8cc5c 100644 --- a/scripts/ziptools.py +++ b/scripts/ziptools.py @@ -57,7 +57,7 @@ def zip_member_crcs(path: str) -> frozenset[str]: return _member_crcs(path, mtime) -_zip_contents_cache: tuple[frozenset[tuple[str, float]], dict] | None = None +_zip_contents_cache: tuple[tuple, dict] | None = None def build_zip_contents_index(db: dict, max_entry_size: int = 512 * 1024 * 1024) -> dict: """Build {inner_rom_md5: zip_file_sha1} for ROMs inside ZIP files. @@ -74,7 +74,13 @@ def build_zip_contents_index(db: dict, max_entry_size: int = 512 * 1024 * 1024) if path.endswith(".zip") and os.path.exists(path): zip_entries.append((path, sha1)) - fingerprint = frozenset((path, os.path.getmtime(path)) for path, _ in zip_entries) + # The bound and the sha1 each path carries are part of the answer: a + # second call with a smaller bound got members the first one admitted, + # and a regenerated database got the sha1 of the old one. + fingerprint = ( + max_entry_size, + frozenset((path, os.path.getmtime(path), sha1) for path, sha1 in zip_entries), + ) if _zip_contents_cache is not None and _zip_contents_cache[0] == fingerprint: return _zip_contents_cache[1] diff --git a/tests/test_audit_regressions.py b/tests/test_audit_regressions.py index a0da649d..9e19e73c 100644 --- a/tests/test_audit_regressions.py +++ b/tests/test_audit_regressions.py @@ -2448,6 +2448,29 @@ class AnUnreadableDatabaseStopsTheScan(unittest.TestCase): ) +class TheZipIndexCacheKnowsItsInputs(unittest.TestCase): + """build_zip_contents_index cached on (path, mtime) alone: a smaller + max_entry_size, or a database giving the same archive another sha1, + got the first call's index back.""" + + def test_the_bound_and_the_sha1_are_part_of_the_key(self): + from scripts import ziptools + + with tempfile.TemporaryDirectory(dir=TMP_ROOT) as directory: + path = Path(directory) / "a.zip" + with zipfile.ZipFile(path, "w") as archive: + archive.writestr("big.bin", b"x" * 4096) + ziptools._zip_contents_cache = None + db = {"files": {"a" * 40: {"path": str(path)}}} + self.assertEqual(len(ziptools.build_zip_contents_index(db)), 1) + self.assertEqual(ziptools.build_zip_contents_index(db, max_entry_size=10), {}) + other = {"files": {"b" * 40: {"path": str(path)}}} + self.assertEqual( + set(ziptools.build_zip_contents_index(other).values()), {"b" * 40} + ) + ziptools._zip_contents_cache = None + + class TheDatabaseCacheIsWrittenWhole(unittest.TestCase): """A crash while serializing left a truncated db_cache.json behind, and the next run rehashed the whole collection from scratch."""