Replacing 8-byte-long length prefix with varint

This commit is contained in:
Pawel Kadluczka 2017-09-22 17:51:25 -07:00
parent e17cdae046
commit 4f4fb174ea
9 changed files with 292 additions and 122 deletions

View File

@ -29,10 +29,11 @@ describe("Text Message Formatter", () => {
describe("Binary Message Formatter", () => { describe("Binary Message Formatter", () => {
([ ([
[[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], <Uint8Array[]>[ new Uint8Array([])]], [[], <Uint8Array[]>[]],
[[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff], <Uint8Array[]>[ new Uint8Array([0xff])]], [[0x00], <Uint8Array[]>[ new Uint8Array([])]],
[[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, [[0x01, 0xff], <Uint8Array[]>[ new Uint8Array([0xff])]],
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x7f], <Uint8Array[]>[ new Uint8Array([0xff]), new Uint8Array([0x7f])]], [[0x01, 0xff,
0x01, 0x7f], <Uint8Array[]>[ new Uint8Array([0xff]), new Uint8Array([0x7f])]],
] as [[number[], Uint8Array[]]]).forEach(([payload, expected_messages]) => { ] as [[number[], Uint8Array[]]]).forEach(([payload, expected_messages]) => {
it(`should parse '${payload}' correctly`, () => { it(`should parse '${payload}' correctly`, () => {
let messages = BinaryMessageFormat.parse(new Uint8Array(payload).buffer); let messages = BinaryMessageFormat.parse(new Uint8Array(payload).buffer);
@ -41,14 +42,15 @@ describe("Binary Message Formatter", () => {
}); });
([ ([
[[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], new Error("Cannot read message size")], [[0x80], new Error("Cannot read message size.")],
[[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x01, 0x80, 0x00], new Error("Cannot read message size")], [[0x02, 0x01, 0x80, 0x80], new Error("Cannot read message size.")],
[[0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], new Error("Messages bigger than 2147483647 bytes are not supported")], [[0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80], new Error("Cannot read message size.")], // the size of the second message is cut
[[0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], new Error("Messages bigger than 2147483647 bytes are not supported")], [[0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01], new Error("Incomplete message.")], // second message has only size
[[0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00], new Error("Messages bigger than 2147483647 bytes are not supported")], [[0xff, 0xff, 0xff, 0xff, 0xff], new Error("Messages bigger than 2GB are not supported.")],
[[0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00], new Error("Messages bigger than 2147483647 bytes are not supported")], [[0x80, 0x80, 0x80, 0x80, 0x08], new Error("Messages bigger than 2GB are not supported.")],
[[0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00], new Error("Messages bigger than 2147483647 bytes are not supported")], [[0x80, 0x80, 0x80, 0x80, 0x80], new Error("Messages bigger than 2GB are not supported.")],
[[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00], new Error("Incomplete message")], [[0x02, 0x00], new Error("Incomplete message.")],
[[0xff, 0xff, 0xff, 0xff, 0x07], new Error("Incomplete message.")]
] as [[number[], Error]]).forEach(([payload, expected_error]) => { ] as [[number[], Error]]).forEach(([payload, expected_error]) => {
it(`should fail to parse '${payload}'`, () => { it(`should fail to parse '${payload}'`, () => {
expect(() => BinaryMessageFormat.parse(new Uint8Array(payload).buffer)).toThrow(expected_error); expect(() => BinaryMessageFormat.parse(new Uint8Array(payload).buffer)).toThrow(expected_error);
@ -56,8 +58,8 @@ describe("Binary Message Formatter", () => {
}); });
([ ([
[[], [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]], [[], [0x00]],
[[0x20], [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x20]], [[0x20], [0x01, 0x20]],
] as [[number[], number[]]]).forEach(([input, expected_payload]) => { ] as [[number[], number[]]]).forEach(([input, expected_payload]) => {
it(`should write '${input}'`, () => { it(`should write '${input}'`, () => {
let actual = new Uint8Array(BinaryMessageFormat.write(new Uint8Array(input))); let actual = new Uint8Array(BinaryMessageFormat.write(new Uint8Array(input)));
@ -65,4 +67,18 @@ describe("Binary Message Formatter", () => {
expect(actual).toEqual(expected); expect(actual).toEqual(expected);
}) })
}); });
([0x0000, 0x0001, 0x007f, 0x0080, 0x3fff, 0x4000, 0xc0de] as number[]).forEach(size => {
it(`messages should be roundtrippable (message size: '${size}')`, () => {
const message = [];
for (let i = 0; i < size; i++) {
message.push(i & 0xff);
}
var payload = new Uint8Array(message);
expect(payload).toEqual(BinaryMessageFormat.parse(BinaryMessageFormat.write(payload))[0]);
})
});
}); });

View File

@ -20,24 +20,21 @@ describe("MessageHubProtocol", () => {
}); });
([ ([
[ [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0b, [ [ 0x0b, 0x94, 0x03, 0xa3, 0x61, 0x62, 0x63, 0x01, 0xa3, 0x45, 0x72, 0x72],
0x94, 0x03, 0xa3, 0x61, 0x62, 0x63, 0x01, 0xa3, 0x45, 0x72, 0x72],
{ {
type: MessageType.Completion, type: MessageType.Completion,
invocationId: "abc", invocationId: "abc",
error: "Err", error: "Err",
result: null result: null
} as CompletionMessage ], } as CompletionMessage ],
[ [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, [ [ 0x0a, 0x94, 0x03, 0xa3, 0x61, 0x62, 0x63, 0x03, 0xa2, 0x4f, 0x4b ],
0x94, 0x03, 0xa3, 0x61, 0x62, 0x63, 0x03, 0xa2, 0x4f, 0x4b ],
{ {
type: MessageType.Completion, type: MessageType.Completion,
invocationId: "abc", invocationId: "abc",
error: null, error: null,
result: "OK" result: "OK"
} as CompletionMessage ], } as CompletionMessage ],
[ [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, [ [ 0x07, 0x93, 0x03, 0xa3, 0x61, 0x62, 0x63, 0x02 ],
0x93, 0x03, 0xa3, 0x61, 0x62, 0x63, 0x02 ],
{ {
type: MessageType.Completion, type: MessageType.Completion,
invocationId: "abc", invocationId: "abc",
@ -51,8 +48,7 @@ describe("MessageHubProtocol", () => {
})); }));
([ ([
[ [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, [ [ 0x07, 0x93, 0x02, 0xa3, 0x61, 0x62, 0x63, 0x08 ],
0x93, 0x02, 0xa3, 0x61, 0x62, 0x63, 0x08 ],
{ {
type: MessageType.Result, type: MessageType.Result,
invocationId: "abc", invocationId: "abc",
@ -65,17 +61,17 @@ describe("MessageHubProtocol", () => {
})); }));
([ ([
[ [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ], new Error("Invalid payload.") ], [ [ 0x00 ], new Error("Invalid payload.") ],
[ [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x90 ], new Error("Invalid payload.") ], [ [ 0x01, 0x90 ], new Error("Invalid payload.") ],
[ [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xc2 ], new Error("Invalid payload.") ], [ [ 0x01, 0xc2 ], new Error("Invalid payload.") ],
[ [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x91, 0x05 ], new Error("Invalid message type.") ], [ [ 0x02, 0x91, 0x05 ], new Error("Invalid message type.") ],
[ [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x91, 0xa1, 0x78 ], new Error("Invalid message type.") ], [ [ 0x03, 0x91, 0xa1, 0x78 ], new Error("Invalid message type.") ],
[ [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x91, 0x01 ], new Error("Invalid payload for Invocation message.") ], [ [ 0x02, 0x91, 0x01 ], new Error("Invalid payload for Invocation message.") ],
[ [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x91, 0x02 ], new Error("Invalid payload for stream Result message.") ], [ [ 0x02, 0x91, 0x02 ], new Error("Invalid payload for stream Result message.") ],
[ [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x92, 0x03, 0xa0 ], new Error("Invalid payload for Completion message.") ], [ [ 0x03, 0x92, 0x03, 0xa0 ], new Error("Invalid payload for Completion message.") ],
[ [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x94, 0x03, 0xa0, 0x02, 0x00 ], new Error("Invalid payload for Completion message.") ], [ [ 0x05, 0x94, 0x03, 0xa0, 0x02, 0x00 ], new Error("Invalid payload for Completion message.") ],
[ [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x93, 0x03, 0xa0, 0x01 ], new Error("Invalid payload for Completion message.") ], [ [ 0x04, 0x93, 0x03, 0xa0, 0x01 ], new Error("Invalid payload for Completion message.") ],
[ [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x93, 0x03, 0xa0, 0x03 ], new Error("Invalid payload for Completion message.") ] [ [ 0x04, 0x93, 0x03, 0xa0, 0x03 ], new Error("Invalid payload for Completion message.") ]
] as [[number[], Error]]).forEach(([payload, expected_error]) => ] as [[number[], Error]]).forEach(([payload, expected_error]) =>
it("throws for invalid messages", () => { it("throws for invalid messages", () => {
expect(() => new MessagePackHubProtocol().parseMessages(new Uint8Array(payload).buffer)) expect(() => new MessagePackHubProtocol().parseMessages(new Uint8Array(payload).buffer))
@ -84,10 +80,8 @@ describe("MessageHubProtocol", () => {
it("can read multiple messages", () => { it("can read multiple messages", () => {
let payload = [ let payload = [
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x07, 0x93, 0x02, 0xa3, 0x61, 0x62, 0x63, 0x08,
0x93, 0x02, 0xa3, 0x61, 0x62, 0x63, 0x08, 0x0a, 0x94, 0x03, 0xa3, 0x61, 0x62, 0x63, 0x03, 0xa2, 0x4f, 0x4b ];
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a,
0x94, 0x03, 0xa3, 0x61, 0x62, 0x63, 0x03, 0xa2, 0x4f, 0x4b ];
let messages = new MessagePackHubProtocol().parseMessages(new Uint8Array(payload).buffer); let messages = new MessagePackHubProtocol().parseMessages(new Uint8Array(payload).buffer);
expect(messages).toEqual([ expect(messages).toEqual([
{ {

View File

@ -21,55 +21,73 @@ export namespace TextMessageFormat {
} }
export namespace BinaryMessageFormat { export namespace BinaryMessageFormat {
// The length prefix of binary messages is encoded as VarInt. Read the comment in
// the BinaryMessageParser.TryParseMessage for details.
export function write(output: Uint8Array): ArrayBuffer { export function write(output: Uint8Array): ArrayBuffer {
// .byteLength does is undefined in IE10 // msgpack5 uses returns Buffer instead of Uint8Array on IE10 and some other browser
// in which case .byteLength does will be undefined
let size = output.byteLength || output.length; let size = output.byteLength || output.length;
let buffer = new Uint8Array(size + 8); let lenBuffer = [];
do
// javascript bitwise operators only support 32-bit integers {
for (let i = 7; i >= 4; i--) { let sizePart = size & 0x7f;
buffer[i] = size & 0xff; size = size >> 7;
size = size >> 8; if (size > 0) {
sizePart |= 0x80;
}
lenBuffer.push(sizePart);
} }
while (size > 0);
buffer.set(output, 8); // msgpack5 uses returns Buffer instead of Uint8Array on IE10 and some other browser
// in which case .byteLength does will be undefined
size = output.byteLength || output.length;
let buffer = new Uint8Array(lenBuffer.length + size);
buffer.set(lenBuffer, 0);
buffer.set(output, lenBuffer.length);
return buffer.buffer; return buffer.buffer;
} }
export function parse(input: ArrayBuffer): Uint8Array[] { export function parse(input: ArrayBuffer): Uint8Array[] {
let result: Uint8Array[] = []; let result: Uint8Array[] = [];
let uint8Array = new Uint8Array(input); let uint8Array = new Uint8Array(input);
// 8 - the length prefix size const maxLengthPrefixSize = 5;
const numBitsToShift = [0, 7, 14, 21, 28 ];
for (let offset = 0; offset < input.byteLength;) { for (let offset = 0; offset < input.byteLength;) {
let numBytes = 0;
if (input.byteLength < offset + 8) {
throw new Error("Cannot read message size")
}
// Note javascript bitwise operators only support 32-bit integers - for now cutting bigger messages.
// Tracking bug https://github.com/aspnet/SignalR/issues/613
if (!(uint8Array[offset] == 0 && uint8Array[offset + 1] == 0 && uint8Array[offset + 2] == 0
&& uint8Array[offset + 3] == 0 && (uint8Array[offset + 4] & 0x80) == 0)) {
throw new Error("Messages bigger than 2147483647 bytes are not supported");
}
let size = 0; let size = 0;
for (let i = 4; i < 8; i++) { let byteRead;
size = (size << 8) | uint8Array[offset + i]; do
{
byteRead = uint8Array[offset + numBytes];
size = size | ((byteRead & 0x7f) << (numBitsToShift[numBytes]));
numBytes++;
}
while (numBytes < Math.min(maxLengthPrefixSize, input.byteLength - offset) && (byteRead & 0x80) != 0);
if ((byteRead & 0x80) !== 0 && numBytes < maxLengthPrefixSize) {
throw new Error("Cannot read message size.");
} }
if (uint8Array.byteLength >= (offset + 8 + size)) { if (numBytes === maxLengthPrefixSize && byteRead > 7) {
throw new Error("Messages bigger than 2GB are not supported.");
}
if (uint8Array.byteLength >= (offset + numBytes + size)) {
// IE does not support .slice() so use subarray // IE does not support .slice() so use subarray
result.push(uint8Array.slice result.push(uint8Array.slice
? uint8Array.slice(offset + 8, offset + 8 + size) ? uint8Array.slice(offset + numBytes, offset + numBytes + size)
: uint8Array.subarray(offset + 8, offset + 8 + size)); : uint8Array.subarray(offset + numBytes, offset + numBytes + size));
} }
else { else {
throw new Error("Incomplete message"); throw new Error("Incomplete message.");
} }
offset = offset + 8 + size; offset = offset + numBytes + size;
} }
return result; return result;

View File

@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Binary;
using System.Buffers; using System.Buffers;
using System.IO; using System.IO;
@ -10,18 +9,35 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Formatters
{ {
public static class BinaryMessageFormatter public static class BinaryMessageFormatter
{ {
public static void WriteMessage(ReadOnlySpan<byte> payload, Stream output) public unsafe static void WriteMessage(ReadOnlySpan<byte> payload, Stream output)
{ {
// TODO: Optimize for size - (e.g. use Varints) // This code writes length prefix of the message as a VarInt. Read the comment in
var length = sizeof(long); // the BinaryMessageParser.TryParseMessage for details.
var buffer = ArrayPool<byte>.Shared.Rent(length);
BufferWriter.WriteBigEndian<long>(buffer, payload.Length); var lenBuffer = stackalloc byte[5];
output.Write(buffer, 0, length); var lenNumBytes = 0;
ArrayPool<byte>.Shared.Return(buffer); var length = payload.Length;
do
{
ref var current = ref lenBuffer[lenNumBytes];
current = (byte)(length & 0x7f);
length >>= 7;
if (length > 0)
{
current |= 0x80;
}
lenNumBytes++;
}
while (length > 0);
var buffer = ArrayPool<byte>.Shared.Rent(lenNumBytes + payload.Length);
var bufferSpan = buffer.AsSpan();
new Span<byte>(lenBuffer, lenNumBytes).CopyTo(bufferSpan);
bufferSpan = bufferSpan.Slice(lenNumBytes);
payload.CopyTo(bufferSpan);
output.Write(buffer, 0, lenNumBytes + payload.Length);
buffer = ArrayPool<byte>.Shared.Rent(payload.Length);
payload.CopyTo(buffer);
output.Write(buffer, 0, payload.Length);
ArrayPool<byte>.Shared.Return(buffer); ArrayPool<byte>.Shared.Return(buffer);
} }
} }

View File

@ -2,44 +2,70 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Binary;
namespace Microsoft.AspNetCore.SignalR.Internal.Formatters namespace Microsoft.AspNetCore.SignalR.Internal.Formatters
{ {
public static class BinaryMessageParser public static class BinaryMessageParser
{ {
private static int[] _numBitsToShift = new[] { 0, 7, 14, 21, 28 };
private const int MaxLengthPrefixSize = 5;
public static bool TryParseMessage(ref ReadOnlyBuffer<byte> buffer, out ReadOnlyBuffer<byte> payload) public static bool TryParseMessage(ref ReadOnlyBuffer<byte> buffer, out ReadOnlyBuffer<byte> payload)
{ {
long length = 0; payload = default;
payload = default(ReadOnlyBuffer<byte>);
if (buffer.Length < sizeof(long)) if (buffer.IsEmpty)
{ {
return false; return false;
} }
// Read the length // The payload starts with a length prefix encoded as a VarInt. VarInts use the most significant bit
length = buffer.Span.Slice(0, sizeof(long)).ReadBigEndian<long>(); // as a marker whether the byte is the last byte of the VarInt or if it spans to the next byte. Bytes
// appear in the reverse order - i.e. the first byte contains the least significant bits of the value
// Examples:
// VarInt: 0x35 - %00110101 - the most significant bit is 0 so the value is %x0110101 i.e. 0x35 (53)
// VarInt: 0x80 0x25 - %10000000 %00101001 - the most significant bit of the first byte is 1 so the
// remaining bits (%x0000000) are the lowest bits of the value. The most significant bit of the second
// byte is 0 meaning this is last byte of the VarInt. The actual value bits (%x0101001) need to be
// prepended to the bits we already read so the values is %01010010000000 i.e. 0x1480 (5248)
// We support paylads up to 2GB so the biggest number we support is 7fffffff which when encoded as
// VarInt is 0xFF 0xFF 0xFF 0xFF 0x7F - hence the maximum length prefix is 5 bytes.
if (length > Int32.MaxValue) var length = 0U;
var numBytes = 0;
var lengthPrefixBuffer = buffer.Span.Slice(0, Math.Min(MaxLengthPrefixSize, buffer.Length));
byte byteRead;
do
{ {
throw new FormatException("Messages over 2GB in size are not supported"); byteRead = lengthPrefixBuffer[numBytes];
length = length | (((uint)(byteRead & 0x7f)) << _numBitsToShift[numBytes]);
numBytes++;
}
while (numBytes < lengthPrefixBuffer.Length && ((byteRead & 0x80) != 0));
// size bytes are missing
if ((byteRead & 0x80) != 0 && (numBytes < MaxLengthPrefixSize))
{
return false;
} }
// Skip over the length if ((byteRead & 0x80) != 0 || (numBytes == MaxLengthPrefixSize && byteRead > 7))
var remaining = buffer.Slice(sizeof(long)); {
throw new FormatException("Messages over 2GB in size are not supported.");
}
// We don't have enough data // We don't have enough data
while (remaining.Length < (int)length) if (buffer.Length < length + numBytes)
{ {
return false; return false;
} }
// Get the payload // Get the payload
payload = remaining.Slice(0, (int)length); payload = buffer.Slice(numBytes, (int)length);
// Skip the payload // Skip the payload
buffer = remaining.Slice((int)length); buffer = buffer.Slice(numBytes + (int)length);
return true; return true;
} }
} }

View File

@ -4,6 +4,7 @@
<Description>Common serialiation primitives for SignalR Clients Servers</Description> <Description>Common serialiation primitives for SignalR Clients Servers</Description>
<TargetFramework>netstandard2.0</TargetFramework> <TargetFramework>netstandard2.0</TargetFramework>
<RootNamespace>Microsoft.AspNetCore.SignalR</RootNamespace> <RootNamespace>Microsoft.AspNetCore.SignalR</RootNamespace>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -1,6 +1,8 @@
// 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
@ -16,9 +18,9 @@ namespace Microsoft.AspNetCore.Sockets.Tests.Internal.Formatters
{ {
var expectedEncoding = new byte[] var expectedEncoding = new byte[]
{ {
/* length: */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* length: */ 0x00,
/* body: <empty> */ /* body: <empty> */
/* length: */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, /* length: */ 0x0E,
/* body: */ 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x2C, 0x0D, 0x0A, 0x57, 0x6F, 0x72, 0x6C, 0x64, 0x21, /* body: */ 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x2C, 0x0D, 0x0A, 0x57, 0x6F, 0x72, 0x6C, 0x64, 0x21,
}; };
@ -38,10 +40,34 @@ namespace Microsoft.AspNetCore.Sockets.Tests.Internal.Formatters
} }
[Theory] [Theory]
[InlineData(0, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, new byte[0])] [InlineData(0, new byte[] { 0x00 }, new byte[0])]
[InlineData(0, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xAB, 0xCD, 0xEF, 0x12 }, new byte[] { 0xAB, 0xCD, 0xEF, 0x12 })] [InlineData(0, new byte[] { 0x04, 0xAB, 0xCD, 0xEF, 0x12 }, new byte[] { 0xAB, 0xCD, 0xEF, 0x12 })]
[InlineData(4, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, new byte[0])] [InlineData(0, new byte[]
[InlineData(4, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xAB, 0xCD, 0xEF, 0x12 }, new byte[] { 0xAB, 0xCD, 0xEF, 0x12 })] {
0x80, 0x01, // Size - 128
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f,
0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f,
0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f,
0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f
},
new byte[]
{
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f,
0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f,
0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f,
0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f
})]
[InlineData(4, new byte[] { 0x00 }, new byte[0])]
[InlineData(4, new byte[] { 0x04, 0xAB, 0xCD, 0xEF, 0x12 }, new byte[] { 0xAB, 0xCD, 0xEF, 0x12 })]
public void WriteBinaryMessage(int offset, byte[] encoded, byte[] payload) public void WriteBinaryMessage(int offset, byte[] encoded, byte[] payload)
{ {
var output = new MemoryStream(); var output = new MemoryStream();
@ -57,10 +83,10 @@ namespace Microsoft.AspNetCore.Sockets.Tests.Internal.Formatters
} }
[Theory] [Theory]
[InlineData(0, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, "")] [InlineData(0, new byte[] { 0x00 }, "")]
[InlineData(0, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x41, 0x42, 0x43 }, "ABC")] [InlineData(0, new byte[] { 0x03, 0x41, 0x42, 0x43 }, "ABC")]
[InlineData(0, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0B, 0x41, 0x0A, 0x52, 0x0D, 0x43, 0x0D, 0x0A, 0x3B, 0x44, 0x45, 0x46 }, "A\nR\rC\r\n;DEF")] [InlineData(0, new byte[] { 0x0B, 0x41, 0x0A, 0x52, 0x0D, 0x43, 0x0D, 0x0A, 0x3B, 0x44, 0x45, 0x46 }, "A\nR\rC\r\n;DEF")]
[InlineData(4, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, "")] [InlineData(4, new byte[] { 0x00 }, "")]
public void WriteTextMessage(int offset, byte[] encoded, string payload) public void WriteTextMessage(int offset, byte[] encoded, string payload)
{ {
var message = Encoding.UTF8.GetBytes(payload); var message = Encoding.UTF8.GetBytes(payload);
@ -75,5 +101,36 @@ namespace Microsoft.AspNetCore.Sockets.Tests.Internal.Formatters
Assert.Equal(encoded, output.ToArray().Skip(offset)); Assert.Equal(encoded, output.ToArray().Skip(offset));
} }
[Theory]
[MemberData(nameof(RandomPayloads))]
public void RoundTrippingTest(byte[] payload)
{
using (var ms = new MemoryStream())
{
BinaryMessageFormatter.WriteMessage(payload, ms);
var buffer = new ReadOnlyBuffer<byte>(ms.ToArray());
Assert.True(BinaryMessageParser.TryParseMessage(ref buffer, out var roundtripped));
Assert.Equal(payload, roundtripped.ToArray());
}
}
public static IEnumerable<object[]> RandomPayloads()
{
// boundaries
yield return new[] { CreatePayload(0) };
yield return new[] { CreatePayload(1) };
yield return new[] { CreatePayload(0x7f) };
yield return new[] { CreatePayload(0x80) };
yield return new[] { CreatePayload(0x3fff) };
yield return new[] { CreatePayload(0x4000) };
// random
yield return new[] { CreatePayload(0xc0de) };
}
private static byte[] CreatePayload(int size) =>
Enumerable.Range(0, size).Select(n => (byte)(n & 0xff)).ToArray();
} }
} }

View File

@ -12,9 +12,9 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Formatters
public class BinaryMessageParserTests public class BinaryMessageParserTests
{ {
[Theory] [Theory]
[InlineData(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, "")] [InlineData(new byte[] { 0x00 }, "")]
[InlineData(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x41, 0x42, 0x43 }, "ABC")] [InlineData(new byte[] { 0x03, 0x41, 0x42, 0x43 }, "ABC")]
[InlineData(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0B, 0x41, 0x0A, 0x52, 0x0D, 0x43, 0x0D, 0x0A, 0x3B, 0x44, 0x45, 0x46 }, "A\nR\rC\r\n;DEF")] [InlineData(new byte[] { 0x0B, 0x41, 0x0A, 0x52, 0x0D, 0x43, 0x0D, 0x0A, 0x3B, 0x44, 0x45, 0x46 }, "A\nR\rC\r\n;DEF")]
public void ReadMessage(byte[] encoded, string payload) public void ReadMessage(byte[] encoded, string payload)
{ {
ReadOnlyBuffer<byte> span = encoded; ReadOnlyBuffer<byte> span = encoded;
@ -25,8 +25,31 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Formatters
} }
[Theory] [Theory]
[InlineData(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, new byte[0])] [InlineData(new byte[] { 0x00 }, new byte[0])]
[InlineData(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xAB, 0xCD, 0xEF, 0x12 }, new byte[] { 0xAB, 0xCD, 0xEF, 0x12 })] [InlineData(new byte[] { 0x04, 0xAB, 0xCD, 0xEF, 0x12 }, new byte[] { 0xAB, 0xCD, 0xEF, 0x12 })]
[InlineData(new byte[]
{
0x80, 0x01, // Size - 128
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f,
0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f,
0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f,
0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f
},
new byte[]
{
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f,
0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f,
0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f,
0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f
})]
public void ReadBinaryMessage(byte[] encoded, byte[] payload) public void ReadBinaryMessage(byte[] encoded, byte[] payload)
{ {
ReadOnlyBuffer<byte> span = encoded; ReadOnlyBuffer<byte> span = encoded;
@ -35,14 +58,36 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Formatters
Assert.Equal(payload, message.ToArray()); Assert.Equal(payload, message.ToArray());
} }
[Theory]
[InlineData(new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF })]
[InlineData(new byte[] { 0x80, 0x80, 0x80, 0x80, 0x08 })] // 2GB + 1
[InlineData(new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF })]
public void BinaryMessageParserThrowsForMessagesOver2GB(byte[] payload)
{
var buffer = new ReadOnlyBuffer<byte>(payload);
var ex = Assert.Throws<FormatException>(() => BinaryMessageParser.TryParseMessage(ref buffer, out var message));
Assert.Equal("Messages over 2GB in size are not supported.", ex.Message);
}
[Theory]
[InlineData(new byte[] { })]
[InlineData(new byte[] { 0x04, 0xAB, 0xCD, 0xEF })]
[InlineData(new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, 0x07 })] // 2GB
[InlineData(new byte[] { 0x80 })] // size is cut
public void BinaryMessageParserReturnsFalseForPartialPayloads(byte[] payload)
{
var buffer = new ReadOnlyBuffer<byte>(payload);
Assert.False(BinaryMessageParser.TryParseMessage(ref buffer, out var message));
}
[Fact] [Fact]
public void ReadMultipleMessages() public void ReadMultipleMessages()
{ {
var encoded = new byte[] var encoded = new byte[]
{ {
/* length: */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* length: */ 0x00,
/* body: <empty> */ /* body: <empty> */
/* length: */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, /* length: */ 0x0E,
/* body: */ 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x2C, 0x0D, 0x0A, 0x57, 0x6F, 0x72, 0x6C, 0x64, 0x21, /* body: */ 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x2C, 0x0D, 0x0A, 0x57, 0x6F, 0x72, 0x6C, 0x64, 0x21,
}; };
ReadOnlyBuffer<byte> buffer = encoded; ReadOnlyBuffer<byte> buffer = encoded;
@ -62,7 +107,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Formatters
[Theory] [Theory]
[InlineData(new byte[0])] // Empty [InlineData(new byte[0])] // Empty
[InlineData(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00 })] // Not enough data for payload [InlineData(new byte[] { 0x09, 0x00, 0x00 })] // Not enough data for payload
public void ReadIncompleteMessages(byte[] encoded) public void ReadIncompleteMessages(byte[] encoded)
{ {
ReadOnlyBuffer<byte> buffer = encoded; ReadOnlyBuffer<byte> buffer = encoded;

View File

@ -117,12 +117,12 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
public void ParserThrowsForInvalidMessages(byte[] payload, string expectedExceptionMessage) public void ParserThrowsForInvalidMessages(byte[] payload, string expectedExceptionMessage)
{ {
var payloadSize = payload.Length; var payloadSize = payload.Length;
Debug.Assert(payloadSize <= 0xff, "This test does not support payloads larger than 255"); Debug.Assert(payloadSize <= 0x7f, "This test does not support payloads larger than 127 bytes");
// prefix payload with the size // prefix payload with the size
var buffer = new byte[8 + payloadSize]; var buffer = new byte[1 + payloadSize];
buffer[7] = (byte)(payloadSize & 0xff); buffer[0] = (byte)(payloadSize & 0x7f);
Array.Copy(payload, 0, buffer, 8, payloadSize); Array.Copy(payload, 0, buffer, 1, payloadSize);
var binder = new TestBinder(new[] { typeof(string) }, typeof(string)); var binder = new TestBinder(new[] { typeof(string) }, typeof(string));
var exception = Assert.Throws<FormatException>(() => _hubProtocol.TryParseMessages(buffer, binder, out var messages)); var exception = Assert.Throws<FormatException>(() => _hubProtocol.TryParseMessages(buffer, binder, out var messages));
@ -131,13 +131,13 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
} }
[Theory] [Theory]
[InlineData(new object[] { new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x01 }, 0 })] [InlineData(new object[] { new byte[] { 0x05, 0x01 }, 0 })]
[InlineData(new object[] { [InlineData(new object[] {
new byte[] new byte[]
{ {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x93, 0x03, 0xa1, 0x78, 0x02, 0x05, 0x93, 0x03, 0xa1, 0x78, 0x02,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x93, 0x03, 0xa1, 0x78, 0x02, 0x05, 0x93, 0x03, 0xa1, 0x78, 0x02,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x93, 0x03, 0xa1 0x05, 0x93, 0x03, 0xa1
}, 2 })] }, 2 })]
public void ParserDoesNotConsumePartialData(byte[] payload, int expectedMessagesCount) public void ParserDoesNotConsumePartialData(byte[] payload, int expectedMessagesCount)
{ {
@ -154,7 +154,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
new InvocationMessage("0", false, "A", 1, new CustomObject()), new InvocationMessage("0", false, "A", 1, new CustomObject()),
new byte[] new byte[]
{ {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5b, 0x95, 0x01, 0xa1, 0x30, 0xc2, 0xa1, 0x41, 0x5b, 0x95, 0x01, 0xa1, 0x30, 0xc2, 0xa1, 0x41,
0x92, // argument array 0x92, // argument array
0x01, // 1 - first argument 0x01, // 1 - first argument
// 0x85 - a map of 5 items (properties) // 0x85 - a map of 5 items (properties)
@ -171,7 +171,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
CompletionMessage.WithResult("0", new CustomObject()), CompletionMessage.WithResult("0", new CustomObject()),
new byte[] new byte[]
{ {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x57, 0x94, 0x03, 0xa1, 0x30, 0x03, 0x57, 0x94, 0x03, 0xa1, 0x30, 0x03,
// 0x85 - a map of 5 items (properties) // 0x85 - a map of 5 items (properties)
0x85, 0xac, 0x44, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x50, 0x72, 0x6f, 0x70, 0xd3, 0x08, 0x85, 0xac, 0x44, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x50, 0x72, 0x6f, 0x70, 0xd3, 0x08,
0xd4, 0x80, 0x6d, 0xb2, 0x76, 0xc0, 0x00, 0xaa, 0x44, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x50, 0x72, 0xd4, 0x80, 0x6d, 0xb2, 0x76, 0xc0, 0x00, 0xaa, 0x44, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x50, 0x72,
@ -186,7 +186,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
new StreamItemMessage("0", new CustomObject()), new StreamItemMessage("0", new CustomObject()),
new byte[] new byte[]
{ {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x56, 0x93, 0x02, 0xa1, 0x30, 0x56, 0x93, 0x02, 0xa1, 0x30,
// 0x85 - a map of 5 items (properties) // 0x85 - a map of 5 items (properties)
0x85, 0xac, 0x44, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x50, 0x72, 0x6f, 0x70, 0xd3, 0x08, 0x85, 0xac, 0x44, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x50, 0x72, 0x6f, 0x70, 0xd3, 0x08,
0xd4, 0x80, 0x6d, 0xb2, 0x76, 0xc0, 0x00, 0xaa, 0x44, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x50, 0x72, 0xd4, 0x80, 0x6d, 0xb2, 0x76, 0xc0, 0x00, 0xaa, 0x44, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x50, 0x72,
@ -212,10 +212,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
[Fact] [Fact]
public void CanWriteObjectsWithoutDefaultCtors() public void CanWriteObjectsWithoutDefaultCtors()
{ {
var expectedPayload = new byte[] var expectedPayload = new byte[] { 0x07, 0x94, 0x03, 0xa1, 0x30, 0x03, 0x91, 0x2a };
{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x94, 0x03, 0xa1, 0x30, 0x03, 0x91, 0x2a
};
using (var memoryStream = new MemoryStream()) using (var memoryStream = new MemoryStream())
{ {