From ae874534e2049b8d956dc6853dcd8b2587493ae2 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Wed, 21 Oct 2026 05:07:06 +0200 Subject: [PATCH] fix: reread a contradicted hash from disk _stable_hashes retried through the hash cache, so both retries returned the first reading and the file was read once before the manifest run stopped. The retries now read the file again. --- scripts/generate_pack.py | 4 +++- tests/test_install_experience.py | 27 +++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/scripts/generate_pack.py b/scripts/generate_pack.py index c4b06d0f..acf92ef9 100644 --- a/scripts/generate_pack.py +++ b/scripts/generate_pack.py @@ -3468,7 +3468,9 @@ def _stable_hashes(local_path: str, db: dict) -> dict: if expected is None or hashes["sha1"] == expected: _STABLE_HASHES[key] = hashes return hashes - hashes = compute_hashes(local_path, _MANIFEST_HASHES) + # Read from disk again: the cached digest is the one in doubt, and a + # retry through the cache answered with it without a second read. + hashes = compute_hashes(local_path, _MANIFEST_HASHES, cache=False) raise RuntimeError( f"{local_path}: sha1 {hashes['sha1']} contradicts the database scan " f"({expected}). Rehash the file or regenerate database.json" diff --git a/tests/test_install_experience.py b/tests/test_install_experience.py index 4a0aaae6..f356a2f3 100644 --- a/tests/test_install_experience.py +++ b/tests/test_install_experience.py @@ -237,6 +237,33 @@ class ManifestHashesMatchTheScan(unittest.TestCase): generate_pack._stable_hashes(str(path), bad) self.assertEqual(generate_pack._stable_hashes(str(path), {"files": {}})["sha1"], real) + def test_a_wrong_first_reading_is_read_again_from_disk(self): + """The retries went through the hash cache and got the first, wrong + value back: one read of the file, then a refusal a second read would + have avoided.""" + sys.path.insert(0, str(REPO_ROOT / "scripts")) + import generate_pack + import hashing + + real_compute = hashing._compute_hashes + reads: list[str] = [] + + def flaky(filepath, algos): + reads.append(str(filepath)) + result = real_compute(filepath, algos) + if len(reads) == 1: + result = {**result, "sha1": "0" * 40} + return result + + with tempfile.TemporaryDirectory() as tmp: + path = Path(tmp) / "nand.bin" + path.write_bytes(b"flaky") + real = hashlib.sha1(b"flaky").hexdigest() + db = {"files": {real: {"path": str(path)}}} + with mock.patch.object(hashing, "_compute_hashes", flaky): + self.assertEqual(generate_pack._stable_hashes(str(path), db)["sha1"], real) + self.assertEqual(len(reads), 2) + def test_the_cache_is_keyed_by_what_was_checked(self): """Keyed by id(db), a database freed and replaced at the same address got hashes verified against the first one back, unchecked."""