cultcache-py: only a missing store reads as empty; an empty file is refused

`Path.exists()` returns False for ELOOP and ENOTDIR, so a symlink loop or a
file where the parent directory should be read as an empty store. The read
now catches FileNotFoundError only. An existing zero-byte file is refused
with ValueError, like any other undecodable store, matching the Rust and C#
runtimes. `[0x90]` stays an empty legacy store.

Refusal text no longer points at a variant-aware runtime: an unsupported
header names the format found, and a v1 record with a fifth slot says the
file is not a valid store.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-30 17:04:39 +02:00
commit 202c4bbd63
2 changed files with 47 additions and 8 deletions

View file

@ -88,10 +88,11 @@ class SingleFileMessagePackBackingStore:
def pull_all(self) -> list[CultCacheEnvelope]:
msgpack = self._msgpack()
with self._lock:
if not self.path.exists():
return []
data = self.path.read_bytes()
if not data:
# Only a store that is not there is empty. An empty file is not a store (no
# writer leaves one), and any other failure to reach the file stays an OSError.
try:
data = self.path.read_bytes()
except FileNotFoundError:
return []
decoded = msgpack.unpackb(data, raw=False)
snapshot = _decode_v1_snapshot(decoded)
@ -159,8 +160,7 @@ def _decode_v1_snapshot(decoded: Any) -> list[CultCacheEnvelope] | None:
return None
if isinstance(decoded[0], str) and decoded[0].startswith(_STORE_FORMAT_PREFIX) and decoded[0] != STORE_FORMAT_VERSION:
raise ValueError(
f"CultCache store format {decoded[0]!r} is not readable; this runtime reads {STORE_FORMAT_VERSION!r} only. "
"The store needs a runtime that resolves document variants."
f"CultCache store format {decoded[0]!r} is not one this runtime reads; it reads {STORE_FORMAT_VERSION!r} only"
)
if decoded[0] != STORE_FORMAT_VERSION:
return None
@ -180,8 +180,8 @@ def _decode_v1_snapshot(decoded: Any) -> list[CultCacheEnvelope] | None:
raise ValueError("CultCache persisted records must be MessagePack arrays")
if len(raw_record) > _PERSISTED_RECORD_SLOTS:
raise ValueError(
f"CultCache record {raw_record[0]!r} (schema {raw_record[1]!r}) has {len(raw_record)} slots; "
f"this runtime reads {_PERSISTED_RECORD_SLOTS}. The store needs a runtime that resolves document variants."
f"CultCache record {raw_record[0]!r} (schema {raw_record[1]!r}) has {len(raw_record)} slots, more than "
f"the {_PERSISTED_RECORD_SLOTS} of a {STORE_FORMAT_VERSION} record, so this is not a valid store"
)
key, schema_id, stored_at, payload = raw_record
if not isinstance(key, str) or not key:

View file

@ -1,5 +1,6 @@
from __future__ import annotations
import os
import tempfile
import threading
import unittest
@ -841,6 +842,44 @@ class CultCacheTests(unittest.TestCase):
self.assertEqual(envelopes[0].payload, b"\x92\xa5alpha\x01")
self.assertEqual(envelopes[1].payload, b"\x92\xa4beta\x02")
def test_single_file_refusals_say_what_was_found_without_blaming_variants(self) -> None:
with self.assertRaises(ValueError) as unsupported:
self._pull_vector("unknown-header.msgpack")
self.assertIn("not one this runtime reads", str(unsupported.exception))
self.assertNotIn("variant", str(unsupported.exception))
with self.assertRaises(ValueError) as invalid:
self._pull_vector("extra-slot-full-payload.msgpack")
self.assertIn("not a valid store", str(invalid.exception))
self.assertNotIn("variant", str(invalid.exception))
def test_single_file_only_a_missing_store_reads_as_empty(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
self.assertEqual(SingleFileMessagePackBackingStore(Path(tmp) / "store.cc").pull_all(), [])
# An existing empty file is not a store: no CultCache writer leaves one.
empty = Path(tmp) / "empty.cc"
empty.write_bytes(b"")
with self.assertRaises(ValueError):
SingleFileMessagePackBackingStore(empty).pull_all()
self.assertEqual(empty.read_bytes(), b"")
# An empty array is the legacy envelope array with no envelopes: no header, no format claimed.
legacy_empty = Path(tmp) / "legacy-empty.cc"
legacy_empty.write_bytes(b"\x90")
self.assertEqual(SingleFileMessagePackBackingStore(legacy_empty).pull_all(), [])
@unittest.skipIf(os.name != "posix", "symlink loops and file-as-parent errors are POSIX errno cases")
def test_single_file_store_that_cannot_be_reached_is_an_os_error(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
loop = Path(tmp) / "loop.cc"
loop.symlink_to(loop)
parent = Path(tmp) / "body"
parent.write_bytes(b"not a directory")
for path in (loop, parent / "store.cc"):
with self.assertRaises(OSError) as caught:
SingleFileMessagePackBackingStore(path).pull_all()
self.assertNotIsInstance(caught.exception, FileNotFoundError)
if __name__ == "__main__":
unittest.main()