Skip to content

Commit

Permalink
MQTT Wire Encoding / Decoding (#7)
Browse files Browse the repository at this point in the history
* add MQTT protocol version

* optimized enums to `byte` data storage

* make the compiler happy

* added some basic tests for packet types

* more fixes

* added MQTT 3.1.1 ConnectPacket size estimation specs

* fixed some failing specs

* adjusted namespaces

* added MQTT5 size estimation specs

* fixed the ASCII indicator for MQTT 3.1.1

* working on ConnAckSpecs

* added ConnAck specs

* finished all packet size estimators

* fixed compilation errors

* enabled treat warnings as errors
  • Loading branch information
Aaronontheweb authored Apr 13, 2024
1 parent ad71345 commit e6a5ac6
Show file tree
Hide file tree
Showing 21 changed files with 1,442 additions and 84 deletions.
5 changes: 1 addition & 4 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<PropertyGroup>
<Copyright>Copyright © 2023 Your Company</Copyright>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<VersionPrefix>1.0.0</VersionPrefix>
</PropertyGroup>

Expand All @@ -17,10 +18,6 @@
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>

<PropertyGroup Label="Dependencies">
<PbmVersion>1.4.0</PbmVersion>
</PropertyGroup>

<ItemGroup>
<None Include="$(MSBuildThisFileDirectory)\README.md" Pack="true" Visible="false" PackagePath="\"/>
<None Include="$(MSBuildThisFileDirectory)\LICENSE.md" Pack="true" Visible="false" PackagePath="\"/>
Expand Down
2 changes: 1 addition & 1 deletion src/TurboMqtt.Core/ControlPacketHeaders.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ namespace TurboMqtt.Core;
///
/// Also supports MQTT 5.0.
/// </remarks>
public enum MqttPacketType
public enum MqttPacketType : byte
{
Connect = 0x10,
ConnAck = 0x20,
Expand Down
44 changes: 0 additions & 44 deletions src/TurboMqtt.Core/MqttPubAckReasonCode.cs

This file was deleted.

2 changes: 1 addition & 1 deletion src/TurboMqtt.Core/PacketTypes/AuthPacket.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public override string ToString()
/// <summary>
/// Enumerates the reason codes applicable to the AUTH packet in MQTT 5.0.
/// </summary>
public enum AuthReasonCode
public enum AuthReasonCode : byte
{
Success = 0x00,
ContinueAuthentication = 0x18,
Expand Down
4 changes: 2 additions & 2 deletions src/TurboMqtt.Core/PacketTypes/ConnAckPacket.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ public sealed class ConnAckPacket : MqttPacket
public ConnAckReasonCode ReasonCode { get; set; } // Enum defined below

// MQTT 5.0 - Optional Properties
public IReadOnlyDictionary<string, string>? Properties { get; set; }
public IReadOnlyDictionary<string, string>? UserProperties { get; set; }

public override string ToString()
{
return $"ConnAck: [SessionPresent={SessionPresent}] [ReasonCode={ReasonCode}]";
}
}

public enum ConnAckReasonCode
public enum ConnAckReasonCode : byte
{
Success = 0x00,
UnspecifiedError = 0x80,
Expand Down
110 changes: 95 additions & 15 deletions src/TurboMqtt.Core/PacketTypes/ConnectPacket.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,114 @@
// </copyright>
// -----------------------------------------------------------------------

using TurboMqtt.Core.Protocol;

namespace TurboMqtt.Core.PacketTypes;

/// <summary>
/// Used to initiate a connection to the MQTT broker.
/// </summary>
public class ConnectPacket(string clientId) : MqttPacket
public class ConnectPacket(string clientId, MqttProtocolVersion protocolVersion) : MqttPacket
{
public override MqttPacketType PacketType => MqttPacketType.Connect;

public string ClientId { get; } = clientId;
public bool CleanSession { get; set; }
public ushort KeepAlive { get; set; }

// MQTT 5.0 - Optional Properties
public ConnectFlags Flags { get; set; }

public MqttLastWill? Will { get; set; }

public string? Username { get; set; }
public string? Password { get; set; }
public ReadOnlyMemory<byte>? Password { get; set; }

public MqttProtocolVersion ProtocolVersion { get; } = protocolVersion;

// MQTT 5.0 - Optional Properties
public ushort ReceiveMaximum { get; set; } // MQTT 5.0 only
public uint MaximumPacketSize { get; set; } // MQTT 5.0 only
public ushort TopicAliasMaximum { get; set; } // MQTT 5.0 only
public uint SessionExpiryInterval { get; set; } // MQTT 5.0 only
public bool RequestProblemInformation { get; set; } // MQTT 5.0 only
public bool RequestResponseInformation { get; set; } // MQTT 5.0 only
public string? AuthenticationMethod { get; set; } // MQTT 5.0 only
public ReadOnlyMemory<byte>? AuthenticationData { get; set; } // MQTT 5.0 only
public IReadOnlyDictionary<string, string>? UserProperties { get; set; } // MQTT 5.0 custom properties


}

/// <summary>
/// Payload for the Last Will and Testament message.
/// </summary>
public sealed class MqttLastWill
{
public MqttLastWill(string topic, ReadOnlyMemory<byte> message)
{
Topic = topic;
Message = message;

// throw if topic is invalid
if (string.IsNullOrEmpty(topic))
throw new ArgumentException("Topic cannot be null or empty.", nameof(topic));
}

public bool? WillFlag { get; set; }
public string? WillTopic { get; set; }
public ReadOnlyMemory<byte>? WillMessage { get; set; }
public QualityOfService? WillQos { get; set; }
public bool? WillRetain { get; set; }
public string Topic { get; }
public ReadOnlyMemory<byte> Message { get; }

// MQTT 5.0 - Optional Properties for Last Will and Testament
public string? ResponseTopic { get; set; } // MQTT 5.0 only
public ReadOnlyMemory<byte>? WillCorrelationData { get; set; } // MQTT 5.0 only
public string? ContentType { get; set; } // MQTT 5.0 only
public PayloadFormatIndicator PayloadFormatIndicator { get; set; } // MQTT 5.0 only
public NonZeroUInt32 DelayInterval { get; set; } // MQTT 5.0 only
public uint MessageExpiryInterval { get; set; } // MQTT 5.0 only
public IReadOnlyDictionary<string, string>? WillProperties { get; set; } // MQTT 5.0 custom properties

// QoS and Retain are determined by ConnectFlags
}

public IReadOnlyDictionary<string, string>?
Properties { get; set; } // Custom properties like Session Expiry Interval, Maximum Packet Size, etc.
public struct ConnectFlags
{
public bool UsernameFlag { get; set; }
public bool PasswordFlag { get; set; }
public bool WillRetain { get; set; }
public QualityOfService WillQoS { get; set; }
public bool WillFlag { get; set; }
public bool CleanSession { get; set; } // Renamed from CleanStart for 3.1.1 compatibility

public override string ToString()
public byte Encode(MqttProtocolVersion version)
{
return $"Connect: [ClientId={ClientId}] [CleanSession={CleanSession}] [KeepAlive={KeepAlive}]";
byte result = 0;
if (UsernameFlag)
result |= 0b1000_0000;
if (PasswordFlag)
result |= 0b0100_0000;
if (version == MqttProtocolVersion.V5_0 && WillRetain)
result |= 0b0010_0000;
if (WillFlag)
result |= 0b0000_0100;
if (CleanSession)
result |= 0b0000_0010;
if (WillFlag) // Only encode Will QoS if Will is set
result |= (byte)(((int)WillQoS & 0x03) << 3);

return result;
}

public static ConnectFlags Decode(byte flags)
{
var result = new ConnectFlags
{
UsernameFlag = (flags & 0b1000_0000) != 0,
PasswordFlag = (flags & 0b0100_0000) != 0,
WillRetain = (flags & 0b0010_0000) != 0,
WillFlag = (flags & 0b0000_0100) != 0,
CleanSession = (flags & 0b0000_0010) != 0
};

if (result.WillFlag)
result.WillQoS = (QualityOfService)((flags & 0b0001_1000) >> 3);

return result;
}
}
}
2 changes: 1 addition & 1 deletion src/TurboMqtt.Core/PacketTypes/DisconnectPacket.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public override string ToString()
}
}

