This is an automated email from the ASF dual-hosted git repository. astitcher pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/qpid-proton.git
commit 5aa83a6d162fc4bf405149756b9f67dcfc43d0cc Author: Andrew Stitcher <[email protected]> AuthorDate: Wed Dec 3 19:37:13 2025 -0500 PROTON-1779: Omit empty HEADERS and PROPERTIES message sections --- c/src/core/message.c | 8 ++--- c/tests/message_test.cpp | 12 +++++++ c/tools/codec-generator/generate.py | 35 +++++++++++++++++-- c/tools/codec-generator/specs.json | 4 +-- python/tests/proton_tests/message.py | 67 +++++++++++++++++------------------- 5 files changed, 81 insertions(+), 45 deletions(-) diff --git a/c/src/core/message.c b/c/src/core/message.c index 787a5ba09..a4ddb5f7a 100644 --- a/c/src/core/message.c +++ b/c/src/core/message.c @@ -855,8 +855,8 @@ int pn_message_encode(pn_message_t *msg, char *bytes, size_t *isize) size_t remaining = *isize; size_t total = 0; - /* "DL[?o?B?I?o?I]" */ - size_t last_size = pn_amqp_encode_bytes_DLEQoQBQIQoQIe(bytes, remaining, AMQP_DESC_HEADER, + /* "DL[?o?B?I?o?I]!" */ + size_t last_size = pn_amqp_encode_bytes_DLEQoQBQIQoQIeX(bytes, remaining, AMQP_DESC_HEADER, msg->durable, msg->durable, msg->priority!=AMQP_HEADER_PRIORITY_DEFAULT, msg->priority, (bool)msg->ttl, msg->ttl, @@ -887,10 +887,10 @@ int pn_message_encode(pn_message_t *msg, char *bytes, size_t *isize) total += last_size; } - /* "DL[CzSSSCss?t?tS?IS]" */ + /* "DL[azSSSass?t?tS?IS]!" */ pn_atom_t id = pn_message_get_id(msg); pn_atom_t correlation_id = pn_message_get_correlation_id(msg); - last_size = pn_amqp_encode_bytes_DLEazSSSassQtQtSQISe(bytes, remaining, AMQP_DESC_PROPERTIES, + last_size = pn_amqp_encode_bytes_DLEazSSSassQtQtSQISeX(bytes, remaining, AMQP_DESC_PROPERTIES, &id, pn_string_size(msg->user_id), pn_string_get(msg->user_id), pn_string_bytes(msg->address), diff --git a/c/tests/message_test.cpp b/c/tests/message_test.cpp index 5d76abe4f..1d3dea9d1 100644 --- a/c/tests/message_test.cpp +++ b/c/tests/message_test.cpp @@ -29,6 +29,8 @@ using namespace pn_test; TEST_CASE("message_overflow_error") { pn_message_t *message = pn_message(); + // Set a field so message is non-empty (empty messages encode to 0 bytes) + pn_message_set_durable(message, true); char buf[6]; size_t size = 6; @@ -38,6 +40,16 @@ TEST_CASE("message_overflow_error") { pn_message_free(message); } +TEST_CASE("message_empty_encodes") { + pn_message_t *message = pn_message(); + char buf[64]; + size_t size = 64; + + int err = pn_message_encode(message, buf, &size); + CHECK(0 == err); + pn_message_free(message); +} + static void recode(pn_message_t *dst, pn_message_t *src) { pn_rwbytes_t buf = {0}; int size = pn_message_encode2(src, &buf); diff --git a/c/tools/codec-generator/generate.py b/c/tools/codec-generator/generate.py index da573db13..c9d8322ac 100644 --- a/c/tools/codec-generator/generate.py +++ b/c/tools/codec-generator/generate.py @@ -170,6 +170,31 @@ class DescListNode(ListNode): ] +class DescListOmitIfEmptyNode(ListNode): + """Described list that is omitted entirely if all fields would be null (resulting in LIST0)""" + def __init__(self, l: List[ASTNode]): + super().__init__('described_list_omit_empty', l, ['uint64_t']) + + def gen_emit_code(self, prefix: List[str], first_arg: int, indent: int) -> List[str]: + args = self.gen_args(first_arg) + return [ + f'{self.mk_indent(indent)}size_t section_start = emitter->position;', + f'{self.mk_indent(indent)}emit_descriptor({", ".join(prefix+args)});', + f'{self.mk_indent(indent)}for (bool small_encoding = true; ; small_encoding = false) {{', + f'{self.mk_indent(indent+1)}pni_compound_context c = ' + f'{self.mk_funcall("emit_list", prefix+["small_encoding", "true"])};', + f'{self.mk_indent(indent+1)}pni_compound_context compound = c;', + *(self.gen_emit_list_code(prefix, first_arg+len(args), indent + 1)), + f'{self.mk_indent(indent+1)}if (compound.count == 0) {{', + f'{self.mk_indent(indent+2)}emitter->position = section_start;', + f'{self.mk_indent(indent+2)}break;', + f'{self.mk_indent(indent+1)}}}', + f'{self.mk_indent(indent+1)}{self.mk_funcall("emit_end_list", prefix+["small_encoding"])};', + f'{self.mk_indent(indent+1)}if (encode_succeeded({", ".join(prefix)})) break;', + f'{self.mk_indent(indent)}}}', + ] + + class DescListIgnoreTypeNode(ListNode): def __init__(self, l: List[ASTNode]): super().__init__('described_unknown_list', l, []) @@ -274,7 +299,11 @@ def parse_item(format: str) -> Tuple[ASTNode, str]: b, rest = expect_char(rest, ']') if not b: raise ParseError(format) - return DescListNode(l), rest + # Check for ! suffix - omit entire described list if empty + if rest.startswith('!'): + return DescListOmitIfEmptyNode(l), rest[1:] + else: + return DescListNode(l), rest elif format.startswith('D.['): l, rest = parse_list(format[3:]) b, rest = expect_char(rest, ']') @@ -354,10 +383,10 @@ def parse_item(format: str) -> Tuple[ASTNode, str]: raise ParseError(format) -# Need to translate '@[]*?.' to legal identifier characters +# Need to translate '@[]*?.!' to legal identifier characters # These will be fairly arbitrary and just need to avoid already used codes def make_legal_identifier(s: str) -> str: - subs = {'@': 'A', '[': 'E', ']': 'e', '*': 'j', '.': 'q', '?': 'Q'} + subs = {'@': 'A', '[': 'E', ']': 'e', '*': 'j', '.': 'q', '?': 'Q', '!': 'X'} r = '' for c in s: if c in subs: diff --git a/c/tools/codec-generator/specs.json b/c/tools/codec-generator/specs.json index c77f26e1d..2def140f8 100644 --- a/c/tools/codec-generator/specs.json +++ b/c/tools/codec-generator/specs.json @@ -6,7 +6,7 @@ "DL[c]", "DL[?HIIII]", "DL[?IIII?I?I?In?o]", - "DL[?o?B?I?o?I]", + "DL[?o?B?I?o?I]!", "DL[@T[*s]]", "DL[Bz]", "DL[I?oc]", @@ -16,7 +16,7 @@ "DL[SS?I?H?InnMMR]", "DL[S]", "DL[Z]", - "DL[azSSSass?t?tS?IS]", + "DL[azSSSass?t?tS?IS]!", "DL[oI?I?o?DL[]]", "DL[oIn?od]", "DL[szS]" diff --git a/python/tests/proton_tests/message.py b/python/tests/proton_tests/message.py index 5876da0d7..f0b5bb1de 100644 --- a/python/tests/proton_tests/message.py +++ b/python/tests/proton_tests/message.py @@ -224,20 +224,36 @@ class CodecTest(Test): assert self.msg.subject == msg2.subject, (self.msg.subject, msg2.subject) assert self.msg.body == msg2.body, (self.msg.body, msg2.body) - def testExpiryEncodeAsNull(self): - self.msg.group_id = "A" # Force creation and expiry fields to be present - data = self.msg.encode() - + def _skip_header_section(self, data): + """Skip HEADER section (0x70) if present, then return the next section. + HEADER (0x70) may or may not be present depending on whether it has non-default values.""" decoder = Data() + if not data: + return None - # Skip past the headers + # Decode first section consumed = decoder.decode(data) - decoder.clear() - data = data[consumed:] + described = decoder.get_py_described() - decoder.decode(data) - dproperties = decoder.get_py_described() + # If first section is HEADER (0x70), skip it + if described.descriptor == 0x70: # HEADER + decoder.clear() + data = data[consumed:] + if not data: + return None + # Decode next section + consumed = decoder.decode(data) + described = decoder.get_py_described() + + return described + + def testExpiryEncodeAsNull(self): + self.msg.group_id = "A" # Force creation and expiry fields to be present + data = self.msg.encode() + + dproperties = self._skip_header_section(data) # Check we've got the correct described list + assert dproperties is not None, "PROPERTIES section not found" assert dproperties.descriptor == 0x73, (dproperties.descriptor) properties = dproperties.value @@ -247,16 +263,9 @@ class CodecTest(Test): self.msg.group_id = "A" # Force creation and expiry fields to be present data = self.msg.encode() - decoder = Data() - - # Skip past the headers - consumed = decoder.decode(data) - decoder.clear() - data = data[consumed:] - - decoder.decode(data) - dproperties = decoder.get_py_described() + dproperties = self._skip_header_section(data) # Check we've got the correct described list + assert dproperties is not None, "PROPERTIES section not found" assert dproperties.descriptor == 0x73, (dproperties.descriptor) properties = dproperties.value @@ -266,16 +275,9 @@ class CodecTest(Test): self.msg.reply_to_group_id = "R" # Force group_id and group_sequence fields to be present data = self.msg.encode() - decoder = Data() - - # Skip past the headers - consumed = decoder.decode(data) - decoder.clear() - data = data[consumed:] - - decoder.decode(data) - dproperties = decoder.get_py_described() + dproperties = self._skip_header_section(data) # Check we've got the correct described list + assert dproperties is not None, "PROPERTIES section not found" assert dproperties.descriptor == 0x73, (dproperties.descriptor) properties = dproperties.value @@ -287,16 +289,9 @@ class CodecTest(Test): self.msg.reply_to_group_id = "R" # Force group_id and group_sequence fields to be present data = self.msg.encode() - decoder = Data() - - # Skip past the headers - consumed = decoder.decode(data) - decoder.clear() - data = data[consumed:] - - decoder.decode(data) - dproperties = decoder.get_py_described() + dproperties = self._skip_header_section(data) # Check we've got the correct described list + assert dproperties is not None, "PROPERTIES section not found" assert dproperties.descriptor == 0x73, (dproperties.descriptor) properties = dproperties.value --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
