// Binary MQTT packet body decoder. // Go reference: golang/nats-server/server/mqtt.go // CONNECT parsing — mqttParseSub / mqttParseConnect (lines ~700–850) // PUBLISH parsing — mqttParsePublish (lines ~1200–1300) // SUBSCRIBE parsing — mqttParseSub (lines ~1400–1500) // Wildcard translation — mqttToNATSSubjectConversion (lines ~2200–2250) namespace NATS.Server.Mqtt; /// /// Decoded fields from an MQTT CONNECT packet body. /// Go reference: server/mqtt.go mqttParseConnect ~line 700. /// public readonly record struct MqttConnectInfo( string ProtocolName, byte ProtocolLevel, bool CleanSession, ushort KeepAlive, string ClientId, string? WillTopic, byte[]? WillMessage, byte WillQoS, bool WillRetain, string? Username, string? Password); /// /// Decoded fields from an MQTT PUBLISH packet body. /// Go reference: server/mqtt.go mqttParsePublish ~line 1200. /// public readonly record struct MqttPublishInfo( string Topic, ushort PacketId, byte QoS, bool Dup, bool Retain, ReadOnlyMemory Payload); /// /// Decoded fields from an MQTT SUBSCRIBE packet body. /// Go reference: server/mqtt.go mqttParseSub ~line 1400. /// public readonly record struct MqttSubscribeInfo( ushort PacketId, IReadOnlyList<(string TopicFilter, byte QoS)> Filters); /// /// Decodes the variable-header and payload of CONNECT, PUBLISH, and SUBSCRIBE /// MQTT 3.1.1 control packets, and translates MQTT wildcards to NATS subjects. /// public static class MqttBinaryDecoder { // ------------------------------------------------------------------------- // CONNECT parsing // Go reference: server/mqtt.go mqttParseConnect ~line 700 // ------------------------------------------------------------------------- /// /// Parses the payload bytes of an MQTT CONNECT packet (everything after the /// fixed header and remaining-length bytes, i.e. the value of /// ). /// /// /// The payload bytes as returned by . /// /// A populated . /// /// Thrown when the packet is malformed or the protocol name is not "MQTT". /// public static MqttConnectInfo ParseConnect(ReadOnlySpan payload) { // Variable header layout (MQTT 3.1.1 spec §3.1): // 2-byte length prefix + protocol name bytes ("MQTT") // 1 byte protocol level (4 = 3.1.1, 5 = 5.0) // 1 byte connect flags // 2 bytes keepalive (big-endian) // Payload: // 2+N client ID // if will flag: 2+N will topic, 2+N will message // if username: 2+N username // if password: 2+N password var pos = 0; // Protocol name var protocolName = ReadUtf8String(payload, ref pos); if (protocolName != "MQTT" && protocolName != "MQIsdp") throw new FormatException($"Unknown MQTT protocol name: '{protocolName}'"); if (pos + 4 > payload.Length) throw new FormatException("MQTT CONNECT packet too short for variable header."); var protocolLevel = payload[pos++]; // Connect flags byte // Bit 1 = CleanSession, Bit 2 = WillFlag, Bits 3-4 = WillQoS, Bit 5 = WillRetain, // Bit 6 = PasswordFlag, Bit 7 = UsernameFlag var connectFlags = payload[pos++]; var cleanSession = (connectFlags & 0x02) != 0; var willFlag = (connectFlags & 0x04) != 0; var willQoS = (byte)((connectFlags >> 3) & 0x03); var willRetain = (connectFlags & 0x20) != 0; var passwordFlag = (connectFlags & 0x40) != 0; var usernameFlag = (connectFlags & 0x80) != 0; // Keep-alive (big-endian uint16) var keepAlive = ReadUInt16BigEndian(payload, ref pos); // Payload fields var clientId = ReadUtf8String(payload, ref pos); string? willTopic = null; byte[]? willMessage = null; if (willFlag) { willTopic = ReadUtf8String(payload, ref pos); willMessage = ReadBinaryField(payload, ref pos); } string? username = null; if (usernameFlag) username = ReadUtf8String(payload, ref pos); string? password = null; if (passwordFlag) password = ReadUtf8String(payload, ref pos); return new MqttConnectInfo( ProtocolName: protocolName, ProtocolLevel: protocolLevel, CleanSession: cleanSession, KeepAlive: keepAlive, ClientId: clientId, WillTopic: willTopic, WillMessage: willMessage, WillQoS: willQoS, WillRetain: willRetain, Username: username, Password: password); } // ------------------------------------------------------------------------- // PUBLISH parsing // Go reference: server/mqtt.go mqttParsePublish ~line 1200 // ------------------------------------------------------------------------- /// /// Parses the payload bytes of an MQTT PUBLISH packet. /// The nibble comes from /// of the fixed header. /// /// The payload bytes from . /// The lower nibble of the fixed header byte (DUP/QoS/RETAIN flags). /// A populated . public static MqttPublishInfo ParsePublish(ReadOnlySpan payload, byte flags) { // Fixed-header flags nibble layout (MQTT 3.1.1 spec §3.3.1): // Bit 3 = DUP // Bits 2-1 = QoS (0, 1, or 2) // Bit 0 = RETAIN var dup = (flags & 0x08) != 0; var qos = (byte)((flags >> 1) & 0x03); var retain = (flags & 0x01) != 0; var pos = 0; // Variable header: topic name (2-byte length prefix + UTF-8) var topic = ReadUtf8String(payload, ref pos); // Packet identifier — only present for QoS > 0 ushort packetId = 0; if (qos > 0) packetId = ReadUInt16BigEndian(payload, ref pos); // Remaining bytes are the application payload var messagePayload = payload[pos..].ToArray(); return new MqttPublishInfo( Topic: topic, PacketId: packetId, QoS: qos, Dup: dup, Retain: retain, Payload: messagePayload); } // ------------------------------------------------------------------------- // SUBSCRIBE parsing // Go reference: server/mqtt.go mqttParseSub ~line 1400 // ------------------------------------------------------------------------- /// /// Parses the payload bytes of an MQTT SUBSCRIBE packet. /// /// The payload bytes from . /// /// Optional fixed-header flags nibble. When provided, must match SUBSCRIBE flags (0x02). /// /// A populated . public static MqttSubscribeInfo ParseSubscribe(ReadOnlySpan payload, byte? flags = null) { if (flags.HasValue && flags.Value != MqttProtocolConstants.SubscribeFlags) throw new FormatException("MQTT SUBSCRIBE packet has invalid fixed-header flags."); // Variable header: packet identifier (2 bytes, big-endian) // Payload: one or more topic-filter entries, each: // 2-byte length prefix + UTF-8 filter string + 1-byte requested QoS var pos = 0; var packetId = ReadUInt16BigEndian(payload, ref pos); var filters = new List<(string, byte)>(); while (pos < payload.Length) { var topicFilter = ReadUtf8String(payload, ref pos); if (pos >= payload.Length) throw new FormatException("MQTT SUBSCRIBE packet missing QoS byte after topic filter."); var filterQoS = payload[pos++]; filters.Add((topicFilter, filterQoS)); } return new MqttSubscribeInfo(packetId, filters); } // ------------------------------------------------------------------------- // MQTT wildcard → NATS subject translation // Go reference: server/mqtt.go mqttToNATSSubjectConversion ~line 2200 // // Simple translation (filter → NATS, wildcards permitted): // '+' → '*' (single-level wildcard) // '#' → '>' (multi-level wildcard) // '/' → '.' (topic separator) // // NOTE: This method implements the simple/naïve translation that the task // description specifies. The full Go implementation also handles dots, // leading/trailing slashes, and empty levels differently (see // MqttTopicMappingParityTests for the complete behavior). This method is // intentionally limited to the four rules requested by the task spec. // ------------------------------------------------------------------------- /// /// Translates an MQTT topic filter to a NATS subject using the simple rules: /// /// +* (single-level wildcard) /// #> (multi-level wildcard) /// /. (separator) /// /// Go reference: server/mqtt.go mqttToNATSSubjectConversion ~line 2200. /// /// An MQTT topic filter string. /// The equivalent NATS subject string. public static string TranslateFilterToNatsSubject(string mqttFilter) { if (mqttFilter.Length == 0) return string.Empty; var result = new char[mqttFilter.Length]; for (var i = 0; i < mqttFilter.Length; i++) { result[i] = mqttFilter[i] switch { '+' => '*', '#' => '>', '/' => '.', var c => c, }; } return new string(result); } // ------------------------------------------------------------------------- // Internal helpers // ------------------------------------------------------------------------- /// /// Reads a 2-byte big-endian length-prefixed UTF-8 string from /// starting at , advancing /// past the consumed bytes. /// private static string ReadUtf8String(ReadOnlySpan data, ref int pos) { if (pos + 2 > data.Length) throw new FormatException("MQTT packet truncated reading string length prefix."); var length = (data[pos] << 8) | data[pos + 1]; pos += 2; if (pos + length > data.Length) throw new FormatException("MQTT packet truncated reading string body."); var value = System.Text.Encoding.UTF8.GetString(data.Slice(pos, length)); pos += length; return value; } /// /// Reads a 2-byte big-endian length-prefixed binary field (e.g. will /// message, password) from , advancing /// past the consumed bytes. /// private static byte[] ReadBinaryField(ReadOnlySpan data, ref int pos) { if (pos + 2 > data.Length) throw new FormatException("MQTT packet truncated reading binary field length prefix."); var length = (data[pos] << 8) | data[pos + 1]; pos += 2; if (pos + length > data.Length) throw new FormatException("MQTT packet truncated reading binary field body."); var value = data.Slice(pos, length).ToArray(); pos += length; return value; } /// /// Reads a big-endian uint16 from at /// , advancing by 2. /// private static ushort ReadUInt16BigEndian(ReadOnlySpan data, ref int pos) { if (pos + 2 > data.Length) throw new FormatException("MQTT packet truncated reading uint16."); var value = (ushort)((data[pos] << 8) | data[pos + 1]); pos += 2; return value; } }