diff --git a/scripts/common.py b/scripts/common.py index bd7ec210..3d84adb3 100644 --- a/scripts/common.py +++ b/scripts/common.py @@ -620,14 +620,22 @@ def declared_hash_verdict( crc_candidates: set, zipped_file: str | None, declared_size, + file_entry: dict | None = None, + db: dict | None = None, ) -> str: """Judge a candidate found by name against everything the entry declares. A file reached through a directory walk was matched on its filename, which is the weakest evidence there is. Whatever the entry declares - about its content is checked here before the walk may return it. + about its content is checked here before the walk may return it, + including the size and Adler-32 the name steps over bios/ apply. """ if not has_strong_hash: + if file_entry is not None and not ( + name_match_size_ok(file_entry, os.path.getsize(candidate)) + and adler32_fits(file_entry, candidate, db) + ): + return "hash_mismatch" return "data_dir" algorithms: set[str] = set() if sha1_candidates: @@ -1236,6 +1244,8 @@ def resolve_local_file( crc_candidates=set(), zipped_file=zipped_file, declared_size=declared_size, + file_entry=file_entry, + db=db, ), ) if hit: @@ -1358,6 +1368,8 @@ def resolve_local_file( crc_candidates=set(crc_candidates), zipped_file=zipped_file, declared_size=declared_size, + file_entry=file_entry, + db=db, ) hit, data_dir_mismatch = _resolve_in_data_dirs( names_to_try, data_dir_registry, verdict diff --git a/scripts/packresolve.py b/scripts/packresolve.py index fe81ee6c..d75717ac 100644 --- a/scripts/packresolve.py +++ b/scripts/packresolve.py @@ -74,6 +74,7 @@ def _verified_asset( crc_candidates=set(declared["crc32"]), zipped_file=file_entry.get("zipped_file"), declared_size=file_entry.get("size"), + file_entry=file_entry, ) if verdict != "hash_mismatch": return cached diff --git a/tests/test_audit_regressions.py b/tests/test_audit_regressions.py index 3e0b5bf6..fcf0af22 100644 --- a/tests/test_audit_regressions.py +++ b/tests/test_audit_regressions.py @@ -2906,3 +2906,33 @@ class AnAcceptedVersionIsNoCoreDiscrepancy(unittest.TestCase): 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\(') + + +class ADataDirectoryCopyPassesTheNameChecks(unittest.TestCase): + """The name steps over bios/ refuse a candidate outside the size or the + Adler-32 the entry's code checks; a cache copy found by the same name + was returned without either check.""" + + def setUp(self): + self.tmp = Path(tempfile.mkdtemp(dir=TMP_ROOT)) + self.addCleanup(lambda: __import__("shutil").rmtree(self.tmp, True)) + cache = self.tmp / "cache" + cache.mkdir() + (cache / "wiitdb-pt.txt").write_bytes(b"12345") + self.registry = {"dolphin-sys": {"local_cache": str(cache)}} + self.db = {"files": {}, "indexes": {"by_name": {}, "by_md5": {}, + "by_path_suffix": {}, "by_crc32": {}}} + + def resolve(self, **fields): + entry = {"name": "wiitdb-pt.txt", **fields} + return resolve_local_file(entry, self.db, data_dir_registry=self.registry) + + def test_a_size_the_code_accepts_resolves(self): + self.assertEqual(self.resolve(size=5, validation=["size"])[1], "data_dir") + + def test_another_size_is_a_contradiction(self): + self.assertEqual(self.resolve(size=1, validation=["size"])[1], "hash_mismatch") + + def test_another_adler32_is_a_contradiction(self): + status = self.resolve(validation=["adler32"], known_hash_adler32="0xdeadbeef")[1] + self.assertEqual(status, "hash_mismatch")