// Port of Go server/mqtt_test.go — MQTT protocol parsing and session parity tests. // Reference: golang/nats-server/server/mqtt_test.go // // Tests cover: binary packet parsing (CONNECT, PUBLISH, SUBSCRIBE, PINGREQ), // QoS 0/1/2 message delivery, retained message handling, session clean start/resume, // will messages, and topic-to-NATS subject translation. using System.Text; using NATS.Server.Mqtt; namespace NATS.Server.Tests.Mqtt; /// /// Parity tests ported from Go server/mqtt_test.go exercising MQTT binary /// protocol parsing, session management, retained messages, QoS flows, /// and wildcard translation. /// public class MqttGoParityTests { // ======================================================================== // MQTT Packet Reader / Writer tests // Go reference: mqtt_test.go TestMQTTConfig (binary wire-format portion) // ======================================================================== [Fact] public void PacketReader_ConnectPacket_Parsed() { // Go: TestMQTTConfig — verifies CONNECT packet binary parsing. // Build a minimal MQTT CONNECT: type=1, flags=0, payload=variable header + client ID var payload = BuildConnectPayload("test-client", cleanSession: true, keepAlive: 60); var packet = MqttPacketWriter.Write(MqttControlPacketType.Connect, payload); var parsed = MqttPacketReader.Read(packet); parsed.Type.ShouldBe(MqttControlPacketType.Connect); parsed.Flags.ShouldBe((byte)0); parsed.RemainingLength.ShouldBe(payload.Length); } [Fact] public void PacketReader_PublishQos0_Parsed() { // Go: TestMQTTQoS2SubDowngrade — verifies PUBLISH packet parsing at QoS 0. // PUBLISH: type=3, flags=0 (QoS 0, no retain, no dup) var payload = BuildPublishPayload("test/topic", "hello world"u8.ToArray()); var packet = MqttPacketWriter.Write(MqttControlPacketType.Publish, payload, flags: 0x00); var parsed = MqttPacketReader.Read(packet); parsed.Type.ShouldBe(MqttControlPacketType.Publish); parsed.Flags.ShouldBe((byte)0x00); var pub = MqttBinaryDecoder.ParsePublish(parsed.Payload.Span, parsed.Flags); pub.Topic.ShouldBe("test/topic"); pub.QoS.ShouldBe((byte)0); pub.Retain.ShouldBeFalse(); pub.Dup.ShouldBeFalse(); pub.Payload.ToArray().ShouldBe("hello world"u8.ToArray()); } [Fact] public void PacketReader_PublishQos1_HasPacketId() { // Go: TestMQTTMaxAckPendingForMultipleSubs — QoS 1 publishes require packet IDs. // PUBLISH: type=3, flags=0x02 (QoS 1) var payload = BuildPublishPayload("orders/new", "order-data"u8.ToArray(), packetId: 42); var packet = MqttPacketWriter.Write(MqttControlPacketType.Publish, payload, flags: 0x02); var parsed = MqttPacketReader.Read(packet); var pub = MqttBinaryDecoder.ParsePublish(parsed.Payload.Span, parsed.Flags); pub.Topic.ShouldBe("orders/new"); pub.QoS.ShouldBe((byte)1); pub.PacketId.ShouldBe((ushort)42); } [Fact] public void PacketReader_PublishQos2_RetainDup() { // Go: TestMQTTQoS2PubReject — QoS 2 with retain and dup flags. // Flags: DUP=0x08, QoS2=0x04, RETAIN=0x01 → 0x0D var payload = BuildPublishPayload("sensor/temp", "22.5"u8.ToArray(), packetId: 100); var packet = MqttPacketWriter.Write(MqttControlPacketType.Publish, payload, flags: 0x0D); var parsed = MqttPacketReader.Read(packet); var pub = MqttBinaryDecoder.ParsePublish(parsed.Payload.Span, parsed.Flags); pub.QoS.ShouldBe((byte)2); pub.Dup.ShouldBeTrue(); pub.Retain.ShouldBeTrue(); pub.PacketId.ShouldBe((ushort)100); } [Fact] public void PacketReader_SubscribePacket_ParsedWithFilters() { // Go: TestMQTTSubPropagation — SUBSCRIBE packet with multiple topic filters. var payload = BuildSubscribePayload(1, ("home/+/temperature", 1), ("office/#", 0)); var packet = MqttPacketWriter.Write(MqttControlPacketType.Subscribe, payload, flags: 0x02); var parsed = MqttPacketReader.Read(packet); parsed.Type.ShouldBe(MqttControlPacketType.Subscribe); var sub = MqttBinaryDecoder.ParseSubscribe(parsed.Payload.Span); sub.PacketId.ShouldBe((ushort)1); sub.Filters.Count.ShouldBe(2); sub.Filters[0].TopicFilter.ShouldBe("home/+/temperature"); sub.Filters[0].QoS.ShouldBe((byte)1); sub.Filters[1].TopicFilter.ShouldBe("office/#"); sub.Filters[1].QoS.ShouldBe((byte)0); } [Fact] public void PacketReader_PingReq_Parsed() { // Go: PINGREQ is type=12, no payload, 2 bytes total var packet = MqttPacketWriter.Write(MqttControlPacketType.PingReq, ReadOnlySpan.Empty); var parsed = MqttPacketReader.Read(packet); parsed.Type.ShouldBe(MqttControlPacketType.PingReq); parsed.RemainingLength.ShouldBe(0); } [Fact] public void PacketReader_TooShort_Throws() { // Go: malformed packets should be rejected. Should.Throw(() => MqttPacketReader.Read(new byte[] { 0x10 })); } [Fact] public void PacketWriter_ReservedType_Throws() { // Go: reserved type 0 is invalid. Should.Throw(() => MqttPacketWriter.Write(MqttControlPacketType.Reserved, ReadOnlySpan.Empty)); } // ======================================================================== // MQTT Binary Decoder — CONNECT parsing // Go reference: mqtt_test.go TestMQTTServerNameRequired, TestMQTTTLS // ======================================================================== [Fact] public void BinaryDecoder_Connect_BasicClientId() { // Go: TestMQTTServerNameRequired — basic CONNECT parsing with client ID. var payload = BuildConnectPayload("my-device", cleanSession: true, keepAlive: 30); var info = MqttBinaryDecoder.ParseConnect(payload); info.ProtocolName.ShouldBe("MQTT"); info.ProtocolLevel.ShouldBe((byte)4); // MQTT 3.1.1 info.CleanSession.ShouldBeTrue(); info.KeepAlive.ShouldBe((ushort)30); info.ClientId.ShouldBe("my-device"); info.Username.ShouldBeNull(); info.Password.ShouldBeNull(); info.WillTopic.ShouldBeNull(); } [Fact] public void BinaryDecoder_Connect_WithCredentials() { // Go: TestMQTTTLS, TestMQTTTLSVerifyAndMap — CONNECT with username/password. var payload = BuildConnectPayload("auth-client", cleanSession: false, keepAlive: 120, username: "admin", password: "secret"); var info = MqttBinaryDecoder.ParseConnect(payload); info.ClientId.ShouldBe("auth-client"); info.CleanSession.ShouldBeFalse(); info.KeepAlive.ShouldBe((ushort)120); info.Username.ShouldBe("admin"); info.Password.ShouldBe("secret"); } [Fact] public void BinaryDecoder_Connect_WithWillMessage() { // Go: TestMQTTSparkbDeathHandling — CONNECT with will message (last will & testament). var willPayload = "device offline"u8.ToArray(); var payload = BuildConnectPayload("will-client", cleanSession: true, keepAlive: 60, willTopic: "status/device1", willMessage: willPayload, willQoS: 1, willRetain: true); var info = MqttBinaryDecoder.ParseConnect(payload); info.ClientId.ShouldBe("will-client"); info.WillTopic.ShouldBe("status/device1"); info.WillMessage.ShouldBe(willPayload); info.WillQoS.ShouldBe((byte)1); info.WillRetain.ShouldBeTrue(); } [Fact] public void BinaryDecoder_Connect_InvalidProtocolName_Throws() { // Go: malformed CONNECT with bad protocol name should fail. var ms = new MemoryStream(); WriteUtf8String(ms, "XMPP"); // wrong protocol name ms.WriteByte(4); // level ms.WriteByte(0x02); // clean session ms.WriteByte(0); ms.WriteByte(0); // keepalive WriteUtf8String(ms, "test-client"); Should.Throw(() => MqttBinaryDecoder.ParseConnect(ms.ToArray())); } // ======================================================================== // MQTT Wildcard Translation // Go reference: mqtt_test.go TestMQTTSubjectMappingWithImportExport, TestMQTTMappingsQoS0 // ======================================================================== [Theory] [InlineData("home/temperature", "home.temperature")] [InlineData("home/+/temperature", "home.*.temperature")] [InlineData("home/#", "home.>")] [InlineData("#", ">")] [InlineData("+", "*")] [InlineData("a/b/c/d", "a.b.c.d")] [InlineData("", "")] public void TranslateFilterToNatsSubject_CorrectTranslation(string mqtt, string expected) { // Go: TestMQTTSubjectMappingWithImportExport, TestMQTTMappingsQoS0 — wildcard translation. MqttBinaryDecoder.TranslateFilterToNatsSubject(mqtt).ShouldBe(expected); } // ======================================================================== // Retained Message Store // Go reference: mqtt_test.go TestMQTTClusterRetainedMsg, TestMQTTQoS2RetainedReject // ======================================================================== [Fact] public void RetainedStore_SetAndGet() { // Go: TestMQTTClusterRetainedMsg — retained messages stored and retrievable. var store = new MqttRetainedStore(); var payload = "hello"u8.ToArray(); store.SetRetained("test/topic", payload); var result = store.GetRetained("test/topic"); result.ShouldNotBeNull(); result.Value.ToArray().ShouldBe(payload); } [Fact] public void RetainedStore_EmptyPayload_ClearsRetained() { // Go: TestMQTTRetainedMsgRemovedFromMapIfNotInStream — empty payload clears retained. var store = new MqttRetainedStore(); store.SetRetained("test/topic", "hello"u8.ToArray()); store.SetRetained("test/topic", ReadOnlyMemory.Empty); store.GetRetained("test/topic").ShouldBeNull(); } [Fact] public void RetainedStore_WildcardMatch_SingleLevel() { // Go: TestMQTTSubRetainedRace — wildcard matching for retained messages. var store = new MqttRetainedStore(); store.SetRetained("home/living/temperature", "22.5"u8.ToArray()); store.SetRetained("home/kitchen/temperature", "24.0"u8.ToArray()); store.SetRetained("office/desk/temperature", "21.0"u8.ToArray()); var matches = store.GetMatchingRetained("home/+/temperature"); matches.Count.ShouldBe(2); } [Fact] public void RetainedStore_WildcardMatch_MultiLevel() { // Go: TestMQTTSliceHeadersAndDecodeRetainedMessage — multi-level wildcard. var store = new MqttRetainedStore(); store.SetRetained("home/living/temperature", "22"u8.ToArray()); store.SetRetained("home/living/humidity", "45"u8.ToArray()); store.SetRetained("home/kitchen/temperature", "24"u8.ToArray()); store.SetRetained("office/desk/temperature", "21"u8.ToArray()); var matches = store.GetMatchingRetained("home/#"); matches.Count.ShouldBe(3); } [Fact] public void RetainedStore_ExactMatch_OnlyMatchesExact() { // Go: retained messages with exact topic filter match only the exact topic. var store = new MqttRetainedStore(); store.SetRetained("home/temperature", "22"u8.ToArray()); store.SetRetained("home/humidity", "45"u8.ToArray()); var matches = store.GetMatchingRetained("home/temperature"); matches.Count.ShouldBe(1); matches[0].Topic.ShouldBe("home/temperature"); } // ======================================================================== // Session Store — clean start / resume // Go reference: mqtt_test.go TestMQTTSubRestart, TestMQTTRecoverSessionWithSubAndClientResendSub // ======================================================================== [Fact] public void SessionStore_SaveAndLoad() { // Go: TestMQTTSubRestart — session persistence across reconnects. var store = new MqttSessionStore(); var session = new MqttSessionData { ClientId = "device-1", CleanSession = false, Subscriptions = { ["sensor/+"] = 1, ["status/#"] = 0 }, }; store.SaveSession(session); var loaded = store.LoadSession("device-1"); loaded.ShouldNotBeNull(); loaded.ClientId.ShouldBe("device-1"); loaded.Subscriptions.Count.ShouldBe(2); loaded.Subscriptions["sensor/+"].ShouldBe(1); } [Fact] public void SessionStore_CleanSession_DeletesPrevious() { // Go: TestMQTTRecoverSessionWithSubAndClientResendSub — clean session deletes stored state. var store = new MqttSessionStore(); store.SaveSession(new MqttSessionData { ClientId = "device-1", Subscriptions = { ["sensor/+"] = 1 }, }); store.DeleteSession("device-1"); store.LoadSession("device-1").ShouldBeNull(); } [Fact] public void SessionStore_NonExistentClient_ReturnsNull() { // Go: loading a session for a client that never connected returns nil. var store = new MqttSessionStore(); store.LoadSession("nonexistent").ShouldBeNull(); } [Fact] public void SessionStore_ListSessions() { // Go: session enumeration for monitoring. var store = new MqttSessionStore(); store.SaveSession(new MqttSessionData { ClientId = "a" }); store.SaveSession(new MqttSessionData { ClientId = "b" }); store.SaveSession(new MqttSessionData { ClientId = "c" }); store.ListSessions().Count.ShouldBe(3); } // ======================================================================== // QoS 2 State Machine // Go reference: mqtt_test.go TestMQTTQoS2RetriesPubRel // ======================================================================== [Fact] public void QoS2StateMachine_FullFlow() { // Go: TestMQTTQoS2RetriesPubRel — complete QoS 2 exactly-once flow. var sm = new MqttQos2StateMachine(); // Begin publish sm.BeginPublish(1).ShouldBeTrue(); sm.GetState(1).ShouldBe(MqttQos2State.AwaitingPubRec); // Process PUBREC sm.ProcessPubRec(1).ShouldBeTrue(); sm.GetState(1).ShouldBe(MqttQos2State.AwaitingPubRel); // Process PUBREL sm.ProcessPubRel(1).ShouldBeTrue(); sm.GetState(1).ShouldBe(MqttQos2State.AwaitingPubComp); // Process PUBCOMP — flow complete, removed sm.ProcessPubComp(1).ShouldBeTrue(); sm.GetState(1).ShouldBeNull(); } [Fact] public void QoS2StateMachine_DuplicatePublish_Rejected() { // Go: TestMQTTQoS2PubReject — duplicate publish with same packet ID is rejected. var sm = new MqttQos2StateMachine(); sm.BeginPublish(1).ShouldBeTrue(); sm.BeginPublish(1).ShouldBeFalse(); // duplicate } [Fact] public void QoS2StateMachine_WrongStateTransition_Rejected() { // Go: out-of-order state transitions are rejected. var sm = new MqttQos2StateMachine(); sm.BeginPublish(1).ShouldBeTrue(); // Cannot process PUBREL before PUBREC sm.ProcessPubRel(1).ShouldBeFalse(); // Cannot process PUBCOMP before PUBREL sm.ProcessPubComp(1).ShouldBeFalse(); } [Fact] public void QoS2StateMachine_UnknownPacketId_Rejected() { // Go: processing PUBREC for unknown packet ID returns false. var sm = new MqttQos2StateMachine(); sm.ProcessPubRec(99).ShouldBeFalse(); } [Fact] public void QoS2StateMachine_Timeout_DetectsStaleFlows() { // Go: TestMQTTQoS2RetriesPubRel — stale flows are detected for cleanup. var time = new FakeTimeProvider(DateTimeOffset.UtcNow); var sm = new MqttQos2StateMachine(timeout: TimeSpan.FromSeconds(5), timeProvider: time); sm.BeginPublish(1); sm.BeginPublish(2); // Advance past timeout time.Advance(TimeSpan.FromSeconds(10)); var timedOut = sm.GetTimedOutFlows(); timedOut.Count.ShouldBe(2); timedOut.ShouldContain((ushort)1); timedOut.ShouldContain((ushort)2); } // ======================================================================== // Session Store — flapper detection // Go reference: mqtt_test.go TestMQTTLockedSession // ======================================================================== [Fact] public void SessionStore_FlapperDetection_BackoffApplied() { // Go: TestMQTTLockedSession — rapid reconnects trigger flapper backoff. var time = new FakeTimeProvider(DateTimeOffset.UtcNow); var store = new MqttSessionStore( flapWindow: TimeSpan.FromSeconds(5), flapThreshold: 3, flapBackoff: TimeSpan.FromSeconds(2), timeProvider: time); // Under threshold — no backoff store.TrackConnectDisconnect("client-1", connected: true); store.TrackConnectDisconnect("client-1", connected: true); store.ShouldApplyBackoff("client-1").ShouldBe(TimeSpan.Zero); // At threshold — backoff applied store.TrackConnectDisconnect("client-1", connected: true); store.ShouldApplyBackoff("client-1").ShouldBe(TimeSpan.FromSeconds(2)); } [Fact] public void SessionStore_FlapperDetection_DisconnectsIgnored() { // Go: disconnect events do not count toward the flap threshold. var store = new MqttSessionStore(flapThreshold: 3); store.TrackConnectDisconnect("client-1", connected: false); store.TrackConnectDisconnect("client-1", connected: false); store.TrackConnectDisconnect("client-1", connected: false); store.ShouldApplyBackoff("client-1").ShouldBe(TimeSpan.Zero); } [Fact] public void SessionStore_FlapperDetection_WindowExpiry() { // Go: connections outside the flap window are pruned. var time = new FakeTimeProvider(DateTimeOffset.UtcNow); var store = new MqttSessionStore( flapWindow: TimeSpan.FromSeconds(5), flapThreshold: 3, flapBackoff: TimeSpan.FromSeconds(2), timeProvider: time); store.TrackConnectDisconnect("client-1", connected: true); store.TrackConnectDisconnect("client-1", connected: true); store.TrackConnectDisconnect("client-1", connected: true); store.ShouldApplyBackoff("client-1").ShouldBe(TimeSpan.FromSeconds(2)); // Advance past the window — old events should be pruned time.Advance(TimeSpan.FromSeconds(10)); store.ShouldApplyBackoff("client-1").ShouldBe(TimeSpan.Zero); } // ======================================================================== // Remaining-Length encoding/decoding roundtrip // Go reference: mqtt_test.go various — validates wire encoding // ======================================================================== [Theory] [InlineData(0)] [InlineData(127)] [InlineData(128)] [InlineData(16383)] [InlineData(16384)] [InlineData(2097151)] [InlineData(2097152)] [InlineData(268435455)] public void RemainingLength_EncodeDecode_Roundtrip(int value) { // Go: various tests that exercise different remaining-length sizes. var encoded = MqttPacketWriter.EncodeRemainingLength(value); var decoded = MqttPacketReader.DecodeRemainingLength(encoded, out var consumed); decoded.ShouldBe(value); consumed.ShouldBe(encoded.Length); } [Fact] public void RemainingLength_NegativeValue_Throws() { Should.Throw(() => MqttPacketWriter.EncodeRemainingLength(-1)); } [Fact] public void RemainingLength_ExceedsMax_Throws() { Should.Throw(() => MqttPacketWriter.EncodeRemainingLength(268_435_456)); } // ======================================================================== // Text Protocol Parser (MqttProtocolParser.ParseLine) // Go reference: mqtt_test.go TestMQTTPermissionsViolation // ======================================================================== [Fact] public void TextParser_ConnectWithAuth() { // Go: TestMQTTNoAuthUserValidation — text-mode CONNECT with credentials. var parser = new MqttProtocolParser(); var pkt = parser.ParseLine("CONNECT my-client user=admin pass=secret"); pkt.Type.ShouldBe(MqttPacketType.Connect); pkt.ClientId.ShouldBe("my-client"); pkt.Username.ShouldBe("admin"); pkt.Password.ShouldBe("secret"); } [Fact] public void TextParser_ConnectWithKeepalive() { // Go: CONNECT with keepalive field. var parser = new MqttProtocolParser(); var pkt = parser.ParseLine("CONNECT device-1 keepalive=30 clean=false"); pkt.Type.ShouldBe(MqttPacketType.Connect); pkt.ClientId.ShouldBe("device-1"); pkt.KeepAliveSeconds.ShouldBe(30); pkt.CleanSession.ShouldBeFalse(); } [Fact] public void TextParser_Subscribe() { // Go: TestMQTTSubPropagation — text-mode SUB. var parser = new MqttProtocolParser(); var pkt = parser.ParseLine("SUB home/+/temperature"); pkt.Type.ShouldBe(MqttPacketType.Subscribe); pkt.Topic.ShouldBe("home/+/temperature"); } [Fact] public void TextParser_Publish() { // Go: TestMQTTPermissionsViolation — text-mode PUB. var parser = new MqttProtocolParser(); var pkt = parser.ParseLine("PUB sensor/temp 22.5"); pkt.Type.ShouldBe(MqttPacketType.Publish); pkt.Topic.ShouldBe("sensor/temp"); pkt.Payload.ShouldBe("22.5"); } [Fact] public void TextParser_PublishQos1() { // Go: text-mode PUBQ1 with packet ID. var parser = new MqttProtocolParser(); var pkt = parser.ParseLine("PUBQ1 42 sensor/temp 22.5"); pkt.Type.ShouldBe(MqttPacketType.PublishQos1); pkt.PacketId.ShouldBe(42); pkt.Topic.ShouldBe("sensor/temp"); pkt.Payload.ShouldBe("22.5"); } [Fact] public void TextParser_Ack() { // Go: text-mode ACK. var parser = new MqttProtocolParser(); var pkt = parser.ParseLine("ACK 42"); pkt.Type.ShouldBe(MqttPacketType.Ack); pkt.PacketId.ShouldBe(42); } [Fact] public void TextParser_EmptyLine_ReturnsUnknown() { var parser = new MqttProtocolParser(); var pkt = parser.ParseLine(""); pkt.Type.ShouldBe(MqttPacketType.Unknown); } [Fact] public void TextParser_MalformedLine_ReturnsUnknown() { var parser = new MqttProtocolParser(); parser.ParseLine("GARBAGE").Type.ShouldBe(MqttPacketType.Unknown); parser.ParseLine("PUB").Type.ShouldBe(MqttPacketType.Unknown); parser.ParseLine("PUBQ1 bad").Type.ShouldBe(MqttPacketType.Unknown); parser.ParseLine("ACK bad").Type.ShouldBe(MqttPacketType.Unknown); } // ======================================================================== // MqttTopicMatch — internal matching logic // Go reference: mqtt_test.go TestMQTTCrossAccountRetain // ======================================================================== [Theory] [InlineData("a/b/c", "a/b/c", true)] [InlineData("a/b/c", "a/+/c", true)] [InlineData("a/b/c", "a/#", true)] [InlineData("a/b/c", "#", true)] [InlineData("a/b/c", "a/b", false)] [InlineData("a/b", "a/b/c", false)] [InlineData("a/b/c", "+/+/+", true)] [InlineData("a/b/c", "+/#", true)] [InlineData("a", "+", true)] [InlineData("a/b/c/d", "a/+/c/+", true)] [InlineData("a/b/c/d", "a/+/+/e", false)] public void MqttTopicMatch_CorrectBehavior(string topic, string filter, bool expected) { // Go: TestMQTTCrossAccountRetain — internal topic matching. MqttRetainedStore.MqttTopicMatch(topic, filter).ShouldBe(expected); } // ======================================================================== // Helpers — binary packet builders // ======================================================================== private static byte[] BuildConnectPayload( string clientId, bool cleanSession, ushort keepAlive, string? username = null, string? password = null, string? willTopic = null, byte[]? willMessage = null, byte willQoS = 0, bool willRetain = false) { var ms = new MemoryStream(); // Protocol name WriteUtf8String(ms, "MQTT"); // Protocol level (4 = 3.1.1) ms.WriteByte(4); // Connect flags byte flags = 0; if (cleanSession) flags |= 0x02; if (willTopic != null) flags |= 0x04; flags |= (byte)((willQoS & 0x03) << 3); if (willRetain) flags |= 0x20; if (password != null) flags |= 0x40; if (username != null) flags |= 0x80; ms.WriteByte(flags); // Keep alive ms.WriteByte((byte)(keepAlive >> 8)); ms.WriteByte((byte)(keepAlive & 0xFF)); // Client ID WriteUtf8String(ms, clientId); // Will if (willTopic != null) { WriteUtf8String(ms, willTopic); WriteBinaryField(ms, willMessage ?? []); } // Username if (username != null) WriteUtf8String(ms, username); // Password if (password != null) WriteUtf8String(ms, password); return ms.ToArray(); } private static byte[] BuildPublishPayload(string topic, byte[] payload, ushort packetId = 0) { var ms = new MemoryStream(); WriteUtf8String(ms, topic); if (packetId > 0) { ms.WriteByte((byte)(packetId >> 8)); ms.WriteByte((byte)(packetId & 0xFF)); } ms.Write(payload); return ms.ToArray(); } private static byte[] BuildSubscribePayload(ushort packetId, params (string filter, byte qos)[] filters) { var ms = new MemoryStream(); ms.WriteByte((byte)(packetId >> 8)); ms.WriteByte((byte)(packetId & 0xFF)); foreach (var (filter, qos) in filters) { WriteUtf8String(ms, filter); ms.WriteByte(qos); } return ms.ToArray(); } private static void WriteUtf8String(MemoryStream ms, string value) { var bytes = Encoding.UTF8.GetBytes(value); ms.WriteByte((byte)(bytes.Length >> 8)); ms.WriteByte((byte)(bytes.Length & 0xFF)); ms.Write(bytes); } private static void WriteBinaryField(MemoryStream ms, byte[] data) { ms.WriteByte((byte)(data.Length >> 8)); ms.WriteByte((byte)(data.Length & 0xFF)); ms.Write(data); } }