cultcache-py: depth walk starts only from a container root; refusals of a stack-exhausting codec and the validator's arguments pinned

Removes the equivalent continue in the walk (the stack only ever holds containers).
Pins the exact refusal text on the RecursionError and StackError paths, both sides,
and that the validator receives the written and the decoded value. Every commit builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-11 10:20:35 +02:00
commit 99b0ff4e1a
2 changed files with 33 additions and 4 deletions

View file

@ -29,6 +29,9 @@ except ModuleNotFoundError:
_NESTING_FAULTS = (RecursionError,)
_CONTAINERS = (list, tuple, dict)
class CultCachePayloadDepthError(ValueError):
"""A payload nests deeper than `MAX_PAYLOAD_DEPTH`. Names the type, the depth reached and the limit; never the payload."""
@ -43,15 +46,13 @@ class CultCachePayloadDepthError(ValueError):
def _refuse_past_depth_limit(tree: Any, type_name: str, direction: str) -> None:
"""The one nesting count. Walks a payload tree iteratively (lists, tuples and dicts are the containers), refusing
the first container past `MAX_PAYLOAD_DEPTH`."""
stack = [(tree, 1)]
stack = [(tree, 1)] if isinstance(tree, _CONTAINERS) else []
while stack:
node, depth = stack.pop()
if not isinstance(node, (list, tuple, dict)):
continue
if depth > MAX_PAYLOAD_DEPTH:
raise CultCachePayloadDepthError(type_name, direction)
children = chain(node.keys(), node.values()) if isinstance(node, dict) else node
stack.extend((child, depth + 1) for child in children if isinstance(child, (list, tuple, dict)))
stack.extend((child, depth + 1) for child in children if isinstance(child, _CONTAINERS))
def _default_encode(value: Any) -> Any:

View file

@ -504,6 +504,34 @@ class BoundsVectorTests(unittest.TestCase):
document.decode_payload(b"[" * 100000)
self.assertIn("refused on read", str(read.exception))
def test_a_codec_that_exhausts_the_interpreter_stack_is_the_depth_refusal_on_each_side(self) -> None:
def boom(value: object) -> object:
return boom(value)
document = define_document_type("tests.boom", encode=boom, decode=boom)
with self.assertRaises(CultCachePayloadDepthError) as write:
document.encode_payload("x")
self.assertEqual(
str(write.exception), "tests.boom payload nests 257 containers deep; the limit is 256 (refused on write)."
)
with self.assertRaises(CultCachePayloadDepthError) as read:
document.decode_payload(b'"x"')
self.assertEqual(
str(read.exception), "tests.boom payload nests 257 containers deep; the limit is 256 (refused on read)."
)
with self.assertRaises(CultCachePayloadDepthError) as stack:
surface_document.decode_payload(bytes([0x91]) * 2000 + bytes([0xC0]))
self.assertEqual(
str(stack.exception),
"tests.union_surface payload nests 257 containers deep; the limit is 256 (refused on read).",
)
def test_the_validator_sees_the_value_on_write_and_the_decoded_value_on_read(self) -> None:
seen: list[object] = []
document = define_document_type("tests.validated", validate=seen.append)
document.decode_payload(document.encode_payload({"a": 1}))
self.assertEqual(seen, [{"a": 1}, {"a": 1}])
def test_a_cycle_is_refused_as_depth(self) -> None:
loop: list = []
loop.append(loop)