Skip to content
Open
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 CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ dev

**Bugfixes**

- Avoided copying exact ``bytes`` inputs before Huffman decoding.


4.2.0 (2026-06-22)
------------------
Expand Down
8 changes: 3 additions & 5 deletions src/hpack/huffman_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,9 @@ def decode_huffman(huffman_string: bytes | bytearray | memoryview | None) -> byt
flags = 0
decoded_bytes = bytearray()

# Perversely, bytearrays are a lot more convenient across Python 2 and
# Python 3 because they behave *the same way* on both platforms. Given that
# we really do want numerical bytes when we iterate here, let's use a
# bytearray.
huffman_string = bytearray(huffman_string)
# Exact bytes already yield the octets this table expects, so skip the copy.
if type(huffman_string) is not bytes:
huffman_string = bytearray(huffman_string)

# This loop is unrolled somewhat. Because we use a nibble, not a byte, we
# need to handle each nibble twice. We unroll that: it makes the loop body
Expand Down
32 changes: 32 additions & 0 deletions tests/test_huffman.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,21 @@
from hypothesis.strategies import binary


class BytesWithCustomConversion(bytes):
def __bytes__(self):
return b'\xff'


class BytearrayWithCustomConversion(bytearray):
def __bytes__(self):
return b'\xff'


class BytesWithCustomIteration(bytes):
def __iter__(self):
return iter(b'\xff')


class TestHuffman:

def test_request_huffman_decoder(self):
Expand Down Expand Up @@ -34,6 +49,23 @@ def test_request_huffman_encode(self):


class TestHuffmanDecoder:
def test_huffman_decoder_accepts_byte_buffers(self):
encoded = b'\xa8\xeb\x10d\x9c\xbf'
interleaved = bytes(value for byte in encoded for value in (byte, 0))
buffers = (
encoded,
BytesWithCustomConversion(encoded),
BytesWithCustomIteration(encoded),
bytearray(encoded),
BytearrayWithCustomConversion(encoded),
memoryview(encoded),
memoryview(encoded).cast('H'),
memoryview(interleaved)[::2],
)

for buffer in buffers:
assert decode_huffman(buffer) == b'no-cache'

@given(data=binary())
@example(b'\xff')
@example(b'\x5f\xff\xff\xff\xff')
Expand Down