diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a4544ef..2f8dd44 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,6 +10,8 @@ dev **API Changes (Backward Compatible)** +- Encoding indexed header fields no longer allocates temporary buffers. + **Bugfixes** diff --git a/src/hpack/hpack.py b/src/hpack/hpack.py index 1aff8ab..ebf78f1 100644 --- a/src/hpack/hpack.py +++ b/src/hpack/hpack.py @@ -22,6 +22,8 @@ INDEX_NEVER = b"\x10" INDEX_INCREMENTAL = b"\x40" +_INDEXED_BYTES = tuple(bytes([index | 0x80]) for index in range(127)) + VARINT_MAX_LENGTH = 5 # octets, enough for encoding prefix + uint32 # Precompute 2^i for 1-8 for use in prefix calcs. @@ -354,6 +356,8 @@ def _encode_indexed(self, index: int) -> bytes: """ Encodes a header using the indexed representation. """ + if type(index) is int and 0 <= index < 127 and not log.isEnabledFor(logging.DEBUG): + return _INDEXED_BYTES[index] field = encode_integer(index, 7) field[0] |= 0x80 # we set the top bit return bytes(field) diff --git a/tests/test_hpack.py b/tests/test_hpack.py index e9c170c..317e136 100644 --- a/tests/test_hpack.py +++ b/tests/test_hpack.py @@ -856,3 +856,39 @@ def _prepend_colon(k): assert expected_special == received_special assert expected_boring == received_boring + + +def test_debug_logging_preserves_roundtrip(caplog): + import logging + caplog.set_level(logging.DEBUG, logger='hpack') + encoder, decoder = Encoder(), Decoder() + encoder.header_table_size = 1024 + headers = [(b':method', b'GET'), (b'custom-name', b'value'), + (b'authorization', b'private', True)] + for huffman in (False, True): + assert decoder.decode(encoder.encode(headers, huffman=huffman), raw=True) == [ + (b':method', b'GET'), (b'custom-name', b'value'), + (b'authorization', b'private'), + ] + messages = [record.getMessage() for record in caplog.records] + assert any('SENSITIVE_REDACTED' in message for message in messages) + encoding_records = [record for record in caplog.records if record.getMessage().startswith('Encoding ')] + assert encoding_records + assert all(record.funcName == 'encode_integer' for record in encoding_records) + assert any(message.startswith('Decoded ') for message in messages) + + +@pytest.mark.parametrize('index, wire', [ + (0, b'\x80'), (1, b'\x81'), (126, b'\xfe'), + (127, b'\xff\x00'), (128, b'\xff\x01'), (255, b'\xff\x80\x01'), + (True, b'\x81'), +]) +def test_indexed_encoding_at_prefix_boundary(index, wire): + assert Encoder()._encode_indexed(index) == wire + + +def test_indexed_encoding_invalid_values(): + with pytest.raises(ValueError, match='Can only encode positive integers, got -1'): + Encoder()._encode_indexed(-1) + with pytest.raises(TypeError): + Encoder()._encode_indexed(1.5)