// 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;
}
}