Changing length prefixing to separator for JSON C#

This commit is contained in:
Pawel Kadluczka 2017-08-15 13:32:20 -07:00 committed by Pawel Kadluczka
parent b12451025f
commit 5ad5f36f88
22 changed files with 354 additions and 231 deletions

View File

@ -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<byte>(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();
}
}

View File

@ -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;
/// <summary>
/// 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.
/// </summary>
public static bool TryParseMessage(ref ReadOnlyBuffer<byte> buffer, out ReadOnlyBuffer<byte> payload)
{
payload = default(ReadOnlyBuffer<byte>);
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<byte> 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<byte> 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<byte> 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;
}
}
}

View File

@ -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<byte> 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<byte>.Shared.Rent(Int32OverflowLength);
var encodedLength = Encoding.UTF8.GetBytes(lengthString, 0, lengthString.Length, buffer, 0);
output.Write(buffer, 0, encodedLength);
ArrayPool<byte>.Shared.Return(buffer);
// Write the field delimiter ':'
output.WriteByte((byte)FieldDelimiter);
buffer = ArrayPool<byte>.Shared.Rent(payload.Length);
payload.CopyTo(buffer);
output.Write(buffer, 0, payload.Length);
ArrayPool<byte>.Shared.Return(buffer);
// Terminator
output.WriteByte((byte)MessageDelimiter);
}
}
}

View File

@ -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
{

View File

@ -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
{

View File

@ -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<byte> 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<byte>.Shared.Rent(Int32OverflowLength);
var encodedLength = Encoding.UTF8.GetBytes(lengthString, 0, lengthString.Length, buffer, 0);
output.Write(buffer, 0, encodedLength);
ArrayPool<byte>.Shared.Return(buffer);
// Write the field delimiter ':'
output.WriteByte((byte)FieldDelimiter);
buffer = ArrayPool<byte>.Shared.Rent(payload.Length);
var buffer = ArrayPool<byte>.Shared.Rent(payload.Length);
payload.CopyTo(buffer);
output.Write(buffer, 0, payload.Length);
ArrayPool<byte>.Shared.Return(buffer);
// Terminator
output.WriteByte((byte)MessageDelimiter);
output.WriteByte(RecordSeparator);
}
}
}

View File

@ -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;
/// <summary>
/// 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.
/// </summary>
public static bool TryParseMessage(ref ReadOnlyBuffer<byte> buffer, out ReadOnlyBuffer<byte> payload)
{
payload = default(ReadOnlyBuffer<byte>);
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<byte> 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<byte> 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<byte> 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;
}
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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();

View File

@ -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
{

View File

@ -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

View File

@ -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()));
}

View File

@ -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<byte> 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<byte> buffer = Encoding.UTF8.GetBytes(encoded);
var messages = new List<byte[]>();
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<byte> 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<byte> buffer = Encoding.UTF8.GetBytes(encoded);
var ex = Assert.Throws<FormatException>(() =>
{
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<byte> buffer = new byte[] { 0x48, 0x65, 0x80, 0x6C, 0x6F, (byte)':' };
var ex = Assert.Throws<FormatException>(() =>
{
TextMessageParser.TryParseMessage(ref buffer, out _);
LengthPrefixedTextMessageParser.TryParseMessage(ref buffer, out _);
});
Assert.Equal("Invalid length: 'He<48>lo'", ex.Message);
}

View File

@ -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

View File

@ -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
{

View File

@ -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<byte>(Encoding.UTF8.GetBytes("ABC")), ms);
Assert.Equal("ABC\u001e", Encoding.UTF8.GetString(ms.ToArray()));
}
}
}
}

View File

@ -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<byte>(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<byte>(Encoding.UTF8.GetBytes("ABC"));
Assert.False(TextMessageParser.TryParseMessage(ref message, out var payload));
}
[Fact]
public void TryReadingMultipleMessages()
{
var message = new ReadOnlyBuffer<byte>(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<byte>(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));
}
}
}

View File

@ -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;

View File

@ -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);

View File

@ -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
{