public enum DisconnectReasonCode
public enum DisconnectReasonCode : byte
{
NormalDisconnection = 0x00,
DisconnectWithWillMessage = 0x04,
Expand Down
2 changes: 1 addition & 1 deletion src/TurboMqtt.Core/PacketTypes/PubCompPacket.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public override string ToString()
/// <summary>
/// Enum for PUBCOMP reason codes, using the same as PUBREC for simplicity and because MQTT 5.0 reuses these
/// </summary>
public enum PubCompReasonCode
public enum PubCompReasonCode : byte
{
Success = 0x00,
PacketIdentifierNotFound = 0x92
Expand Down
2 changes: 1 addition & 1 deletion src/TurboMqtt.Core/PacketTypes/PubRecPacket.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public override string ToString()
/// <summary>
/// Enum for PUBREC and PUBCOMP reason codes (as they share the same codes)
/// </summary>
public enum PubRecReasonCode
public enum PubRecReasonCode : byte
{
Success = 0x00,
NoMatchingSubscribers = 0x10,
Expand Down
2 changes: 1 addition & 1 deletion src/TurboMqtt.Core/PacketTypes/PubRelPacket.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public override string ToString()
/// <summary>
/// Enum for PUBREL reason codes (typically these would be simpler as successful flow is usually assumed)
/// </summary>
public enum PubRelReasonCode
public enum PubRelReasonCode : byte
{
Success = 0x00,
PacketIdentifierNotFound = 0x92
Expand Down
39 changes: 37 additions & 2 deletions src/TurboMqtt.Core/PacketTypes/PublishAckPacket.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,42 @@

namespace TurboMqtt.Core.PacketTypes;

using static MqttPubAckHelpers;
/// <summary>
/// All possible reason codes for the PubAck packet.
/// </summary>
public enum MqttPubAckReasonCode : byte
{
Success = 0x00,
NoMatchingSubscribers = 0x10,
UnspecifiedError = 0x80,
ImplementationSpecificError = 0x83,
NotAuthorized = 0x87,
TopicNameInvalid = 0x90,
PacketIdentifierInUse = 0x91,
QuotaExceeded = 0x97,
PayloadFormatInvalid = 0x99
}

// add a static helper method that can turn a MqttPubAckReason code into a hard-coded string representation
internal static class MqttPubAckHelpers
{
public static string ReasonCodeToString(MqttPubAckReasonCode reasonCode)
{
return reasonCode switch
{
MqttPubAckReasonCode.Success => "Success",
MqttPubAckReasonCode.NoMatchingSubscribers => "NoMatchingSubscribers",
MqttPubAckReasonCode.UnspecifiedError => "UnspecifiedError",
MqttPubAckReasonCode.ImplementationSpecificError => "ImplementationSpecificError",
MqttPubAckReasonCode.NotAuthorized => "NotAuthorized",
MqttPubAckReasonCode.TopicNameInvalid => "TopicNameInvalid",
MqttPubAckReasonCode.PacketIdentifierInUse => "PacketIdentifierInUse",
MqttPubAckReasonCode.QuotaExceeded => "QuotaExceeded",
MqttPubAckReasonCode.PayloadFormatInvalid => "PayloadFormatInvalid",
_ => throw new ArgumentOutOfRangeException(nameof(reasonCode), reasonCode, null)
};
}
}

/// <summary>
/// Used to acknowledge the receipt of a Publish packet.
Expand All @@ -27,7 +62,7 @@ public sealed class PublishAckPacket : MqttPacketWithId
/// User Properties, available in MQTT 5.0.
/// These are key-value pairs that can be sent to provide additional information in the acknowledgment.
/// </summary>
public string ReasonString => ReasonCodeToString(ReasonCode);
public string ReasonString => MqttPubAckHelpers.ReasonCodeToString(ReasonCode);

public override string ToString()
{
Expand Down
34 changes: 26 additions & 8 deletions src/TurboMqtt.Core/PacketTypes/PublishPacket.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,33 @@ namespace TurboMqtt.Core.PacketTypes;
/// <summary>
/// Used to send data to the server or client.
/// </summary>
/// <param name="Qos">The delivery guarantee for this packet.</param>
/// <param name="Duplicate">Is this packet a duplicate?</param>
/// <param name="RetainRequested">Indicates whether or not this value has been retained by the MQTT broker.</param>
public sealed class PublishPacket(QualityOfService Qos, bool Duplicate, bool RetainRequested) : MqttPacketWithId
/// <param name="qos">The delivery guarantee for this packet.</param>
/// <param name="duplicate">Is this packet a duplicate?</param>
/// <param name="retainRequested">Indicates whether or not this value has been retained by the MQTT broker.</param>
public sealed class PublishPacket(QualityOfService qos, bool duplicate, bool retainRequested, string topicName) : MqttPacketWithId
{
public override MqttPacketType PacketType => MqttPacketType.Publish;

public override bool Duplicate { get; } = Duplicate;
public override bool Duplicate { get; } = duplicate;

public override QualityOfService QualityOfService { get; } = Qos;
public override QualityOfService QualityOfService { get; } = qos;

public override bool RetainRequested { get; } = RetainRequested;
public override bool RetainRequested { get; } = retainRequested;

public ushort TopicAlias { get; set; } // MQTT 5.0 only

/// <summary>
/// Optional for <see cref="QualityOfService.AtMostOnce"/>
/// </summary>
public string? TopicName { get; set; }
public string TopicName { get; } = topicName;

public uint MessageExpiryInterval { get; set; } // MQTT 5.0 only

// Payload
public ReadOnlyMemory<byte> Payload { get; set; } = ReadOnlyMemory<byte>.Empty;

// MQTT 3.1.1 and 5.0 - Optional Properties
public PayloadFormatIndicator PayloadFormatIndicator { get; set; } // MQTT 5.0 only

/// <summary>
/// The Content Type property, available in MQTT 5.0.
Expand Down Expand Up @@ -68,4 +73,17 @@ public override string ToString()
return
$"Publish: [Topic={TopicName}] [PayloadLength={Payload.Length}] [QoSLevel={QualityOfService}] [Dup={Duplicate}] [Retain={RetainRequested}] [PacketIdentifier={PacketId}]";
}
}

public enum PayloadFormatIndicator : byte
{
/// <summary>
/// The payload is unspecified bytes, which should not be interpreted as UTF-8 encoded character data.
/// </summary>
Unspecified = 0,

/// <summary>
/// The payload is UTF-8 encoded character data.
/// </summary>
Utf8Encoded = 1
}
Loading

0 comments on commit e6a5ac6

Please sign in to comment.