Skip to content

Commit 3d2cb34

Browse files
fix: reject UNIXTIME tag in outgoing params tuple
UNIXTIME is a server-to-client-only type — the C server's tuple_decoder has no inbound case for DQLITE_UNIXTIME, so a request frame with that tag would be rejected mid-parse. Encoder inference never picks UNIXTIME in practice, but guard encode_params_tuple defensively so any future caller threading an explicit UNIXTIME hint gets a clear EncodeError rather than a confusing server-side error. Encode_value continues to support UNIXTIME explicitly for server-to- client roundtrip tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a0a90cd commit 3d2cb34

4 files changed

Lines changed: 36 additions & 3 deletions

File tree

src/dqlitewire/constants.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,11 @@ class ValueType(IntEnum):
7171
TEXT = 3
7272
BLOB = 4
7373
NULL = 5
74-
UNIXTIME = 9 # Unix time (deprecated, maps to INTEGER)
74+
# Unix time (deprecated, maps to INTEGER).
75+
# Server-to-client ONLY: the C server's tuple_decoder has no inbound
76+
# case for DQLITE_UNIXTIME, so this tag must never appear on outgoing
77+
# parameters. encode_params_tuple enforces this defensively.
78+
UNIXTIME = 9
7579
ISO8601 = 10 # ISO8601 string (maps to TEXT)
7680
BOOLEAN = 11 # Boolean (maps to INTEGER)
7781

src/dqlitewire/tuples.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,16 @@ def encode_params_tuple(params: Sequence[Any], schema: int = 0, buffer_offset: i
5454
types: list[int] = []
5555
values: list[bytes] = []
5656

57-
for param in params:
57+
for i, param in enumerate(params):
5858
encoded, value_type = encode_value(param)
59+
# Defense in depth: UNIXTIME is server-to-client only. Inference
60+
# never picks it, but guard against a future caller threading an
61+
# explicit UNIXTIME hint through this path.
62+
if value_type == ValueType.UNIXTIME:
63+
raise EncodeError(
64+
f"Parameter {i} resolved to UNIXTIME, which the server "
65+
"cannot decode; use INTEGER or ISO8601 instead"
66+
)
5967
types.append(value_type)
6068
values.append(encoded)
6169

src/dqlitewire/types.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,13 @@ def encode_value(value: Any, value_type: ValueType | None = None) -> tuple[bytes
276276
raise EncodeError(f"Expected bool or int for BOOLEAN, got {type(value).__name__}")
277277
return encode_uint64(1 if value else 0), value_type
278278
elif value_type in (ValueType.INTEGER, ValueType.UNIXTIME):
279+
# Note: UNIXTIME is a server-to-client-only type (the C server's
280+
# tuple_decoder has no inbound case for DQLITE_UNIXTIME). Explicit
281+
# UNIXTIME encoding here is supported for roundtrip tests that
282+
# simulate server-to-client frames; encode_params_tuple, which is
283+
# the outgoing-params path, uses inference and never picks
284+
# UNIXTIME, so the server-rejection case cannot arise via the
285+
# documented client API.
279286
if isinstance(value, bool):
280287
value = 1 if value else 0
281288
if not isinstance(value, int):

tests/test_tuples.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import pytest
66

77
from dqlitewire.constants import ValueType
8-
from dqlitewire.exceptions import DecodeError
8+
from dqlitewire.exceptions import DecodeError, EncodeError
99
from dqlitewire.tuples import (
1010
RowMarker,
1111
decode_params_tuple,
@@ -23,6 +23,20 @@ def test_encode_empty(self) -> None:
2323
encoded = encode_params_tuple([])
2424
assert encoded == b""
2525

26+
def test_params_tuple_rejects_unixtime_tag(self, monkeypatch: pytest.MonkeyPatch) -> None:
27+
"""Defense in depth: if anything ever produces a UNIXTIME tag on an
28+
outgoing param, encode_params_tuple must reject it — the C server
29+
cannot decode DQLITE_UNIXTIME inbound.
30+
"""
31+
from dqlitewire import tuples as tuples_mod
32+
33+
def fake_encode(value: object, value_type: object = None) -> tuple:
34+
return b"\x00" * 8, ValueType.UNIXTIME
35+
36+
monkeypatch.setattr(tuples_mod, "encode_value", fake_encode)
37+
with pytest.raises(EncodeError, match="UNIXTIME"):
38+
encode_params_tuple([1_700_000_000])
39+
2640
def test_encode_single_integer(self) -> None:
2741
encoded = encode_params_tuple([42])
2842
assert len(encoded) == 16

0 commit comments

Comments
 (0)