From 5441c57ffb40d2bcc0b10554f1f4a379d65f7ec8 Mon Sep 17 00:00:00 2001 From: Tet-9 Date: Mon, 5 Oct 2026 09:05:50 +0100 Subject: [PATCH] Harden verify_sources cache handling --- excore/sources.py | 13 ++++++++++--- tests/test_cli_plots_sources.py | 16 ++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/excore/sources.py b/excore/sources.py index 06a0e5d..fa647da 100644 --- a/excore/sources.py +++ b/excore/sources.py @@ -70,8 +70,10 @@ def verify_sources(lock: dict, root: str | Path, *, cache: str | Path | None = N seen: dict = {} if cache and Path(cache).exists(): try: - seen = json.loads(Path(cache).read_text()) - except (OSError, json.JSONDecodeError): + seen = json.loads(Path(cache).read_text(encoding="utf-8")) + except (OSError, ValueError): + seen = {} + if not isinstance(seen, dict): # valid JSON of the wrong shape is treated as empty seen = {} new_seen = {} for f in lock["sources"]["base"]["files"]: @@ -90,7 +92,12 @@ def verify_sources(lock: dict, root: str | Path, *, cache: str | Path | None = N continue new_seen[f["name"]] = key if cache and not problems: - Path(cache).write_text(json.dumps(new_seen)) + try: + tmp = Path(str(cache) + ".tmp") + tmp.write_text(json.dumps(new_seen), encoding="utf-8") + os.replace(tmp, cache) + except OSError: + pass # the cache only saves re-hashing; never fail verification over it return problems diff --git a/tests/test_cli_plots_sources.py b/tests/test_cli_plots_sources.py index e94c621..715243b 100644 --- a/tests/test_cli_plots_sources.py +++ b/tests/test_cli_plots_sources.py @@ -152,6 +152,22 @@ def test_pin_verify_and_tamper(lock_and_model): load_lock(lock, track="GPU-01") +@pytest.mark.parametrize("junk", ["[]", "null", "42", "not json", "\udcff"]) +def test_verify_ignores_malformed_cache(lock_and_model, junk): + lock, models = lock_and_model + pin(lock, models, repo="org/m", revision="r1", files=["base-bf16.gguf"], llama_cpp_commit="a" * 40) + cache = lock.parent / "verified.json" + cache.write_bytes(junk.encode("utf-8", "surrogateescape")) + assert verify_sources(load_lock(lock), models, cache=cache) == [] + assert isinstance(json.loads(cache.read_text(encoding="utf-8")), dict) # rewritten cleanly + + +def test_verify_survives_unwritable_cache(lock_and_model): + lock, models = lock_and_model + pin(lock, models, repo="org/m", revision="r1", files=["base-bf16.gguf"], llama_cpp_commit="a" * 40) + assert verify_sources(load_lock(lock), models, cache=lock.parent / "no-such-dir" / "verified.json") == [] + + def test_sources_cli(lock_and_model, tiny_config): lock, models = lock_and_model assert main(["--config", tiny_config, "sources", "verify", "--lock", str(lock), "--root", str(models)]) == 1