From 5ad5f36f88dc5e8ab990ff52d4b8a8234f2b649e Mon Sep 17 00:00:00 2001 From: Pawel Kadluczka Date: Tue, 15 Aug 2017 13:32:20 -0700 Subject: [PATCH] Changing length prefixing to separator for JSON C# --- .../Internal/Encoders/Base64Encoder.cs | 5 +- .../LengthPrefixedTextMessageParser.cs | 185 ++++++++++++++++++ .../LengthPrefixedTextMessageWriter.cs | 44 +++++ .../Formatters/BinaryMessageFormatter.cs | 2 +- .../Formatters/BinaryMessageParser.cs | 2 +- .../Formatters/TextMessageFormatter.cs | 34 +--- .../Internal/Formatters/TextMessageParser.cs | 167 +--------------- .../Internal/Protocol/JsonHubProtocol.cs | 2 +- .../Protocol/MessagePackHubProtocol.cs | 2 +- .../Internal/Protocol/NegotiationProtocol.cs | 2 +- .../HubConnectionProtocolTests.cs | 8 +- .../ServerSentEventsParserTests.cs | 2 +- .../TestConnection.cs | 2 +- ...engthPrefixedTextMessageFormatterTests.cs} | 13 +- .../LengthPrefixedTextMessageParserTests.cs} | 17 +- .../Formatters/BinaryMessageFormatterTests.cs | 2 +- .../Formatters/BinaryMessageParserTests.cs | 5 +- .../Formatters/TextMessageFormatterTests.cs | 24 +++ .../Formatters/TextMessageParserTests.cs | 51 +++++ .../Internal/Protocol/JsonHubProtocolTests.cs | 2 +- .../Protocol/NegotiationProtocolTests.cs | 12 +- .../MessageParserBenchmark.cs | 2 +- 22 files changed, 354 insertions(+), 231 deletions(-) create mode 100644 src/Microsoft.AspNetCore.SignalR.Common/Internal/Encoders/LengthPrefixedTextMessageParser.cs create mode 100644 src/Microsoft.AspNetCore.SignalR.Common/Internal/Encoders/LengthPrefixedTextMessageWriter.cs rename test/{Microsoft.AspNetCore.SignalR.Tests/Formatters => Microsoft.AspNetCore.SignalR.Client.Tests}/ServerSentEventsParserTests.cs (99%) rename test/{Microsoft.AspNetCore.SignalR.Tests/Formatters/TextMessageFormatterTests.cs => Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Encoders/LengthPrefixedTextMessageFormatterTests.cs} (79%) rename test/{Microsoft.AspNetCore.SignalR.Tests/Formatters/TextMessageParserTests.cs => Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Encoders/LengthPrefixedTextMessageParserTests.cs} (82%) rename test/{Microsoft.AspNetCore.SignalR.Tests => Microsoft.AspNetCore.SignalR.Common.Tests/Internal}/Formatters/BinaryMessageFormatterTests.cs (98%) rename test/{Microsoft.AspNetCore.SignalR.Tests => Microsoft.AspNetCore.SignalR.Common.Tests/Internal}/Formatters/BinaryMessageParserTests.cs (95%) create mode 100644 test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Formatters/TextMessageFormatterTests.cs create mode 100644 test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Formatters/TextMessageParserTests.cs diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Encoders/Base64Encoder.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Encoders/Base64Encoder.cs index bd5ae52e7c..f5a8c044b0 100644 --- a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Encoders/Base64Encoder.cs +++ b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Encoders/Base64Encoder.cs @@ -4,7 +4,6 @@ using System; using System.IO; using System.Text; -using Microsoft.AspNetCore.Sockets.Internal.Formatters; namespace Microsoft.AspNetCore.SignalR.Internal.Encoders { @@ -13,7 +12,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Encoders public byte[] Decode(byte[] payload) { var buffer = new ReadOnlyBuffer(payload); - TextMessageParser.TryParseMessage(ref buffer, out var message); + LengthPrefixedTextMessageParser.TryParseMessage(ref buffer, out var message); return Convert.FromBase64String(Encoding.UTF8.GetString(message.ToArray())); } @@ -23,7 +22,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Encoders var buffer = Encoding.UTF8.GetBytes(Convert.ToBase64String(payload)); using (var stream = new MemoryStream()) { - TextMessageFormatter.WriteMessage(buffer, stream); + LengthPrefixedTextMessageWriter.WriteMessage(buffer, stream); return stream.ToArray(); } } diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Encoders/LengthPrefixedTextMessageParser.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Encoders/LengthPrefixedTextMessageParser.cs new file mode 100644 index 0000000000..e4943a1f35 --- /dev/null +++ b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Encoders/LengthPrefixedTextMessageParser.cs @@ -0,0 +1,185 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Text; + +namespace Microsoft.AspNetCore.SignalR.Internal.Encoders +{ + public static class LengthPrefixedTextMessageParser + { + private const int Int32OverflowLength = 10; + + /// + /// Attempts to parse a message from the buffer. Returns 'false' if there is not enough data to complete a message. Throws an + /// exception if there is a format error in the provided data. + /// + public static bool TryParseMessage(ref ReadOnlyBuffer buffer, out ReadOnlyBuffer payload) + { + payload = default(ReadOnlyBuffer); + var span = buffer.Span; + + if (!TryReadLength(span, out var index, out var length)) + { + return false; + } + + var remaining = buffer.Slice(index); + span = remaining.Span; + + if (!TryReadDelimiter(span, LengthPrefixedTextMessageWriter.FieldDelimiter, "length")) + { + return false; + } + + // Skip the delimeter + remaining = remaining.Slice(1); + + if (remaining.Length < length + 1) + { + return false; + } + + payload = remaining.Slice(0, length); + + remaining = remaining.Slice(length); + + if (!TryReadDelimiter(remaining.Span, LengthPrefixedTextMessageWriter.MessageDelimiter, "payload")) + { + return false; + } + + // Skip the delimeter + buffer = remaining.Slice(1); + return true; + } + + private static bool TryReadLength(ReadOnlySpan buffer, out int index, out int length) + { + length = 0; + // Read until the first ':' to find the length + index = buffer.IndexOf((byte)LengthPrefixedTextMessageWriter.FieldDelimiter); + + if (index == -1) + { + // Insufficient data + return false; + } + + var lengthSpan = buffer.Slice(0, index); + + if (!TryParseInt32(lengthSpan, out length, out var bytesConsumed) || bytesConsumed < lengthSpan.Length) + { + throw new FormatException($"Invalid length: '{Encoding.UTF8.GetString(lengthSpan.ToArray())}'"); + } + + return true; + } + + private static bool TryReadDelimiter(ReadOnlySpan buffer, char delimiter, string field) + { + if (buffer.Length == 0) + { + return false; + } + + if (buffer[0] != delimiter) + { + throw new FormatException($"Missing delimiter '{delimiter}' after {field}"); + } + + return true; + } + + private static bool TryParseInt32(ReadOnlySpan text, out int value, out int bytesConsumed) + { + if (text.Length < 1) + { + bytesConsumed = 0; + value = default(int); + return false; + } + + int indexOfFirstDigit = 0; + int sign = 1; + if (text[0] == '-') + { + indexOfFirstDigit = 1; + sign = -1; + } + else if (text[0] == '+') + { + indexOfFirstDigit = 1; + } + + int overflowLength = Int32OverflowLength + indexOfFirstDigit; + + // Parse the first digit separately. If invalid here, we need to return false. + int firstDigit = text[indexOfFirstDigit] - 48; // '0' + if (firstDigit < 0 || firstDigit > 9) + { + bytesConsumed = 0; + value = default(int); + return false; + } + int parsedValue = firstDigit; + + if (text.Length < overflowLength) + { + // Length is less than Int32OverflowLength; overflow is not possible + for (int index = indexOfFirstDigit + 1; index < text.Length; index++) + { + int nextDigit = text[index] - 48; // '0' + if (nextDigit < 0 || nextDigit > 9) + { + bytesConsumed = index; + value = parsedValue * sign; + return true; + } + parsedValue = parsedValue * 10 + nextDigit; + } + } + else + { + // Length is greater than Int32OverflowLength; overflow is only possible after Int32OverflowLength + // digits. There may be no overflow after Int32OverflowLength if there are leading zeroes. + for (int index = indexOfFirstDigit + 1; index < overflowLength - 1; index++) + { + int nextDigit = text[index] - 48; // '0' + if (nextDigit < 0 || nextDigit > 9) + { + bytesConsumed = index; + value = parsedValue * sign; + return true; + } + parsedValue = parsedValue * 10 + nextDigit; + } + for (int index = overflowLength - 1; index < text.Length; index++) + { + int nextDigit = text[index] - 48; // '0' + if (nextDigit < 0 || nextDigit > 9) + { + bytesConsumed = index; + value = parsedValue * sign; + return true; + } + // If parsedValue > (int.MaxValue / 10), any more appended digits will cause overflow. + // if parsedValue == (int.MaxValue / 10), any nextDigit greater than 7 or 8 (depending on sign) implies overflow. + bool positive = sign > 0; + bool nextDigitTooLarge = nextDigit > 8 || (positive && nextDigit > 7); + if (parsedValue > int.MaxValue / 10 || parsedValue == int.MaxValue / 10 && nextDigitTooLarge) + { + bytesConsumed = 0; + value = default(int); + return false; + } + parsedValue = parsedValue * 10 + nextDigit; + } + } + + bytesConsumed = text.Length; + value = parsedValue * sign; + return true; + } + } +} diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Encoders/LengthPrefixedTextMessageWriter.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Encoders/LengthPrefixedTextMessageWriter.cs new file mode 100644 index 0000000000..516b5e9d0e --- /dev/null +++ b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Encoders/LengthPrefixedTextMessageWriter.cs @@ -0,0 +1,44 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Buffers; +using System.Globalization; +using System.IO; +using System.Text; + +namespace Microsoft.AspNetCore.SignalR.Internal.Encoders +{ + public static class LengthPrefixedTextMessageWriter + { + private const int Int32OverflowLength = 10; + + internal const char FieldDelimiter = ':'; + internal const char MessageDelimiter = ';'; + + public static void WriteMessage(ReadOnlySpan payload, Stream output) + { + // Calculate the length, it's the number of characters for text messages, but number of base64 characters for binary + + // Write the length as a string + + // Super inefficient... + var lengthString = payload.Length.ToString(CultureInfo.InvariantCulture); + var buffer = ArrayPool.Shared.Rent(Int32OverflowLength); + var encodedLength = Encoding.UTF8.GetBytes(lengthString, 0, lengthString.Length, buffer, 0); + output.Write(buffer, 0, encodedLength); + ArrayPool.Shared.Return(buffer); + + // Write the field delimiter ':' + output.WriteByte((byte)FieldDelimiter); + + buffer = ArrayPool.Shared.Rent(payload.Length); + payload.CopyTo(buffer); + output.Write(buffer, 0, payload.Length); + ArrayPool.Shared.Return(buffer); + + // Terminator + output.WriteByte((byte)MessageDelimiter); + } + } +} diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Formatters/BinaryMessageFormatter.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Formatters/BinaryMessageFormatter.cs index 3ce4cbd685..6d23544a9e 100644 --- a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Formatters/BinaryMessageFormatter.cs +++ b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Formatters/BinaryMessageFormatter.cs @@ -6,7 +6,7 @@ using System.Binary; using System.Buffers; using System.IO; -namespace Microsoft.AspNetCore.Sockets.Internal.Formatters +namespace Microsoft.AspNetCore.SignalR.Internal.Formatters { public static class BinaryMessageFormatter { diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Formatters/BinaryMessageParser.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Formatters/BinaryMessageParser.cs index 86b5887738..e730528a9f 100644 --- a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Formatters/BinaryMessageParser.cs +++ b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Formatters/BinaryMessageParser.cs @@ -4,7 +4,7 @@ using System; using System.Binary; -namespace Microsoft.AspNetCore.Sockets.Internal.Formatters +namespace Microsoft.AspNetCore.SignalR.Internal.Formatters { public static class BinaryMessageParser { diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Formatters/TextMessageFormatter.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Formatters/TextMessageFormatter.cs index 3980794abb..ad63c4568d 100644 --- a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Formatters/TextMessageFormatter.cs +++ b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Formatters/TextMessageFormatter.cs @@ -1,44 +1,24 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Buffers; -using System.Globalization; using System.IO; -using System.Text; -namespace Microsoft.AspNetCore.Sockets.Internal.Formatters +namespace Microsoft.AspNetCore.SignalR.Internal.Formatters { public static class TextMessageFormatter { - private const int Int32OverflowLength = 10; - - internal const char FieldDelimiter = ':'; - internal const char MessageDelimiter = ';'; + // This record separator is supposed to be used only for JSON payloads where 0x1e character + // will not occur (is not a valid character) and therefore it is safe to not escape it + internal static readonly byte RecordSeparator = 0x1e; public static void WriteMessage(ReadOnlySpan payload, Stream output) { - // Calculate the length, it's the number of characters for text messages, but number of base64 characters for binary - - // Write the length as a string - - // Super inefficient... - var lengthString = payload.Length.ToString(CultureInfo.InvariantCulture); - var buffer = ArrayPool.Shared.Rent(Int32OverflowLength); - var encodedLength = Encoding.UTF8.GetBytes(lengthString, 0, lengthString.Length, buffer, 0); - output.Write(buffer, 0, encodedLength); - ArrayPool.Shared.Return(buffer); - - // Write the field delimiter ':' - output.WriteByte((byte)FieldDelimiter); - - buffer = ArrayPool.Shared.Rent(payload.Length); + var buffer = ArrayPool.Shared.Rent(payload.Length); payload.CopyTo(buffer); output.Write(buffer, 0, payload.Length); - ArrayPool.Shared.Return(buffer); - - // Terminator - output.WriteByte((byte)MessageDelimiter); + output.WriteByte(RecordSeparator); } } } diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Formatters/TextMessageParser.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Formatters/TextMessageParser.cs index 0233ef00a7..9ead864bee 100644 --- a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Formatters/TextMessageParser.cs +++ b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Formatters/TextMessageParser.cs @@ -2,184 +2,27 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Text; -namespace Microsoft.AspNetCore.Sockets.Internal.Formatters +namespace Microsoft.AspNetCore.SignalR.Internal.Formatters { public static class TextMessageParser { - private const int Int32OverflowLength = 10; - - /// - /// Attempts to parse a message from the buffer. Returns 'false' if there is not enough data to complete a message. Throws an - /// exception if there is a format error in the provided data. - /// public static bool TryParseMessage(ref ReadOnlyBuffer buffer, out ReadOnlyBuffer payload) { payload = default(ReadOnlyBuffer); - var span = buffer.Span; - - if (!TryReadLength(span, out var index, out var length)) - { - return false; - } - - var remaining = buffer.Slice(index); - span = remaining.Span; - - if (!TryReadDelimiter(span, TextMessageFormatter.FieldDelimiter, "length")) - { - return false; - } - - // Skip the delimeter - remaining = remaining.Slice(1); - - if (remaining.Length < length + 1) - { - return false; - } - - payload = remaining.Slice(0, length); - - remaining = remaining.Slice(length); - - if (!TryReadDelimiter(remaining.Span, TextMessageFormatter.MessageDelimiter, "payload")) - { - return false; - } - - // Skip the delimeter - buffer = remaining.Slice(1); - return true; - } - - private static bool TryReadLength(ReadOnlySpan buffer, out int index, out int length) - { - length = 0; - // Read until the first ':' to find the length - index = buffer.IndexOf((byte)TextMessageFormatter.FieldDelimiter); + var index = buffer.Span.IndexOf(TextMessageFormatter.RecordSeparator); if (index == -1) { - // Insufficient data return false; } - var lengthSpan = buffer.Slice(0, index); + payload = buffer.Slice(0, index); - if (!TryParseInt32(lengthSpan, out length, out var bytesConsumed) || bytesConsumed < lengthSpan.Length) - { - throw new FormatException($"Invalid length: '{Encoding.UTF8.GetString(lengthSpan.ToArray())}'"); - } + // Skip record separator + buffer = buffer.Slice(index + 1); return true; } - - private static bool TryReadDelimiter(ReadOnlySpan buffer, char delimiter, string field) - { - if (buffer.Length == 0) - { - return false; - } - - if (buffer[0] != delimiter) - { - throw new FormatException($"Missing delimiter '{delimiter}' after {field}"); - } - - return true; - } - - private static bool TryParseInt32(ReadOnlySpan text, out int value, out int bytesConsumed) - { - if (text.Length < 1) - { - bytesConsumed = 0; - value = default(int); - return false; - } - - int indexOfFirstDigit = 0; - int sign = 1; - if (text[0] == '-') - { - indexOfFirstDigit = 1; - sign = -1; - } - else if (text[0] == '+') - { - indexOfFirstDigit = 1; - } - - int overflowLength = Int32OverflowLength + indexOfFirstDigit; - - // Parse the first digit separately. If invalid here, we need to return false. - int firstDigit = text[indexOfFirstDigit] - 48; // '0' - if (firstDigit < 0 || firstDigit > 9) - { - bytesConsumed = 0; - value = default(int); - return false; - } - int parsedValue = firstDigit; - - if (text.Length < overflowLength) - { - // Length is less than Int32OverflowLength; overflow is not possible - for (int index = indexOfFirstDigit + 1; index < text.Length; index++) - { - int nextDigit = text[index] - 48; // '0' - if (nextDigit < 0 || nextDigit > 9) - { - bytesConsumed = index; - value = parsedValue * sign; - return true; - } - parsedValue = parsedValue * 10 + nextDigit; - } - } - else - { - // Length is greater than Int32OverflowLength; overflow is only possible after Int32OverflowLength - // digits. There may be no overflow after Int32OverflowLength if there are leading zeroes. - for (int index = indexOfFirstDigit + 1; index < overflowLength - 1; index++) - { - int nextDigit = text[index] - 48; // '0' - if (nextDigit < 0 || nextDigit > 9) - { - bytesConsumed = index; - value = parsedValue * sign; - return true; - } - parsedValue = parsedValue * 10 + nextDigit; - } - for (int index = overflowLength - 1; index < text.Length; index++) - { - int nextDigit = text[index] - 48; // '0' - if (nextDigit < 0 || nextDigit > 9) - { - bytesConsumed = index; - value = parsedValue * sign; - return true; - } - // If parsedValue > (int.MaxValue / 10), any more appended digits will cause overflow. - // if parsedValue == (int.MaxValue / 10), any nextDigit greater than 7 or 8 (depending on sign) implies overflow. - bool positive = sign > 0; - bool nextDigitTooLarge = nextDigit > 8 || (positive && nextDigit > 7); - if (parsedValue > int.MaxValue / 10 || parsedValue == int.MaxValue / 10 && nextDigitTooLarge) - { - bytesConsumed = 0; - value = default(int); - return false; - } - parsedValue = parsedValue * 10 + nextDigit; - } - } - - bytesConsumed = text.Length; - value = parsedValue * sign; - return true; - } } } diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/JsonHubProtocol.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/JsonHubProtocol.cs index d98f2968f2..4bf45cd274 100644 --- a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/JsonHubProtocol.cs +++ b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/JsonHubProtocol.cs @@ -4,7 +4,7 @@ using System; using System.Collections.Generic; using System.IO; -using Microsoft.AspNetCore.Sockets.Internal.Formatters; +using Microsoft.AspNetCore.SignalR.Internal.Formatters; using Newtonsoft.Json; using Newtonsoft.Json.Linq; diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/MessagePackHubProtocol.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/MessagePackHubProtocol.cs index 7aa84abab6..f972ad6a66 100644 --- a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/MessagePackHubProtocol.cs +++ b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/MessagePackHubProtocol.cs @@ -4,7 +4,7 @@ using System; using System.Collections.Generic; using System.IO; -using Microsoft.AspNetCore.Sockets.Internal.Formatters; +using Microsoft.AspNetCore.SignalR.Internal.Formatters; using MsgPack; using MsgPack.Serialization; diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/NegotiationProtocol.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/NegotiationProtocol.cs index d68f480b24..7f6abe5b2c 100644 --- a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/NegotiationProtocol.cs +++ b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/NegotiationProtocol.cs @@ -3,7 +3,7 @@ using System; using System.IO; -using Microsoft.AspNetCore.Sockets.Internal.Formatters; +using Microsoft.AspNetCore.SignalR.Internal.Formatters; using Newtonsoft.Json; using Newtonsoft.Json.Linq; diff --git a/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionProtocolTests.cs b/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionProtocolTests.cs index 4fe8e59052..d18ac39e14 100644 --- a/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionProtocolTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionProtocolTests.cs @@ -37,7 +37,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests await connection.ReadSentTextMessageAsync().OrTimeout(); var invokeMessage = await connection.ReadSentTextMessageAsync().OrTimeout(); - Assert.Equal("78:{\"invocationId\":\"1\",\"type\":1,\"target\":\"Foo\",\"nonBlocking\":true,\"arguments\":[]};", invokeMessage); + Assert.Equal("{\"invocationId\":\"1\",\"type\":1,\"target\":\"Foo\",\"nonBlocking\":true,\"arguments\":[]}\u001e", invokeMessage); } finally { @@ -56,7 +56,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests await hubConnection.StartAsync(); var negotiationMessage = await connection.ReadSentTextMessageAsync().OrTimeout(); - Assert.Equal("19:{\"protocol\":\"json\"};", negotiationMessage); + Assert.Equal("{\"protocol\":\"json\"}\u001e", negotiationMessage); } finally { @@ -80,7 +80,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests await connection.ReadSentTextMessageAsync().OrTimeout(); var invokeMessage = await connection.ReadSentTextMessageAsync().OrTimeout(); - Assert.Equal("59:{\"invocationId\":\"1\",\"type\":1,\"target\":\"Foo\",\"arguments\":[]};", invokeMessage); + Assert.Equal("{\"invocationId\":\"1\",\"type\":1,\"target\":\"Foo\",\"arguments\":[]}\u001e", invokeMessage); } finally { @@ -104,7 +104,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests await connection.ReadSentTextMessageAsync().OrTimeout(); var invokeMessage = await connection.ReadSentTextMessageAsync().OrTimeout(); - Assert.Equal("59:{\"invocationId\":\"1\",\"type\":1,\"target\":\"Foo\",\"arguments\":[]};", invokeMessage); + Assert.Equal("{\"invocationId\":\"1\",\"type\":1,\"target\":\"Foo\",\"arguments\":[]}\u001e", invokeMessage); // Complete the channel await connection.ReceiveJsonMessage(new { invocationId = "1", type = 3 }).OrTimeout(); diff --git a/test/Microsoft.AspNetCore.SignalR.Tests/Formatters/ServerSentEventsParserTests.cs b/test/Microsoft.AspNetCore.SignalR.Client.Tests/ServerSentEventsParserTests.cs similarity index 99% rename from test/Microsoft.AspNetCore.SignalR.Tests/Formatters/ServerSentEventsParserTests.cs rename to test/Microsoft.AspNetCore.SignalR.Client.Tests/ServerSentEventsParserTests.cs index 17c5dc9181..5a2eef03f6 100644 --- a/test/Microsoft.AspNetCore.SignalR.Tests/Formatters/ServerSentEventsParserTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Client.Tests/ServerSentEventsParserTests.cs @@ -9,7 +9,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Sockets.Internal.Formatters; using Xunit; -namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters +namespace Microsoft.AspNetCore.SignalR.Client.Tests { public class ServerSentEventsParserTests { diff --git a/test/Microsoft.AspNetCore.SignalR.Client.Tests/TestConnection.cs b/test/Microsoft.AspNetCore.SignalR.Client.Tests/TestConnection.cs index ffe1b7f314..2e57f53cf4 100644 --- a/test/Microsoft.AspNetCore.SignalR.Client.Tests/TestConnection.cs +++ b/test/Microsoft.AspNetCore.SignalR.Client.Tests/TestConnection.cs @@ -8,10 +8,10 @@ using System.Threading; using System.Threading.Tasks; using System.Threading.Tasks.Channels; using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.SignalR.Internal.Formatters; using Microsoft.AspNetCore.Sockets; using Microsoft.AspNetCore.Sockets.Client; using Microsoft.AspNetCore.Sockets.Features; -using Microsoft.AspNetCore.Sockets.Internal.Formatters; using Newtonsoft.Json; namespace Microsoft.AspNetCore.SignalR.Client.Tests diff --git a/test/Microsoft.AspNetCore.SignalR.Tests/Formatters/TextMessageFormatterTests.cs b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Encoders/LengthPrefixedTextMessageFormatterTests.cs similarity index 79% rename from test/Microsoft.AspNetCore.SignalR.Tests/Formatters/TextMessageFormatterTests.cs rename to test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Encoders/LengthPrefixedTextMessageFormatterTests.cs index 8060b961d5..8abb2c70e8 100644 --- a/test/Microsoft.AspNetCore.SignalR.Tests/Formatters/TextMessageFormatterTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Encoders/LengthPrefixedTextMessageFormatterTests.cs @@ -1,15 +1,14 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; using System.IO; using System.Text; -using Microsoft.AspNetCore.Sockets.Internal.Formatters; +using Microsoft.AspNetCore.SignalR.Internal.Encoders; using Xunit; -namespace Microsoft.AspNetCore.Sockets.Tests.Internal.Formatters +namespace Microsoft.AspNetCore.SignalR.Tests.Internal.Encoders { - public class TextMessageFormatterTests + public class LengthPrefixedTextMessageFormatterTests { [Fact] public void WriteMultipleMessages() @@ -24,12 +23,12 @@ namespace Microsoft.AspNetCore.Sockets.Tests.Internal.Formatters var output = new MemoryStream(); foreach (var message in messages) { - TextMessageFormatter.WriteMessage(message, output); + LengthPrefixedTextMessageWriter.WriteMessage(message, output); } Assert.Equal(expectedEncoding, Encoding.UTF8.GetString(output.ToArray())); } - + [Theory] [InlineData(8, "0:;", "")] [InlineData(8, "3:ABC;", "ABC")] @@ -40,7 +39,7 @@ namespace Microsoft.AspNetCore.Sockets.Tests.Internal.Formatters var message = Encoding.UTF8.GetBytes(payload); var output = new MemoryStream(); - TextMessageFormatter.WriteMessage(message, output); + LengthPrefixedTextMessageWriter.WriteMessage(message, output); Assert.Equal(encoded, Encoding.UTF8.GetString(output.ToArray())); } diff --git a/test/Microsoft.AspNetCore.SignalR.Tests/Formatters/TextMessageParserTests.cs b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Encoders/LengthPrefixedTextMessageParserTests.cs similarity index 82% rename from test/Microsoft.AspNetCore.SignalR.Tests/Formatters/TextMessageParserTests.cs rename to test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Encoders/LengthPrefixedTextMessageParserTests.cs index 8213e04a1f..5716daaa90 100644 --- a/test/Microsoft.AspNetCore.SignalR.Tests/Formatters/TextMessageParserTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Encoders/LengthPrefixedTextMessageParserTests.cs @@ -2,15 +2,14 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Buffers; using System.Collections.Generic; using System.Text; -using Microsoft.AspNetCore.Sockets.Internal.Formatters; +using Microsoft.AspNetCore.SignalR.Internal.Encoders; using Xunit; -namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters +namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Encoders { - public class TextMessageParserTests + public class LengthPrefixedTextMessageParserTests { [Theory] [InlineData(0, "0:;", "")] @@ -21,7 +20,7 @@ namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters { ReadOnlyBuffer buffer = Encoding.UTF8.GetBytes(encoded); - Assert.True(TextMessageParser.TryParseMessage(ref buffer, out var message)); + Assert.True(LengthPrefixedTextMessageParser.TryParseMessage(ref buffer, out var message)); Assert.Equal(0, buffer.Length); Assert.Equal(Encoding.UTF8.GetBytes(payload), message.ToArray()); } @@ -33,7 +32,7 @@ namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters ReadOnlyBuffer buffer = Encoding.UTF8.GetBytes(encoded); var messages = new List(); - while (TextMessageParser.TryParseMessage(ref buffer, out var message)) + while (LengthPrefixedTextMessageParser.TryParseMessage(ref buffer, out var message)) { messages.Add(message.ToArray()); } @@ -56,7 +55,7 @@ namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters public void ReadIncompleteMessages(string encoded) { ReadOnlyBuffer buffer = Encoding.UTF8.GetBytes(encoded); - Assert.False(TextMessageParser.TryParseMessage(ref buffer, out _)); + Assert.False(LengthPrefixedTextMessageParser.TryParseMessage(ref buffer, out _)); } [Theory] @@ -70,7 +69,7 @@ namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters ReadOnlyBuffer buffer = Encoding.UTF8.GetBytes(encoded); var ex = Assert.Throws(() => { - TextMessageParser.TryParseMessage(ref buffer, out _); + LengthPrefixedTextMessageParser.TryParseMessage(ref buffer, out _); }); Assert.Equal(expectedMessage, ex.Message); } @@ -83,7 +82,7 @@ namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters ReadOnlyBuffer buffer = new byte[] { 0x48, 0x65, 0x80, 0x6C, 0x6F, (byte)':' }; var ex = Assert.Throws(() => { - TextMessageParser.TryParseMessage(ref buffer, out _); + LengthPrefixedTextMessageParser.TryParseMessage(ref buffer, out _); }); Assert.Equal("Invalid length: 'He�lo'", ex.Message); } diff --git a/test/Microsoft.AspNetCore.SignalR.Tests/Formatters/BinaryMessageFormatterTests.cs b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Formatters/BinaryMessageFormatterTests.cs similarity index 98% rename from test/Microsoft.AspNetCore.SignalR.Tests/Formatters/BinaryMessageFormatterTests.cs rename to test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Formatters/BinaryMessageFormatterTests.cs index d55a163771..6ce23c611e 100644 --- a/test/Microsoft.AspNetCore.SignalR.Tests/Formatters/BinaryMessageFormatterTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Formatters/BinaryMessageFormatterTests.cs @@ -4,7 +4,7 @@ using System.IO; using System.Linq; using System.Text; -using Microsoft.AspNetCore.Sockets.Internal.Formatters; +using Microsoft.AspNetCore.SignalR.Internal.Formatters; using Xunit; namespace Microsoft.AspNetCore.Sockets.Tests.Internal.Formatters diff --git a/test/Microsoft.AspNetCore.SignalR.Tests/Formatters/BinaryMessageParserTests.cs b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Formatters/BinaryMessageParserTests.cs similarity index 95% rename from test/Microsoft.AspNetCore.SignalR.Tests/Formatters/BinaryMessageParserTests.cs rename to test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Formatters/BinaryMessageParserTests.cs index 25cbc0abd1..7c3fe7750f 100644 --- a/test/Microsoft.AspNetCore.SignalR.Tests/Formatters/BinaryMessageParserTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Formatters/BinaryMessageParserTests.cs @@ -2,13 +2,12 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Buffers; using System.Collections.Generic; using System.Text; -using Microsoft.AspNetCore.Sockets.Internal.Formatters; +using Microsoft.AspNetCore.SignalR.Internal.Formatters; using Xunit; -namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters +namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Formatters { public class BinaryMessageParserTests { diff --git a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Formatters/TextMessageFormatterTests.cs b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Formatters/TextMessageFormatterTests.cs new file mode 100644 index 0000000000..f9ae1b4fd5 --- /dev/null +++ b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Formatters/TextMessageFormatterTests.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Text; +using Microsoft.AspNetCore.SignalR.Internal.Formatters; +using Xunit; + +namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Formatters +{ + public class TextMessageFormatterTests + { + [Fact] + public void WriteMessage() + { + using (var ms = new MemoryStream()) + { + TextMessageFormatter.WriteMessage(new ReadOnlySpan(Encoding.UTF8.GetBytes("ABC")), ms); + Assert.Equal("ABC\u001e", Encoding.UTF8.GetString(ms.ToArray())); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Formatters/TextMessageParserTests.cs b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Formatters/TextMessageParserTests.cs new file mode 100644 index 0000000000..e6d953cf87 --- /dev/null +++ b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Formatters/TextMessageParserTests.cs @@ -0,0 +1,51 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Text; +using Microsoft.AspNetCore.SignalR.Internal.Formatters; +using Xunit; + +namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Formatters +{ + public class TextMessageParserTests + { + [Fact] + public void ReadMessage() + { + var message = new ReadOnlyBuffer(Encoding.UTF8.GetBytes("ABC\u001e")); + + Assert.True(TextMessageParser.TryParseMessage(ref message, out var payload)); + Assert.Equal("ABC", Encoding.UTF8.GetString(payload.ToArray())); + Assert.False(TextMessageParser.TryParseMessage(ref message, out payload)); + } + + [Fact] + public void TryReadingIncompleteMessage() + { + var message = new ReadOnlyBuffer(Encoding.UTF8.GetBytes("ABC")); + Assert.False(TextMessageParser.TryParseMessage(ref message, out var payload)); + } + + [Fact] + public void TryReadingMultipleMessages() + { + var message = new ReadOnlyBuffer(Encoding.UTF8.GetBytes("ABC\u001eXYZ\u001e")); + Assert.True(TextMessageParser.TryParseMessage(ref message, out var payload)); + Assert.Equal("ABC", Encoding.UTF8.GetString(payload.ToArray())); + Assert.True(TextMessageParser.TryParseMessage(ref message, out payload)); + Assert.Equal("XYZ", Encoding.UTF8.GetString(payload.ToArray())); + } + + [Fact] + public void IncompleteTrailingMessage() + { + var message = new ReadOnlyBuffer(Encoding.UTF8.GetBytes("ABC\u001eXYZ\u001e123")); + Assert.True(TextMessageParser.TryParseMessage(ref message, out var payload)); + Assert.Equal("ABC", Encoding.UTF8.GetString(payload.ToArray())); + Assert.True(TextMessageParser.TryParseMessage(ref message, out payload)); + Assert.Equal("XYZ", Encoding.UTF8.GetString(payload.ToArray())); + Assert.False(TextMessageParser.TryParseMessage(ref message, out payload)); + } + } +} diff --git a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/JsonHubProtocolTests.cs b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/JsonHubProtocolTests.cs index e72e7b3693..cc652e1d42 100644 --- a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/JsonHubProtocolTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/JsonHubProtocolTests.cs @@ -5,8 +5,8 @@ using System; using System.Collections.Generic; using System.IO; using System.Text; +using Microsoft.AspNetCore.SignalR.Internal.Formatters; using Microsoft.AspNetCore.SignalR.Internal.Protocol; -using Microsoft.AspNetCore.Sockets.Internal.Formatters; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using Xunit; diff --git a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/NegotiationProtocolTests.cs b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/NegotiationProtocolTests.cs index 23d38dc014..bff7c60462 100644 --- a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/NegotiationProtocolTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/NegotiationProtocolTests.cs @@ -26,12 +26,12 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol } [Theory] - [InlineData("2:", "Unable to parse payload as a negotiation message.")] - [InlineData("2:42;", "Unexpected JSON Token Type 'Integer'. Expected a JSON Object.")] - [InlineData("4:\"42\";", "Unexpected JSON Token Type 'String'. Expected a JSON Object.")] - [InlineData("4:null;", "Unexpected JSON Token Type 'Null'. Expected a JSON Object.")] - [InlineData("2:{};", "Missing required property 'protocol'.")] - [InlineData("2:[];", "Unexpected JSON Token Type 'Array'. Expected a JSON Object.")] + [InlineData("", "Unable to parse payload as a negotiation message.")] + [InlineData("42\u001e", "Unexpected JSON Token Type 'Integer'. Expected a JSON Object.")] + [InlineData("\"42\"\u001e", "Unexpected JSON Token Type 'String'. Expected a JSON Object.")] + [InlineData("null\u001e", "Unexpected JSON Token Type 'Null'. Expected a JSON Object.")] + [InlineData("{}\u001e", "Missing required property 'protocol'.")] + [InlineData("[]\u001e", "Unexpected JSON Token Type 'Array'. Expected a JSON Object.")] public void ParsingNegotiationMessageThrowsForInvalidMessages(string payload, string expectedMessage) { var message = Encoding.UTF8.GetBytes(payload); diff --git a/test/Microsoft.AspNetCore.SignalR.Microbenchmarks/MessageParserBenchmark.cs b/test/Microsoft.AspNetCore.SignalR.Microbenchmarks/MessageParserBenchmark.cs index adcad5c9ad..df293dc045 100644 --- a/test/Microsoft.AspNetCore.SignalR.Microbenchmarks/MessageParserBenchmark.cs +++ b/test/Microsoft.AspNetCore.SignalR.Microbenchmarks/MessageParserBenchmark.cs @@ -1,7 +1,7 @@ using System; using System.IO; using BenchmarkDotNet.Attributes; -using Microsoft.AspNetCore.Sockets.Internal.Formatters; +using Microsoft.AspNetCore.SignalR.Internal.Formatters; namespace Microsoft.AspNetCore.SignalR.Microbenchmarks {