diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 030f1186d..6932e6e7e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,15 +23,21 @@ jobs: uses: actions/checkout@v4 - name: Load strong name certificate + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} run: | echo "$SNC_BASE64" | base64 --decode > "${{ github.workspace }}/certificate.snk" shell: bash env: SNC_BASE64: ${{ secrets.SNC_BASE64 }} - - name: Build package + - name: Build package (signed) + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} run: dotnet build MQTTnet.sln --configuration Release /p:FileVersion=${{ env.VERSION }} /p:AssemblyVersion=${{ env.VERSION }} /p:PackageVersion=${{ env.VERSION }}${{ env.PACKAGE_SUFFIX }} /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=${{ github.workspace }}/certificate.snk + - name: Build package (unsigned) + if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository }} + run: dotnet build MQTTnet.sln --configuration Release /p:FileVersion=${{ env.VERSION }} /p:AssemblyVersion=${{ env.VERSION }} /p:PackageVersion=${{ env.VERSION }}${{ env.PACKAGE_SUFFIX }} + - name: Upload nuget packages uses: actions/upload-artifact@v4 with: @@ -55,12 +61,12 @@ jobs: uses: actions/checkout@v4 - name: Execute tests - run: dotnet test --framework net10.0 --configuration Release Source/MQTTnet.Tests/MQTTnet.Tests.csproj + run: dotnet test --framework net10.0 --configuration Release --project Source/MQTTnet.Tests/MQTTnet.Tests.csproj sign: needs: build runs-on: windows-latest # Code signing must run on a Windows agent for Authenticode signing (dll/exe) - if: github.repository == 'dotnet/MQTTnet' + if: github.repository == 'dotnet/MQTTnet' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) steps: - name: Setup .NET SDK uses: actions/setup-dotnet@v4 diff --git a/Samples/Client/Client_Publish_Samples.cs b/Samples/Client/Client_Publish_Samples.cs index 957baf958..ef4eb881e 100644 --- a/Samples/Client/Client_Publish_Samples.cs +++ b/Samples/Client/Client_Publish_Samples.cs @@ -6,6 +6,9 @@ // ReSharper disable UnusedMember.Global // ReSharper disable InconsistentNaming +using System; +using System.Text; + namespace MQTTnet.Samples.Client; public static class Client_Publish_Samples @@ -84,4 +87,21 @@ public static async Task Publish_Multiple_Application_Messages() Console.WriteLine("MQTT application message is published."); } + + public static MqttApplicationMessage Create_Message_With_Binary_User_Property() + { + /* + * MQTT v5 user properties are encoded as UTF-8 strings. When the UTF-8 payload is already available + * as a byte buffer, the builder APIs can avoid creating intermediate strings. + */ + + var encodedValue = Encoding.UTF8.GetBytes("sensor-01"); + + return new MqttApplicationMessageBuilder() + .WithTopic("samples/metadata/binary") + .WithUserProperty("client-id", encodedValue.AsMemory()) + .WithUserProperty("checksum", new ArraySegment(encodedValue)) + .WithPayload("metadata") + .Build(); + } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/MQTTnet.Tests.csproj b/Source/MQTTnet.Tests/MQTTnet.Tests.csproj index 7183f0cea..ad7fb0ebf 100644 --- a/Source/MQTTnet.Tests/MQTTnet.Tests.csproj +++ b/Source/MQTTnet.Tests/MQTTnet.Tests.csproj @@ -6,13 +6,12 @@ default disable $(NoWarn);CA1707 + true - - - + @@ -24,4 +23,10 @@ + + + + \ No newline at end of file diff --git a/Source/MQTTnet.Tests/MQTTv5/Client_Tests.cs b/Source/MQTTnet.Tests/MQTTv5/Client_Tests.cs index 0251a7456..f68323ba2 100644 --- a/Source/MQTTnet.Tests/MQTTv5/Client_Tests.cs +++ b/Source/MQTTnet.Tests/MQTTv5/Client_Tests.cs @@ -4,6 +4,7 @@ using System.Buffers; using System.Diagnostics.CodeAnalysis; +using System.Text; using Microsoft.IO; using MQTTnet.Formatter; using MQTTnet.Internal; @@ -64,8 +65,8 @@ public async Task Connect_With_New_Mqtt_Features() await client.PublishAsync( new MqttApplicationMessageBuilder().WithTopic("a") .WithPayload("x") - .WithUserProperty("a", "1") - .WithUserProperty("b", "2") + .WithUserProperty("a", Encoding.UTF8.GetBytes("1")) + .WithUserProperty("b", Encoding.UTF8.GetBytes("2")) .WithPayloadFormatIndicator(MqttPayloadFormatIndicator.CharacterData) .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) .Build()); @@ -98,8 +99,8 @@ public async Task Publish_And_Receive_New_Properties() var applicationMessage = new MqttApplicationMessageBuilder().WithTopic("Hello") .WithPayload("World") .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtMostOnce) - .WithUserProperty("x", "1") - .WithUserProperty("y", "2") + .WithUserProperty("x", Encoding.UTF8.GetBytes("1")) + .WithUserProperty("y", Encoding.UTF8.GetBytes("2")) .WithResponseTopic("response") .WithContentType("text") .WithMessageExpiryInterval(50) @@ -214,8 +215,8 @@ public async Task Publish_With_Properties() var applicationMessage = new MqttApplicationMessageBuilder().WithTopic("Hello") .WithPayload("World") .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtMostOnce) - .WithUserProperty("x", "1") - .WithUserProperty("y", "2") + .WithUserProperty("x", Encoding.UTF8.GetBytes("1")) + .WithUserProperty("y", Encoding.UTF8.GetBytes("2")) .WithResponseTopic("response") .WithContentType("text") .WithMessageExpiryInterval(50) @@ -250,8 +251,8 @@ public async Task Publish_With_RecyclableMemoryStream() var applicationMessage = new MqttApplicationMessageBuilder().WithTopic("Hello") .WithPayload(memoryStream.GetReadOnlySequence()) .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtMostOnce) - .WithUserProperty("x", "1") - .WithUserProperty("y", "2") + .WithUserProperty("x", Encoding.UTF8.GetBytes("1")) + .WithUserProperty("y", Encoding.UTF8.GetBytes("2")) .WithResponseTopic("response") .WithContentType("text") .WithMessageExpiryInterval(50) diff --git a/Source/MQTTnet.Tests/MqttApplicationMessageBuilder_Tests.cs b/Source/MQTTnet.Tests/MqttApplicationMessageBuilder_Tests.cs index b1689648d..0d9e1e27c 100644 --- a/Source/MQTTnet.Tests/MqttApplicationMessageBuilder_Tests.cs +++ b/Source/MQTTnet.Tests/MqttApplicationMessageBuilder_Tests.cs @@ -2,7 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; +using System.Collections.Generic; using System.Text; +using MQTTnet.Formatter; +using MQTTnet.Formatter.V5; +using MQTTnet.Packets; using MQTTnet.Protocol; namespace MQTTnet.Tests; @@ -59,4 +64,62 @@ public void CreateApplicationMessage_QosLevel2() Assert.IsTrue(message.Retain); Assert.AreEqual(MqttQualityOfServiceLevel.ExactlyOnce, message.QualityOfServiceLevel); } + + [TestMethod] + public void CreateApplicationMessage_UserProperty_ReadOnlyMemoryValue() + { + var value = "utf8"; + var buffer = Encoding.UTF8.GetBytes(value); + + var message = new MqttApplicationMessageBuilder().WithTopic("topic").WithUserProperty("name", buffer.AsMemory()).Build(); + + Assert.IsNotNull(message.UserProperties); + Assert.HasCount(1, message.UserProperties); + + var userProperty = message.UserProperties[0]; + CollectionAssert.AreEqual(buffer, userProperty.ValueBuffer.ToArray()); + Assert.AreEqual(value, userProperty.Value); + } + + [TestMethod] + public void CreateApplicationMessage_UserProperty_ArraySegmentValue() + { + var buffer = Encoding.UTF8.GetBytes("segment"); + var segment = new ArraySegment(buffer); + + var message = new MqttApplicationMessageBuilder().WithTopic("topic").WithUserProperty("name", segment).Build(); + + Assert.IsNotNull(message.UserProperties); + Assert.HasCount(1, message.UserProperties); + + var userProperty = message.UserProperties[0]; + CollectionAssert.AreEqual(buffer, userProperty.ValueBuffer.ToArray()); + Assert.AreEqual("segment", userProperty.Value); + } + + [TestMethod] + public void WriteUserProperty_FromBinaryBuffer_EqualsStringEncoding() + { + var name = "name"; + var value = "value"; + var encoded = Encoding.UTF8.GetBytes(value); + + var binaryWriter = new MqttBufferWriter(32, 256); + var binaryPropertiesWriter = new MqttV5PropertiesWriter(binaryWriter); + binaryPropertiesWriter.WriteUserProperties(new List { new(name, encoded.AsMemory()) }); + + var stringWriter = new MqttBufferWriter(32, 256); + var stringPropertiesWriter = new MqttV5PropertiesWriter(stringWriter); + stringPropertiesWriter.WriteUserProperties(new List { new(name, value) }); + + CollectionAssert.AreEqual(GetWrittenBytes(stringWriter), GetWrittenBytes(binaryWriter)); + } + + static byte[] GetWrittenBytes(MqttBufferWriter writer) + { + var length = writer.Length; + var copy = new byte[length]; + Array.Copy(writer.GetBuffer(), copy, length); + return copy; + } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/MqttApplicationMessageValidator_Tests.cs b/Source/MQTTnet.Tests/MqttApplicationMessageValidator_Tests.cs index 441f125a4..2ad9c732d 100644 --- a/Source/MQTTnet.Tests/MqttApplicationMessageValidator_Tests.cs +++ b/Source/MQTTnet.Tests/MqttApplicationMessageValidator_Tests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Text; using MQTTnet.Formatter; namespace MQTTnet.Tests; @@ -27,7 +28,7 @@ public void Succeed_When_Using_TopicAlias_And_MQTT_500() public void Succeed_When_Using_UserProperties_And_MQTT_500() { MqttApplicationMessageValidator.ThrowIfNotSupported( - new MqttApplicationMessageBuilder().WithTopic("A").WithUserProperty("User", "Property").Build(), + new MqttApplicationMessageBuilder().WithTopic("A").WithUserProperty("User", Encoding.UTF8.GetBytes("Property")).Build(), MqttProtocolVersion.V500); } @@ -35,7 +36,7 @@ public void Succeed_When_Using_UserProperties_And_MQTT_500() public void Succeed_When_Using_WillUserProperties_And_MQTT_311() { Assert.ThrowsExactly(() => MqttApplicationMessageValidator.ThrowIfNotSupported( - new MqttApplicationMessageBuilder().WithTopic("B").WithUserProperty("User", "Property").Build(), + new MqttApplicationMessageBuilder().WithTopic("B").WithUserProperty("User", Encoding.UTF8.GetBytes("Property")).Build(), MqttProtocolVersion.V311)); } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/MqttTcpChannel_Tests.cs b/Source/MQTTnet.Tests/MqttTcpChannel_Tests.cs index 4fb8bef4d..a07424259 100644 --- a/Source/MQTTnet.Tests/MqttTcpChannel_Tests.cs +++ b/Source/MQTTnet.Tests/MqttTcpChannel_Tests.cs @@ -20,7 +20,8 @@ public async Task Dispose_Channel_While_Used() try { - serverSocket.Bind(new IPEndPoint(IPAddress.Any, 50001)); + serverSocket.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + var serverPort = ((IPEndPoint)serverSocket.LocalEndPoint).Port; serverSocket.Listen(0); #pragma warning disable 4014 @@ -37,7 +38,7 @@ public async Task Dispose_Channel_While_Used() }, ct.Token); - var remoteEndPoint = new DnsEndPoint("localhost", 50001); + var remoteEndPoint = new DnsEndPoint("localhost", serverPort); using var clientSocket = new CrossPlatformSocket(AddressFamily.InterNetwork, ProtocolType.Tcp); await clientSocket.ConnectAsync(remoteEndPoint, CancellationToken.None); diff --git a/Source/MQTTnet.Tests/Server/Cross_Version_Tests.cs b/Source/MQTTnet.Tests/Server/Cross_Version_Tests.cs index 32563c353..78de6135d 100644 --- a/Source/MQTTnet.Tests/Server/Cross_Version_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Cross_Version_Tests.cs @@ -43,7 +43,7 @@ public async Task Send_V500_Receive_V311() var applicationMessage = new MqttApplicationMessageBuilder().WithTopic("My/Message") .WithPayload("My_Payload") - .WithUserProperty("A", "B") + .WithUserProperty("A", Encoding.UTF8.GetBytes("B")) .WithResponseTopic("Response") .WithCorrelationData(Encoding.UTF8.GetBytes("Correlation")) .Build(); diff --git a/Source/MQTTnet.Tests/Server/HotSwapCerts_Tests.cs b/Source/MQTTnet.Tests/Server/HotSwapCerts_Tests.cs index 8771b55d6..7d0afc9e0 100644 --- a/Source/MQTTnet.Tests/Server/HotSwapCerts_Tests.cs +++ b/Source/MQTTnet.Tests/Server/HotSwapCerts_Tests.cs @@ -2,6 +2,7 @@ using System.Diagnostics; using System.Net; using System.Net.Security; +using System.Net.Sockets; using System.Security.Authentication; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; @@ -26,6 +27,7 @@ public async Task ClientCertChangeWithoutServerUpdateFailsReconnect() using var client01 = new ClientTestHarness(); server.InstallNewClientCert(client01.GetCurrentClientCert()); client01.InstallNewServerCert(server.GetCurrentServerCert()); + client01.SetServerPort(server.EncryptedPort); await server.StartServer(); @@ -47,6 +49,7 @@ public async Task ClientCertChangeWithServerUpdateAcceptsReconnect() using var client01 = new ClientTestHarness(); server.InstallNewClientCert(client01.GetCurrentClientCert()); client01.InstallNewServerCert(server.GetCurrentServerCert()); + client01.SetServerPort(server.EncryptedPort); await server.StartServer(); @@ -70,6 +73,7 @@ public async Task ServerCertChangeWithClientCertUpdateAllowsReconnect() using var client01 = new ClientTestHarness(); server.InstallNewClientCert(client01.GetCurrentClientCert()); client01.InstallNewServerCert(server.GetCurrentServerCert()); + client01.SetServerPort(server.EncryptedPort); await server.StartServer(); await client01.Connect(); @@ -91,6 +95,7 @@ public async Task ServerCertChangeWithoutClientCertUpdateFailsReconnect() using var client01 = new ClientTestHarness(); server.InstallNewClientCert(client01.GetCurrentClientCert()); client01.InstallNewServerCert(server.GetCurrentServerCert()); + client01.SetServerPort(server.EncryptedPort); await server.StartServer(); await client01.Connect(); @@ -135,10 +140,11 @@ static X509Certificate2 CreateSelfSignedCertificate(string oid) sealed class ClientTestHarness : IDisposable { readonly HotSwappableClientCertProvider _hotSwapClient = new HotSwappableClientCertProvider(); + int _serverPort = 8883; IMqttClient _client; - public string ClientId => _client.Options.ClientId; + public string ClientId => _client?.Options?.ClientId; public Task Connect() { @@ -147,10 +153,15 @@ public Task Connect() public void Dispose() { - _client.Dispose(); + _client?.Dispose(); _hotSwapClient.Dispose(); } + public void SetServerPort(int port) + { + _serverPort = port; + } + public X509Certificate2 GetCurrentClientCert() { var result = _hotSwapClient.GetCertificates()[0]; @@ -216,7 +227,7 @@ async Task Run_Client_Connection() o => o.WithClientCertificatesProvider(_hotSwapClient) .WithCertificateValidationHandler(_hotSwapClient.OnCertificateValidation) .WithSslProtocols(SslProtocols.Tls12)) - .WithTcpServer("localhost") + .WithTcpServer("localhost", _serverPort) .WithCleanSession() .WithProtocolVersion(MqttProtocolVersion.V500); @@ -242,6 +253,7 @@ void WaitForConnect(TimeSpan timeout) sealed class ServerTestHarness : IDisposable { readonly HotSwappableServerCertProvider _hotSwapServer = new HotSwappableServerCertProvider(); + readonly int _encryptedPort = GetFreeTcpPort(); MqttServer _server; @@ -261,6 +273,8 @@ public Task ForceDisconnectAsync(ClientTestHarness client) return _server.DisconnectClientAsync(client.ClientId, MqttDisconnectReasonCode.UnspecifiedError); } + public int EncryptedPort => _encryptedPort; + public X509Certificate2 GetCurrentServerCert() { return _hotSwapServer.GetCertificate(); @@ -281,6 +295,7 @@ public Task StartServer() var mqttServerFactory = new MqttServerFactory(); var mqttServerOptions = new MqttServerOptionsBuilder().WithEncryptionCertificate(_hotSwapServer) + .WithEncryptedEndpointPort(_encryptedPort) .WithRemoteCertificateValidationCallback(_hotSwapServer.RemoteCertificateValidationCallback) .WithEncryptedEndpoint() .Build(); @@ -290,6 +305,17 @@ public Task StartServer() _server = mqttServerFactory.CreateMqttServer(mqttServerOptions); return _server.StartAsync(); } + + static int GetFreeTcpPort() + { + var listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + + var port = ((IPEndPoint)listener.LocalEndpoint).Port; + listener.Stop(); + + return port; + } } sealed class HotSwappableClientCertProvider : IMqttClientCertificatesProvider, IDisposable diff --git a/Source/MQTTnet.Tests/Server/Subscribe_Tests.cs b/Source/MQTTnet.Tests/Server/Subscribe_Tests.cs index 991159c5d..9ac1b49cf 100644 --- a/Source/MQTTnet.Tests/Server/Subscribe_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Subscribe_Tests.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Globalization; +using System.Text; using MQTTnet.Exceptions; using MQTTnet.Formatter; using MQTTnet.Internal; @@ -98,7 +99,7 @@ public async Task Intercept_Subscribe_With_User_Properties() var client = await testEnvironment.ConnectClient(); - var subscribeOptions = testEnvironment.ClientFactory.CreateSubscribeOptionsBuilder().WithTopicFilter("X").WithUserProperty("A", "1").Build(); + var subscribeOptions = testEnvironment.ClientFactory.CreateSubscribeOptionsBuilder().WithTopicFilter("X").WithUserProperty("A", Encoding.UTF8.GetBytes("1")).Build(); await client.SubscribeAsync(subscribeOptions); CollectionAssert.AreEqual(subscribeOptions.UserProperties.ToList(), eventArgs.UserProperties); diff --git a/Source/MQTTnet.Tests/Server/Subscription_TopicHash_Tests.cs b/Source/MQTTnet.Tests/Server/Subscription_TopicHash_Tests.cs index 905bc7fa1..0b580785e 100644 --- a/Source/MQTTnet.Tests/Server/Subscription_TopicHash_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Subscription_TopicHash_Tests.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Linq; using System.Text; using MQTTnet.Packets; using MQTTnet.Protocol; diff --git a/Source/MQTTnet.Tests/Server/Unsubscribe_Tests.cs b/Source/MQTTnet.Tests/Server/Unsubscribe_Tests.cs index a6008ee3f..b6b61eeb4 100644 --- a/Source/MQTTnet.Tests/Server/Unsubscribe_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Unsubscribe_Tests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Text; using MQTTnet.Exceptions; using MQTTnet.Formatter; using MQTTnet.Internal; @@ -45,7 +46,7 @@ public async Task Intercept_Unsubscribe_With_User_Properties() var client = await testEnvironment.ConnectClient(); - var unsubscribeOptions = testEnvironment.ClientFactory.CreateUnsubscribeOptionsBuilder().WithTopicFilter("X").WithUserProperty("A", "1").Build(); + var unsubscribeOptions = testEnvironment.ClientFactory.CreateUnsubscribeOptionsBuilder().WithTopicFilter("X").WithUserProperty("A", Encoding.UTF8.GetBytes("1")).Build(); await client.UnsubscribeAsync(unsubscribeOptions); CollectionAssert.AreEqual(unsubscribeOptions.UserProperties.ToList(), eventArgs.UserProperties); diff --git a/Source/MQTTnet.Tests/Server/User_Properties_Tests.cs b/Source/MQTTnet.Tests/Server/User_Properties_Tests.cs index cf111c39b..afe08efb6 100644 --- a/Source/MQTTnet.Tests/Server/User_Properties_Tests.cs +++ b/Source/MQTTnet.Tests/Server/User_Properties_Tests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Text; using MQTTnet.Formatter; using MQTTnet.Internal; using MQTTnet.Packets; @@ -26,10 +27,10 @@ public async Task Use_User_Properties() var message = new MqttApplicationMessageBuilder() .WithTopic("A") - .WithUserProperty("x", "1") - .WithUserProperty("y", "2") - .WithUserProperty("z", "3") - .WithUserProperty("z", "4"); // z is here two times to test list of items + .WithUserProperty("x", Encoding.UTF8.GetBytes("1")) + .WithUserProperty("y", Encoding.UTF8.GetBytes("2")) + .WithUserProperty("z", Encoding.UTF8.GetBytes("3")) + .WithUserProperty("z", Encoding.UTF8.GetBytes("4")); // z is here two times to test list of items await receiver.SubscribeAsync(new MqttClientSubscribeOptions { diff --git a/Source/MQTTnet/Disconnecting/MqttClientDisconnectOptionsBuilder.cs b/Source/MQTTnet/Disconnecting/MqttClientDisconnectOptionsBuilder.cs index c0a8b0064..45f5d720c 100644 --- a/Source/MQTTnet/Disconnecting/MqttClientDisconnectOptionsBuilder.cs +++ b/Source/MQTTnet/Disconnecting/MqttClientDisconnectOptionsBuilder.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; using MQTTnet.Packets; namespace MQTTnet; @@ -54,4 +55,18 @@ public MqttClientDisconnectOptionsBuilder WithUserProperty(string name, string v _userProperties.Add(new MqttUserProperty(name, value)); return this; } + + public MqttClientDisconnectOptionsBuilder WithUserProperty(string name, ReadOnlyMemory value) + { + _userProperties ??= []; + _userProperties.Add(new MqttUserProperty(name, value)); + return this; + } + + public MqttClientDisconnectOptionsBuilder WithUserProperty(string name, ArraySegment value) + { + _userProperties ??= []; + _userProperties.Add(new MqttUserProperty(name, value)); + return this; + } } \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/MqttBufferWriter.cs b/Source/MQTTnet/Formatter/MqttBufferWriter.cs index c31f56f9d..9b7bdc0a3 100644 --- a/Source/MQTTnet/Formatter/MqttBufferWriter.cs +++ b/Source/MQTTnet/Formatter/MqttBufferWriter.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; using System.Runtime.CompilerServices; using System.Text; using MQTTnet.Exceptions; @@ -193,6 +194,29 @@ public void WriteString(string value) } } + public void WriteString(ReadOnlyMemory value) + { + var span = value.Span; + var length = span.Length; + + if (length > EncodedStringMaxLength) + { + throw new MqttProtocolViolationException($"The maximum string length is 65535. The current string has a length of {length}."); + } + + EnsureAdditionalCapacity(length + 2); + + _buffer[_position] = (byte)(length >> 8); + _buffer[_position + 1] = (byte)length; + + if (length > 0) + { + span.CopyTo(_buffer.AsSpan(_position + 2)); + } + + IncreasePosition(length + 2); + } + public void WriteTwoByteInteger(ushort value) { EnsureAdditionalCapacity(2); diff --git a/Source/MQTTnet/Formatter/V5/MqttV5PropertiesWriter.cs b/Source/MQTTnet/Formatter/V5/MqttV5PropertiesWriter.cs index 60c02d5fe..a6027329b 100644 --- a/Source/MQTTnet/Formatter/V5/MqttV5PropertiesWriter.cs +++ b/Source/MQTTnet/Formatter/V5/MqttV5PropertiesWriter.cs @@ -260,7 +260,7 @@ public void WriteUserProperties(List userProperties) { _bufferWriter.WriteByte((byte)MqttPropertyId.UserProperty); _bufferWriter.WriteString(property.Name); - _bufferWriter.WriteString(property.Value); + _bufferWriter.WriteString(property.ValueBuffer); } } diff --git a/Source/MQTTnet/MqttApplicationMessageBuilder.cs b/Source/MQTTnet/MqttApplicationMessageBuilder.cs index 95e2cef39..9df5a951a 100644 --- a/Source/MQTTnet/MqttApplicationMessageBuilder.cs +++ b/Source/MQTTnet/MqttApplicationMessageBuilder.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; using System.Buffers; using System.Runtime.InteropServices; using System.Text; @@ -263,10 +264,33 @@ public MqttApplicationMessageBuilder WithTopicAlias(ushort topicAlias) /// Adds the user property to the message. /// MQTT 5.0.0+ feature. /// + [Obsolete("Use the WithUserProperty accepting a ReadOnlyMemory for better performance.")] public MqttApplicationMessageBuilder WithUserProperty(string name, string value) { _userProperties ??= []; _userProperties.Add(new MqttUserProperty(name, value)); return this; } + + /// + /// Adds the user property to the message using a pre-encoded UTF-8 value buffer. + /// MQTT 5.0.0+ feature. + /// + public MqttApplicationMessageBuilder WithUserProperty(string name, ReadOnlyMemory value) + { + _userProperties ??= []; + _userProperties.Add(new MqttUserProperty(name, value)); + return this; + } + + /// + /// Adds the user property to the message using a pre-encoded UTF-8 value buffer. + /// MQTT 5.0.0+ feature. + /// + public MqttApplicationMessageBuilder WithUserProperty(string name, ArraySegment value) + { + _userProperties ??= []; + _userProperties.Add(new MqttUserProperty(name, value)); + return this; + } } \ No newline at end of file diff --git a/Source/MQTTnet/Options/MqttClientOptionsBuilder.cs b/Source/MQTTnet/Options/MqttClientOptionsBuilder.cs index 9b5f39a1c..c7bdb8c6c 100644 --- a/Source/MQTTnet/Options/MqttClientOptionsBuilder.cs +++ b/Source/MQTTnet/Options/MqttClientOptionsBuilder.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; using System.Net; using System.Net.Sockets; using System.Text; @@ -368,6 +369,28 @@ public MqttClientOptionsBuilder WithUserProperty(string name, string value) return this; } + public MqttClientOptionsBuilder WithUserProperty(string name, ReadOnlyMemory value) + { + if (_options.UserProperties == null) + { + _options.UserProperties = new List(); + } + + _options.UserProperties.Add(new MqttUserProperty(name, value)); + return this; + } + + public MqttClientOptionsBuilder WithUserProperty(string name, ArraySegment value) + { + if (_options.UserProperties == null) + { + _options.UserProperties = new List(); + } + + _options.UserProperties.Add(new MqttUserProperty(name, value)); + return this; + } + public MqttClientOptionsBuilder WithWebSocketServer(Action configure) { ArgumentNullException.ThrowIfNull(configure); @@ -468,4 +491,18 @@ public MqttClientOptionsBuilder WithWillUserProperty(string name, string value) _options.WillUserProperties.Add(new MqttUserProperty(name, value)); return this; } + + public MqttClientOptionsBuilder WithWillUserProperty(string name, ReadOnlyMemory value) + { + _options.WillUserProperties ??= []; + _options.WillUserProperties.Add(new MqttUserProperty(name, value)); + return this; + } + + public MqttClientOptionsBuilder WithWillUserProperty(string name, ArraySegment value) + { + _options.WillUserProperties ??= []; + _options.WillUserProperties.Add(new MqttUserProperty(name, value)); + return this; + } } \ No newline at end of file diff --git a/Source/MQTTnet/Packets/MqttUserProperty.cs b/Source/MQTTnet/Packets/MqttUserProperty.cs index eb02fb707..40d708e92 100644 --- a/Source/MQTTnet/Packets/MqttUserProperty.cs +++ b/Source/MQTTnet/Packets/MqttUserProperty.cs @@ -2,19 +2,35 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Text; + namespace MQTTnet.Packets; public sealed class MqttUserProperty { + readonly ReadOnlyMemory _valueBuffer; + public MqttUserProperty(string name, string value) + : this(name, new ReadOnlyMemory(Encoding.UTF8.GetBytes(value ?? throw new ArgumentNullException(nameof(value))))) + { + } + + public MqttUserProperty(string name, ArraySegment value) + : this(name, CreateMemory(value)) + { + } + + public MqttUserProperty(string name, ReadOnlyMemory value) { Name = name ?? throw new ArgumentNullException(nameof(name)); - Value = value ?? throw new ArgumentNullException(nameof(value)); + _valueBuffer = value; } public string Name { get; } - public string Value { get; } + public ReadOnlyMemory ValueBuffer => _valueBuffer; + + public string Value => this.ReadValueAsString(); public override bool Equals(object obj) { @@ -33,16 +49,46 @@ public bool Equals(MqttUserProperty other) return true; } - return string.Equals(Name, other.Name, StringComparison.Ordinal) && string.Equals(Value, other.Value, StringComparison.Ordinal); + if (!string.Equals(Name, other.Name, StringComparison.Ordinal)) + { + return false; + } + + return _valueBuffer.Span.SequenceEqual(other._valueBuffer.Span); } + public override int GetHashCode() { - return Name.GetHashCode() ^ Value.GetHashCode(); + var hashCode = new HashCode(); + + if (!string.IsNullOrEmpty(Name)) + { + hashCode.Add(Name, StringComparer.Ordinal); + } + else + { + hashCode.Add(0); + } + + hashCode.AddBytes(_valueBuffer.Span); + + return hashCode.ToHashCode(); } + public override string ToString() { return $"{Name} = {Value}"; } + + static ReadOnlyMemory CreateMemory(ArraySegment value) + { + if (value.Array == null) + { + throw new ArgumentNullException(nameof(value)); + } + + return new ReadOnlyMemory(value.Array, value.Offset, value.Count); + } } \ No newline at end of file diff --git a/Source/MQTTnet/Packets/MqttUserPropertyExtensions.cs b/Source/MQTTnet/Packets/MqttUserPropertyExtensions.cs new file mode 100644 index 000000000..8c2dd140f --- /dev/null +++ b/Source/MQTTnet/Packets/MqttUserPropertyExtensions.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Text; + +namespace MQTTnet.Packets; + +internal static class MqttUserPropertyExtensions +{ + /// + /// Reads the value of the user property as a UTF-8 string. + /// + public static string ReadValueAsString(this MqttUserProperty userProperty) + { + ArgumentNullException.ThrowIfNull(userProperty); + + var buffer = userProperty.ValueBuffer; + if (buffer.IsEmpty) + { + return string.Empty; + } + + return Encoding.UTF8.GetString(buffer.Span); + } +} diff --git a/Source/MQTTnet/Subscribing/MqttClientSubscribeOptionsBuilder.cs b/Source/MQTTnet/Subscribing/MqttClientSubscribeOptionsBuilder.cs index e49af713f..5586d4362 100644 --- a/Source/MQTTnet/Subscribing/MqttClientSubscribeOptionsBuilder.cs +++ b/Source/MQTTnet/Subscribing/MqttClientSubscribeOptionsBuilder.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; using MQTTnet.Exceptions; using MQTTnet.Packets; using MQTTnet.Protocol; @@ -77,6 +78,7 @@ public MqttClientSubscribeOptionsBuilder WithTopicFilter(MqttTopicFilter topicFi /// Adds the user property to the subscribe options. /// MQTT 5.0.0+ feature. /// + [Obsolete("Use the WithUserProperty accepting a ReadOnlyMemory for better performance.")] public MqttClientSubscribeOptionsBuilder WithUserProperty(string name, string value) { _subscribeOptions.UserProperties ??= []; @@ -84,4 +86,28 @@ public MqttClientSubscribeOptionsBuilder WithUserProperty(string name, string va return this; } + + /// + /// Adds the user property to the subscribe options. + /// MQTT 5.0.0+ feature. + /// + public MqttClientSubscribeOptionsBuilder WithUserProperty(string name, ReadOnlyMemory value) + { + _subscribeOptions.UserProperties ??= []; + _subscribeOptions.UserProperties.Add(new MqttUserProperty(name, value)); + + return this; + } + + /// + /// Adds the user property to the subscribe options. + /// MQTT 5.0.0+ feature. + /// + public MqttClientSubscribeOptionsBuilder WithUserProperty(string name, ArraySegment value) + { + _subscribeOptions.UserProperties ??= []; + _subscribeOptions.UserProperties.Add(new MqttUserProperty(name, value)); + + return this; + } } \ No newline at end of file diff --git a/Source/MQTTnet/Unsubscribing/MqttClientUnsubscribeOptionsBuilder.cs b/Source/MQTTnet/Unsubscribing/MqttClientUnsubscribeOptionsBuilder.cs index abec6c353..aee2b9730 100644 --- a/Source/MQTTnet/Unsubscribing/MqttClientUnsubscribeOptionsBuilder.cs +++ b/Source/MQTTnet/Unsubscribing/MqttClientUnsubscribeOptionsBuilder.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; using MQTTnet.Packets; namespace MQTTnet; @@ -36,11 +37,30 @@ public MqttClientUnsubscribeOptionsBuilder WithTopicFilter(MqttTopicFilter topic /// Adds the user property to the unsubscribe options. /// MQTT 5.0.0+ feature. /// + [Obsolete("Use the WithUserProperty accepting a ReadOnlyMemory for better performance.")] public MqttClientUnsubscribeOptionsBuilder WithUserProperty(string name, string value) { return WithUserProperty(new MqttUserProperty(name, value)); } + /// + /// Adds the user property to the unsubscribe options. + /// MQTT 5.0.0+ feature. + /// + public MqttClientUnsubscribeOptionsBuilder WithUserProperty(string name, ReadOnlyMemory value) + { + return WithUserProperty(new MqttUserProperty(name, value)); + } + + /// + /// Adds the user property to the unsubscribe options. + /// MQTT 5.0.0+ feature. + /// + public MqttClientUnsubscribeOptionsBuilder WithUserProperty(string name, ArraySegment value) + { + return WithUserProperty(new MqttUserProperty(name, value)); + } + /// /// Adds the user property to the unsubscribe options. /// MQTT 5.0.0+ feature. diff --git a/global.json b/global.json new file mode 100644 index 000000000..8f73781ca --- /dev/null +++ b/global.json @@ -0,0 +1,5 @@ +{ + "test": { + "runner": "Microsoft.Testing.Platform" + } +} \ No newline at end of file