From 097d487261bc51a9ad9d14f3c39b0083fe67ca60 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Sep 2026 15:58:28 +0200 Subject: [PATCH] [api] Cover truncated bodies in the decode integration test and pin the 64 bit field rejection Three malformed frames (a tag with a dangling continuation bit, a length prefix past the payload, a two byte fixed32) must stop the decode loop without taking the connection down; a generator test records that a double field is rejected before it could reach the loop. --- tests/integration/test_api_decode_wire_types.py | 11 +++++++++++ .../components/api/test_api_protobuf_generator.py | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/tests/integration/test_api_decode_wire_types.py b/tests/integration/test_api_decode_wire_types.py index af5857193c..e3a93a46c2 100644 --- a/tests/integration/test_api_decode_wire_types.py +++ b/tests/integration/test_api_decode_wire_types.py @@ -93,6 +93,17 @@ async def test_api_decode_wire_types( await waiter.expect(number_is(-77.5)) assert len(switch_states) == seen + # Truncated bodies stop the decode loop without taking the connection down: a tag with its + # continuation bit set and nothing after it, a length prefix past the end of the payload, + # and a fixed32 with two of its four bytes + await raw.send_raw(SWITCH_COMMAND, key + b"\x80") + await raw.send_raw(SWITCH_COMMAND, key + tag(2, WIRE_LENGTH) + b"\x7f" + b"ab") + await raw.send_raw(SWITCH_COMMAND, tag(1, WIRE_FIXED32) + b"\x01\x02") + await raw.send_raw(SWITCH_COMMAND, key + on) + await waiter.expect(switch_is(True), label="switch on after truncated frames") + await raw.send_raw(SWITCH_COMMAND, key + off) + await waiter.expect(switch_is(False)) + # An unknown field ahead of the known ones is skipped; field 200 needs a two byte tag await raw.send_raw( SWITCH_COMMAND, tag(200, WIRE_VARINT) + encode_varint(300) + key + on diff --git a/tests/unit_tests/components/api/test_api_protobuf_generator.py b/tests/unit_tests/components/api/test_api_protobuf_generator.py index 76e7b453c6..0e0a4f3fbf 100644 --- a/tests/unit_tests/components/api/test_api_protobuf_generator.py +++ b/tests/unit_tests/components/api/test_api_protobuf_generator.py @@ -46,6 +46,7 @@ def _file_with_messages( UINT64 = descriptor_pb2.FieldDescriptorProto.TYPE_UINT64 MESSAGE = descriptor_pb2.FieldDescriptorProto.TYPE_MESSAGE +DOUBLE = descriptor_pb2.FieldDescriptorProto.TYPE_DOUBLE INT64 = descriptor_pb2.FieldDescriptorProto.TYPE_INT64 SINT64 = descriptor_pb2.FieldDescriptorProto.TYPE_SINT64 UINT32 = descriptor_pb2.FieldDescriptorProto.TYPE_UINT32 @@ -256,6 +257,16 @@ def test_repeated_and_message_fields_decode_through_the_same_case_shape( assert lines[-1].strip() == "break;", case +def test_a_fixed64_field_fails_at_generation_time() -> None: + """The decode loop has no 64 bit wire type path, so such a field must never reach it silently.""" + desc = descriptor_pb2.DescriptorProto(name="Wide") + desc.field.add(name="ratio", number=1, type=DOUBLE) + with pytest.raises( + ValueError, match="64-bit type 'double' .*ratio.* not supported" + ): + build_message_type(desc, {}, {"Wide": SOURCE_CLIENT}) + + def test_message_gets_a_single_decode_field_override() -> None: """All wire types of a decoded message land in one decode_field() switch.""" desc = descriptor_pb2.DescriptorProto(name="Mixed")