Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/compact_encoding/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from .codec import Codec, CompactError, OutOfBounds, decode, encode
from .fixed import fixed, fixed32, fixed64
from .float import float32, float64
from .frame import frame
from .integer import (
int24,
int40,
Expand Down Expand Up @@ -39,6 +40,7 @@
"fixed64",
"float32",
"float64",
"frame",
"int",
"int24",
"int40",
Expand Down
31 changes: 31 additions & 0 deletions src/compact_encoding/frame.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from . import integer
from .state import State


class _Frame:
def __init__(self, codec):
self._codec = codec

def preencode(self, state, m):
end = state.end
self._codec.preencode(state, m)
integer.uint.preencode(state, state.end - end)

def encode(self, state, m):
dummy = State()
self._codec.preencode(dummy, m)
integer.uint.encode(state, dummy.end)
self._codec.encode(state, m)

def decode(self, state):
end = state.end
length = integer.uint.decode(state)
state.end = state.start + length
m = self._codec.decode(state)
state.start = state.end
state.end = end
return m


def frame(codec):
return _Frame(codec)
59 changes: 59 additions & 0 deletions tests/test_frame.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import pytest

import compact_encoding as cenc
from compact_encoding.codec import OutOfBounds

# frame(uint): uint(len) prefix + inner uint bytes.
# 5 -> inner uint(5) = 05 (1 byte) -> len 01 -> 0105
# 0 -> inner uint(0) = 00 (1 byte) -> len 01 -> 0100
# 300 -> inner uint(300) = fd2c01 (3 bytes) -> len 03 -> 03fd2c01
FRAME_UINT_CASES = [
(5, "0105"),
(0, "0100"),
(300, "03fd2c01"),
]


@pytest.mark.parametrize("value,hexbytes", FRAME_UINT_CASES)
def test_frame_uint_encode(value, hexbytes):
assert cenc.encode(cenc.frame(cenc.uint), value).hex() == hexbytes


@pytest.mark.parametrize("value,hexbytes", FRAME_UINT_CASES)
def test_frame_uint_decode(value, hexbytes):
assert cenc.decode(cenc.frame(cenc.uint), bytes.fromhex(hexbytes)) == value


def test_frame_buffer_matches_length_prefix():
# A framed buffer is a length prefix around c.buffer's own bytes: the
# frame length equals the full inner encoding (uint-len + raw bytes).
payload = b"\x01\x02\x03"
inner = cenc.encode(cenc.buffer, payload) # 03010203 (4 bytes)
framed = cenc.encode(cenc.frame(cenc.buffer), payload)
assert framed.hex() == "04" + inner.hex() # len 04 + 03010203
assert cenc.decode(cenc.frame(cenc.buffer), framed) == payload


def test_frame_restores_cursor_for_following_field():
# Encode a framed value then a plain uint; both must decode back in order,
# proving decode restores state.end and leaves state.start after the frame.
from compact_encoding.state import State

fr = cenc.frame(cenc.uint)
state = State()
fr.preencode(state, 300)
cenc.uint.preencode(state, 42)
state.allocate()
fr.encode(state, 300)
cenc.uint.encode(state, 42)

verify = State(bytes(state.buffer))
assert fr.decode(verify) == 300
assert cenc.uint.decode(verify) == 42


def test_frame_decode_truncated_raises():
# Inner uint's continuation byte (0xfd) demands 2 more bytes that
# aren't present; verified this also raises against the JS reference.
with pytest.raises(OutOfBounds):
cenc.decode(cenc.frame(cenc.uint), bytes.fromhex("01fd"))
1 change: 1 addition & 0 deletions tests/test_roundtrip.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
(cenc.fixed32, bytes(range(32))),
(cenc.fixed64, bytes(range(64))),
(cenc.json, {"a": [1, 2], "b": None}),
(cenc.frame(cenc.uint), 42),
]


Expand Down
Loading