From 99b0ff4e1a3577b89e44f8d9facb2191c4e86c3d Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 11 Oct 2026 10:20:35 +0200 Subject: [PATCH] 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 --- .../src/cultcache_py/documents.py | 9 +++--- .../cultcache-py/tests/test_union_parity.py | 28 +++++++++++++++++++ 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/packages/cultcache-py/src/cultcache_py/documents.py b/packages/cultcache-py/src/cultcache_py/documents.py index c1e86fe1..297101b2 100644 --- a/packages/cultcache-py/src/cultcache_py/documents.py +++ b/packages/cultcache-py/src/cultcache_py/documents.py @@ -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: diff --git a/packages/cultcache-py/tests/test_union_parity.py b/packages/cultcache-py/tests/test_union_parity.py index f7378ccb..4abab1f1 100644 --- a/packages/cultcache-py/tests/test_union_parity.py +++ b/packages/cultcache-py/tests/test_union_parity.py @@ -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)