Skip to content

Commit e522b05

Browse files
Reject non-bit BOOLEAN values in wire decoder
decode_value for BOOLEAN previously called bool(decode_uint64(...)), silently coercing any uint64 to True/False. That was asymmetric with encode_value (which rejects integers outside {0, 1}) and made round-trips lossy: uint64=2 decoded to True, re-encoded as 1, so the bytes changed. Require the wire value to be exactly 0 or 1 and raise DecodeError otherwise, matching the existing strictness for row markers and other enforced wire invariants.
1 parent 62c7cac commit e522b05

2 files changed

Lines changed: 25 additions & 1 deletion

File tree

src/dqlitewire/types.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,14 @@ def decode_value(data: bytes | memoryview, value_type: ValueType) -> tuple[Any,
327327
Returns (value, bytes_consumed).
328328
"""
329329
if value_type == ValueType.BOOLEAN:
330-
return bool(decode_uint64(data)), 8
330+
# Symmetric with encode_value: BOOLEAN must be exactly 0 or 1 on
331+
# the wire. Silently coercing any uint64 to True/False would make
332+
# round-trips lossy (encoding restores 1/0) and hide malformed
333+
# frames produced by a broken or hostile peer.
334+
raw = decode_uint64(data)
335+
if raw not in (0, 1):
336+
raise DecodeError(f"BOOLEAN wire value must be 0 or 1, got {raw}")
337+
return bool(raw), 8
331338
elif value_type == ValueType.INTEGER:
332339
return decode_int64(data), 8
333340
elif value_type == ValueType.UNIXTIME:

tests/test_types.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,23 @@ def test_decode_boolean(self) -> None:
435435
assert value is True
436436
assert consumed == 8
437437

438+
def test_decode_boolean_zero(self) -> None:
439+
value, consumed = decode_value(encode_int64(0), ValueType.BOOLEAN)
440+
assert value is False
441+
assert consumed == 8
442+
443+
def test_decode_boolean_rejects_non_bit(self) -> None:
444+
"""Decoder is symmetric with encoder: only {0, 1} are valid BOOLEAN
445+
wire values. Silently coercing any uint64 via ``bool(...)`` would
446+
make round-trips lossy (e.g., uint64=2 → True → re-encodes as 1)
447+
and would mask protocol violations that we want to surface.
448+
"""
449+
from dqlitewire.exceptions import DecodeError
450+
451+
for bad in (2, 3, 255, 2**63 - 1):
452+
with pytest.raises(DecodeError, match="BOOLEAN"):
453+
decode_value(encode_int64(bad), ValueType.BOOLEAN)
454+
438455
def test_encode_decode_iso8601_roundtrips_as_text(self) -> None:
439456
"""ISO8601 is stored as text at the wire level — datetime conversion
440457
lives in the driver/DBAPI layer, matching C and Go's split.

0 commit comments

Comments
 (0)