diff --git a/src/Types/DescribedList.cs b/src/Types/DescribedList.cs index 4b68fee2..eae1731a 100644 --- a/src/Types/DescribedList.cs +++ b/src/Types/DescribedList.cs @@ -401,22 +401,17 @@ internal override void DecodeValue(ByteBuffer buffer) internal override void EncodeValue(ByteBuffer buffer) { - if (this.fields == 0) + if (this.fieldCount == 0) { AmqpBitConverter.WriteUByte(buffer, FormatCode.List0); } else { - // Count non-null fields by removing leading zeros - int count = 0; - int temp = this.fields; - while (temp > 0) - { - count++; - temp <<= 1; - } - - count = 32 - count; + // Always encode all defined fields so receivers that access fields by fixed + // numeric index (e.g. Python _pyamqp) get the full-length list regardless of + // which fields are set. This matches real broker behaviour and satisfies + // AMQP 1.0 spec section 1.4 (trailing nulls are permitted). + int count = this.fieldCount; int pos = buffer.WritePos; AmqpBitConverter.WriteUByte(buffer, 0); AmqpBitConverter.WriteULong(buffer, 0); diff --git a/test/Common/EncoderTests.cs b/test/Common/EncoderTests.cs index b8b143d5..acd0c73e 100644 --- a/test/Common/EncoderTests.cs +++ b/test/Common/EncoderTests.cs @@ -74,5 +74,61 @@ public void EncoderNestedNestedListTest() Assert.AreEqual(1, subList.Count); Assert.AreEqual("test", subList[0]); } + + // Verify that every AMQP 1.0 performative is encoded with its full field count even + // when only mandatory fields are set. Receivers that access fields by fixed numeric + // index (e.g. Python _pyamqp) must see the complete list length. + [TestMethod] + public void PerformativesEncodeFullFieldCount() + { + // (performative, expected field count per AMQP 1.0 spec) + var cases = new (global::Amqp.Types.DescribedList frame, int expectedCount)[] + { + (new global::Amqp.Framing.Open { ContainerId = "test" }, 10), + (new global::Amqp.Framing.Begin { NextOutgoingId = 0, IncomingWindow = 100, OutgoingWindow = 100 }, 8), + (new global::Amqp.Framing.Attach { LinkName = "test", Handle = 0, Role = true }, 14), + (new global::Amqp.Framing.Flow(), 11), + (new global::Amqp.Framing.Transfer { Handle = 0 }, 11), + (new global::Amqp.Framing.Dispose { Role = false }, 6), + (new global::Amqp.Framing.Detach { Handle = 0 }, 3), + (new global::Amqp.Framing.End(), 1), + (new global::Amqp.Framing.Close(), 1), + }; + + foreach (var (frame, expectedCount) in cases) + { + var buffer = new ByteBuffer(512, true); + frame.Encode(buffer); + + // Skip described-type prefix: 0x00 + ulong format code + descriptor value + AmqpBitConverter.ReadUByte(buffer); // 0x00 (described type) + byte ulongFc = AmqpBitConverter.ReadUByte(buffer); + if (ulongFc == 0x53) // smallulong + AmqpBitConverter.ReadUByte(buffer); + else // 0x80 ulong + AmqpBitConverter.ReadULong(buffer); + + // Read list header and extract count + byte listFc = AmqpBitConverter.ReadUByte(buffer); + int count; + if (listFc == 0x45) // list0 — empty + { + count = 0; + } + else if (listFc == 0xc0) // list8 — size(1) + count(1) + { + AmqpBitConverter.ReadUByte(buffer); // size + count = AmqpBitConverter.ReadUByte(buffer); + } + else // list32 (0xd0) — size(4) + count(4) + { + AmqpBitConverter.ReadUInt(buffer); // size + count = (int)AmqpBitConverter.ReadUInt(buffer); + } + + Assert.AreEqual(expectedCount, count, + $"{frame.GetType().Name} expected {expectedCount} fields but encoded {count}"); + } + } } }