diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a4544ef..d789873 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -12,6 +12,8 @@ dev **Bugfixes** +- Avoided copying exact ``bytes`` inputs before Huffman decoding. + 4.2.0 (2026-06-22) ------------------ diff --git a/src/hpack/huffman_table.py b/src/hpack/huffman_table.py index 3083e92..5c8d1d6 100644 --- a/src/hpack/huffman_table.py +++ b/src/hpack/huffman_table.py @@ -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 diff --git a/tests/test_huffman.py b/tests/test_huffman.py index 0ea789f..b960cdc 100644 --- a/tests/test_huffman.py +++ b/tests/test_huffman.py @@ -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): @@ -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')