diff --git a/.gitmodules b/.gitmodules index 964aad14a70f..a64d4061292a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,7 @@ [submodule "googletest"] path = src/submodules/googletest url = https://github.com/google/googletest + +[submodule "src/submodules/MessagePack-CSharp"] + path = src/submodules/MessagePack-CSharp + url = https://github.com/aspnet/MessagePack-CSharp.git diff --git a/src/Components/Browser.JS/src/Boot.Server.ts b/src/Components/Browser.JS/src/Boot.Server.ts index 8e531c69e2a8..799071d9c040 100644 --- a/src/Components/Browser.JS/src/Boot.Server.ts +++ b/src/Components/Browser.JS/src/Boot.Server.ts @@ -42,9 +42,11 @@ async function boot() { } async function initializeConnection(circuitHandlers: CircuitHandler[]): Promise { + const hubProtocol = new MessagePackHubProtocol(); + (hubProtocol as any).name = 'blazorpack'; const connection = new signalR.HubConnectionBuilder() .withUrl('_blazor') - .withHubProtocol(new MessagePackHubProtocol()) + .withHubProtocol(hubProtocol) .configureLogging(signalR.LogLevel.Information) .build(); diff --git a/src/Components/Server/src/BlazorPack/ArrayBufferWriter.cs b/src/Components/Server/src/BlazorPack/ArrayBufferWriter.cs new file mode 100644 index 000000000000..16a676cb0691 --- /dev/null +++ b/src/Components/Server/src/BlazorPack/ArrayBufferWriter.cs @@ -0,0 +1,193 @@ +// 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. + +// Copied from https://github.com/dotnet/corefx/blob/b0751dcd4a419ba6731dcaa7d240a8a1946c934c/src/System.Text.Json/src/System/Text/Json/Serialization/ArrayBufferWriter.cs + +using System; +using System.Buffers; +using System.Diagnostics; + +namespace Microsoft.AspNetCore.Components.Server.BlazorPack +{ + // Note: this is currently an internal class that will be replaced with a shared version. + internal sealed class ArrayBufferWriter : IBufferWriter, IDisposable + { + private T[] _rentedBuffer; + private int _index; + + private const int MinimumBufferSize = 256; + + public ArrayBufferWriter() + { + _rentedBuffer = ArrayPool.Shared.Rent(MinimumBufferSize); + _index = 0; + } + + public ArrayBufferWriter(int initialCapacity) + { + if (initialCapacity <= 0) + { + throw new ArgumentException(nameof(initialCapacity)); + } + + _rentedBuffer = ArrayPool.Shared.Rent(initialCapacity); + _index = 0; + } + + public ReadOnlyMemory WrittenMemory + { + get + { + CheckIfDisposed(); + + return _rentedBuffer.AsMemory(0, _index); + } + } + + public int WrittenCount + { + get + { + CheckIfDisposed(); + + return _index; + } + } + + public int Capacity + { + get + { + CheckIfDisposed(); + + return _rentedBuffer.Length; + } + } + + public int FreeCapacity + { + get + { + CheckIfDisposed(); + + return _rentedBuffer.Length - _index; + } + } + + public void Clear() + { + CheckIfDisposed(); + + ClearHelper(); + } + + private void ClearHelper() + { + Debug.Assert(_rentedBuffer != null); + + _rentedBuffer.AsSpan(0, _index).Clear(); + _index = 0; + } + + // Returns the rented buffer back to the pool + public void Dispose() + { + if (_rentedBuffer == null) + { + return; + } + + ClearHelper(); + ArrayPool.Shared.Return(_rentedBuffer); + _rentedBuffer = null; + } + + private void CheckIfDisposed() + { + if (_rentedBuffer == null) + { + ThrowObjectDisposedException(); + } + } + + private static void ThrowObjectDisposedException() + { + throw new ObjectDisposedException(nameof(ArrayBufferWriter)); + } + + public void Advance(int count) + { + CheckIfDisposed(); + + if (count < 0) + throw new ArgumentException(nameof(count)); + + if (_index > _rentedBuffer.Length - count) + { + ThrowInvalidOperationException(_rentedBuffer.Length); + } + + _index += count; + } + + public Memory GetMemory(int sizeHint = 0) + { + CheckIfDisposed(); + + CheckAndResizeBuffer(sizeHint); + return _rentedBuffer.AsMemory(_index); + } + + public Span GetSpan(int sizeHint = 0) + { + CheckIfDisposed(); + + CheckAndResizeBuffer(sizeHint); + return _rentedBuffer.AsSpan(_index); + } + + private void CheckAndResizeBuffer(int sizeHint) + { + Debug.Assert(_rentedBuffer != null); + + if (sizeHint < 0) + { + throw new ArgumentException(nameof(sizeHint)); + } + + if (sizeHint == 0) + { + sizeHint = MinimumBufferSize; + } + + var availableSpace = _rentedBuffer.Length - _index; + + if (sizeHint > availableSpace) + { + var growBy = Math.Max(sizeHint, _rentedBuffer.Length); + + var newSize = checked(_rentedBuffer.Length + growBy); + + var oldBuffer = _rentedBuffer; + + _rentedBuffer = ArrayPool.Shared.Rent(newSize); + + Debug.Assert(oldBuffer.Length >= _index); + Debug.Assert(_rentedBuffer.Length >= _index); + + var previousBuffer = oldBuffer.AsSpan(0, _index); + previousBuffer.CopyTo(_rentedBuffer); + previousBuffer.Clear(); + ArrayPool.Shared.Return(oldBuffer); + } + + Debug.Assert(_rentedBuffer.Length - _index > 0); + Debug.Assert(_rentedBuffer.Length - _index >= sizeHint); + } + + private static void ThrowInvalidOperationException(int capacity) + { + throw new InvalidOperationException($"Cannot advance past the end of the buffer, which has a size of {capacity}."); + } + } +} diff --git a/src/Components/Server/src/BlazorPack/BlazorPackHubProtocol.cs b/src/Components/Server/src/BlazorPack/BlazorPackHubProtocol.cs new file mode 100644 index 000000000000..3fe38ad022cd --- /dev/null +++ b/src/Components/Server/src/BlazorPack/BlazorPackHubProtocol.cs @@ -0,0 +1,638 @@ +// 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.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.ExceptionServices; +using MessagePack; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Internal; +using Microsoft.AspNetCore.SignalR; +using Microsoft.AspNetCore.SignalR.Protocol; + +namespace Microsoft.AspNetCore.Components.Server.BlazorPack +{ + /// + /// Implements the SignalR Hub Protocol using MessagePack with limited type support. + /// + internal sealed class BlazorPackHubProtocol : IHubProtocol + { + internal const string ProtocolName = "blazorpack"; + private const int ErrorResult = 1; + private const int VoidResult = 2; + private const int NonVoidResult = 3; + + private static readonly int ProtocolVersion = 1; + private static readonly int ProtocolMinorVersion = 0; + + /// + public string Name => ProtocolName; + + /// + public int Version => ProtocolVersion; + + /// + public int MinorVersion => ProtocolMinorVersion; + + /// + public TransferFormat TransferFormat => TransferFormat.Binary; + + /// + public bool IsVersionSupported(int version) + { + return version == Version; + } + + /// + public bool TryParseMessage(ref ReadOnlySequence input, IInvocationBinder binder, out HubMessage message) + { + if (!BinaryMessageParser.TryParseMessage(ref input, out var payload)) + { + message = null; + return false; + } + + var reader = new MessagePackReader(payload); + + var itemCount = reader.ReadArrayHeader(); + var messageType = ReadInt32(ref reader, "messageType"); + + switch (messageType) + { + case HubProtocolConstants.InvocationMessageType: + message = CreateInvocationMessage(ref reader, binder, itemCount); + return true; + case HubProtocolConstants.StreamInvocationMessageType: + message = CreateStreamInvocationMessage(ref reader, binder, itemCount); + return true; + case HubProtocolConstants.StreamItemMessageType: + message = CreateStreamItemMessage(ref reader, binder); + return true; + case HubProtocolConstants.CompletionMessageType: + message = CreateCompletionMessage(ref reader, binder); + return true; + case HubProtocolConstants.CancelInvocationMessageType: + message = CreateCancelInvocationMessage(ref reader); + return true; + case HubProtocolConstants.PingMessageType: + message = PingMessage.Instance; + return true; + case HubProtocolConstants.CloseMessageType: + message = CreateCloseMessage(ref reader); + return true; + default: + // Future protocol changes can add message types, old clients can ignore them + message = null; + return false; + } + } + + private static HubMessage CreateInvocationMessage(ref MessagePackReader reader, IInvocationBinder binder, int itemCount) + { + var headers = ReadHeaders(ref reader); + var invocationId = ReadString(ref reader, "invocationId"); + + // For MsgPack, we represent an empty invocation ID as an empty string, + // so we need to normalize that to "null", which is what indicates a non-blocking invocation. + if (string.IsNullOrEmpty(invocationId)) + { + invocationId = null; + } + + var target = ReadString(ref reader, "target"); + + object[] arguments; + try + { + var parameterTypes = binder.GetParameterTypes(target); + arguments = BindArguments(ref reader, parameterTypes); + } + catch (Exception ex) + { + return new InvocationBindingFailureMessage(invocationId, target, ExceptionDispatchInfo.Capture(ex)); + } + + string[] streams = null; + // Previous clients will send 5 items, so we check if they sent a stream array or not + if (itemCount > 5) + { + streams = ReadStreamIds(ref reader); + } + + return ApplyHeaders(headers, new InvocationMessage(invocationId, target, arguments, streams)); + } + + private static HubMessage CreateStreamInvocationMessage(ref MessagePackReader reader, IInvocationBinder binder, int itemCount) + { + var headers = ReadHeaders(ref reader); + var invocationId = ReadString(ref reader, "invocationId"); + var target = ReadString(ref reader, "target"); ; + + object[] arguments; + try + { + var parameterTypes = binder.GetParameterTypes(target); + arguments = BindArguments(ref reader, parameterTypes); + } + catch (Exception ex) + { + return new InvocationBindingFailureMessage(invocationId, target, ExceptionDispatchInfo.Capture(ex)); + } + + string[] streams = null; + // Previous clients will send 5 items, so we check if they sent a stream array or not + if (itemCount > 5) + { + streams = ReadStreamIds(ref reader); + } + + return ApplyHeaders(headers, new StreamInvocationMessage(invocationId, target, arguments, streams)); + } + + private static StreamItemMessage CreateStreamItemMessage(ref MessagePackReader reader, IInvocationBinder binder) + { + var headers = ReadHeaders(ref reader); + var invocationId = ReadString(ref reader, "invocationId"); + + var itemType = binder.GetStreamItemType(invocationId); + var value = DeserializeObject(ref reader, itemType, "item"); + return ApplyHeaders(headers, new StreamItemMessage(invocationId, value)); + } + + private static CompletionMessage CreateCompletionMessage(ref MessagePackReader reader, IInvocationBinder binder) + { + var headers = ReadHeaders(ref reader); + var invocationId = ReadString(ref reader, "invocationId"); + var resultKind = ReadInt32(ref reader, "resultKind"); + + string error = null; + object result = null; + var hasResult = false; + + switch (resultKind) + { + case ErrorResult: + error = ReadString(ref reader, "error"); + break; + case NonVoidResult: + var itemType = binder.GetReturnType(invocationId); + result = DeserializeObject(ref reader, itemType, "argument"); + hasResult = true; + break; + case VoidResult: + hasResult = false; + break; + default: + throw new InvalidDataException("Invalid invocation result kind."); + } + + return ApplyHeaders(headers, new CompletionMessage(invocationId, error, result, hasResult)); + } + + private static CancelInvocationMessage CreateCancelInvocationMessage(ref MessagePackReader reader) + { + var headers = ReadHeaders(ref reader); + var invocationId = ReadString(ref reader, "invocationId"); + return ApplyHeaders(headers, new CancelInvocationMessage(invocationId)); + } + + private static CloseMessage CreateCloseMessage(ref MessagePackReader reader) + { + var error = ReadString(ref reader, "error"); + return new CloseMessage(error); + } + + private static Dictionary ReadHeaders(ref MessagePackReader reader) + { + var headerCount = ReadMapHeader(ref reader, "headers"); + if (headerCount == 0) + { + return null; + } + + var headers = new Dictionary(StringComparer.Ordinal); + for (var i = 0; i < headerCount; i++) + { + var key = ReadString(ref reader, $"headers[{i}].Key"); + var value = ReadString(ref reader, $"headers[{i}].Value"); + + headers[key] = value; + } + + return headers; + } + + private static string[] ReadStreamIds(ref MessagePackReader reader) + { + var streamIdCount = ReadArrayHeader(ref reader, "streamIds"); + + if (streamIdCount == 0) + { + return null; + } + + var streams = new List(); + for (var i = 0; i < streamIdCount; i++) + { + streams.Add(reader.ReadString()); + } + + return streams.ToArray(); + } + + private static object[] BindArguments(ref MessagePackReader reader, IReadOnlyList parameterTypes) + { + var argumentCount = ReadArrayHeader(ref reader, "arguments"); + + if (parameterTypes.Count != argumentCount) + { + throw new InvalidDataException( + $"Invocation provides {argumentCount} argument(s) but target expects {parameterTypes.Count}."); + } + + try + { + var arguments = new object[argumentCount]; + for (var i = 0; i < argumentCount; i++) + { + arguments[i] = DeserializeObject(ref reader, parameterTypes[i], "argument"); + } + + return arguments; + } + catch (Exception ex) + { + throw new InvalidDataException("Error binding arguments. Make sure that the types of the provided values match the types of the hub method being invoked.", ex); + } + } + + /// + public void WriteMessage(HubMessage message, IBufferWriter output) + { + var writer = MemoryBufferWriter.Get(); + + try + { + // Write message to a buffer so we can get its length + WriteMessageCore(message, writer); + + // Write length then message to output + BinaryMessageFormatter.WriteLengthPrefix(writer.Length, output); + writer.CopyTo(output); + } + finally + { + MemoryBufferWriter.Return(writer); + } + } + + ///// + public ReadOnlyMemory GetMessageBytes(HubMessage message) + { + using var writer = new ArrayBufferWriter(); + + // Write message to a buffer so we can get its length + WriteMessageCore(message, writer); + + var memory = writer.WrittenMemory; + + var dataLength = memory.Length; + var prefixLength = BinaryMessageFormatter.LengthPrefixLength(dataLength); + + var array = new byte[dataLength + prefixLength]; + var span = array.AsSpan(); + + // Write length then message to output + var written = BinaryMessageFormatter.WriteLengthPrefix(dataLength, span); + Debug.Assert(written == prefixLength); + + memory.Span.CopyTo(span.Slice(prefixLength)); + + return array; + } + + private void WriteMessageCore(HubMessage message, IBufferWriter bufferWriter) + { + var writer = new MessagePackWriter(bufferWriter); + + switch (message) + { + case InvocationMessage invocationMessage: + WriteInvocationMessage(invocationMessage, ref writer); + break; + case StreamInvocationMessage streamInvocationMessage: + WriteStreamInvocationMessage(streamInvocationMessage, ref writer); + break; + case StreamItemMessage streamItemMessage: + WriteStreamingItemMessage(streamItemMessage, ref writer); + break; + case CompletionMessage completionMessage: + WriteCompletionMessage(completionMessage, ref writer); + break; + case CancelInvocationMessage cancelInvocationMessage: + WriteCancelInvocationMessage(cancelInvocationMessage, ref writer); + break; + case PingMessage pingMessage: + WritePingMessage(pingMessage, ref writer); + break; + case CloseMessage closeMessage: + WriteCloseMessage(closeMessage, ref writer); + break; + default: + throw new InvalidDataException($"Unexpected message type: {message.GetType().Name}"); + } + + writer.Flush(); + } + + private void WriteInvocationMessage(InvocationMessage message, ref MessagePackWriter writer) + { + writer.WriteArrayHeader(6); + + writer.Write(HubProtocolConstants.InvocationMessageType); + PackHeaders(ref writer, message.Headers); + if (string.IsNullOrEmpty(message.InvocationId)) + { + writer.WriteNil(); + } + else + { + writer.Write(message.InvocationId); + } + writer.Write(message.Target); + writer.WriteArrayHeader(message.Arguments.Length); + foreach (var arg in message.Arguments) + { + SerializeArgument(ref writer, arg); + } + + WriteStreamIds(message.StreamIds, ref writer); + } + + private void WriteStreamInvocationMessage(StreamInvocationMessage message, ref MessagePackWriter writer) + { + writer.WriteArrayHeader(6); + + writer.Write(HubProtocolConstants.StreamInvocationMessageType); + PackHeaders(ref writer, message.Headers); + writer.Write(message.InvocationId); + writer.Write(message.Target); + + writer.WriteArrayHeader(message.Arguments.Length); + foreach (var arg in message.Arguments) + { + SerializeArgument(ref writer, arg); + } + + WriteStreamIds(message.StreamIds, ref writer); + } + + private void WriteStreamingItemMessage(StreamItemMessage message, ref MessagePackWriter writer) + { + writer.WriteArrayHeader(4); + writer.Write(HubProtocolConstants.StreamItemMessageType); + PackHeaders(ref writer, message.Headers); + writer.Write(message.InvocationId); + SerializeArgument(ref writer, message.Item); + } + + private void SerializeArgument(ref MessagePackWriter writer, object argument) + { + switch (argument) + { + case null: + writer.WriteNil(); + break; + + case bool boolValue: + writer.Write(boolValue); + break; + + case string stringValue: + writer.Write(stringValue); + break; + + case int intValue: + writer.Write(intValue); + break; + + case long longValue: + writer.Write(longValue); + break; + + case float floatValue: + writer.Write(floatValue); + break; + + case byte[] byteArray: + writer.Write(byteArray); + break; + + default: + throw new FormatException($"Unsupported argument type {argument.GetType()}"); + } + } + + private static object DeserializeObject(ref MessagePackReader reader, Type type, string field) + { + try + { + if (type == typeof(string)) + { + return ReadString(ref reader, "argument"); + } + else if (type == typeof(bool)) + { + return reader.ReadBoolean(); + } + else if (type == typeof(int)) + { + return reader.ReadInt32(); + } + else if (type == typeof(long)) + { + return reader.ReadInt64(); + } + else if (type == typeof(float)) + { + return reader.ReadSingle(); + } + else if (type == typeof(byte[])) + { + var bytes = reader.ReadBytes(); + // MessagePack ensures there are at least as many bytes in the message as declared by the byte header. + // Consequently it is safe to do ToArray on the returned SequenceReader instance. + return bytes.ToArray(); + } + } + catch (Exception ex) + { + throw new InvalidDataException($"Deserializing object of the `{type.Name}` type for '{field}' failed.", ex); + } + + throw new FormatException($"Type {type} is not supported"); + } + + private void WriteStreamIds(string[] streamIds, ref MessagePackWriter writer) + { + if (streamIds != null) + { + writer.WriteArrayHeader(streamIds.Length); + foreach (var streamId in streamIds) + { + writer.Write(streamId); + } + } + else + { + writer.WriteArrayHeader(0); + } + } + + private void WriteCompletionMessage(CompletionMessage message, ref MessagePackWriter writer) + { + var resultKind = + message.Error != null ? ErrorResult : + message.HasResult ? NonVoidResult : + VoidResult; + + writer.WriteArrayHeader(4 + (resultKind != VoidResult ? 1 : 0)); + writer.Write(HubProtocolConstants.CompletionMessageType); + PackHeaders(ref writer, message.Headers); + writer.Write(message.InvocationId); + writer.Write(resultKind); + switch (resultKind) + { + case ErrorResult: + writer.Write(message.Error); + break; + case NonVoidResult: + SerializeArgument(ref writer, message.Result); + break; + } + } + + private void WriteCancelInvocationMessage(CancelInvocationMessage message, ref MessagePackWriter writer) + { + writer.WriteArrayHeader(3); + writer.Write(HubProtocolConstants.CancelInvocationMessageType); + PackHeaders(ref writer, message.Headers); + writer.Write(message.InvocationId); + } + + private void WriteCloseMessage(CloseMessage message, ref MessagePackWriter writer) + { + writer.WriteArrayHeader(2); + writer.Write(HubProtocolConstants.CloseMessageType); + if (string.IsNullOrEmpty(message.Error)) + { + writer.WriteNil(); + } + else + { + writer.Write(message.Error); + } + } + + private void WritePingMessage(PingMessage _, ref MessagePackWriter writer) + { + writer.WriteArrayHeader(1); + writer.Write(HubProtocolConstants.PingMessageType); + } + + private void PackHeaders(ref MessagePackWriter writer, IDictionary headers) + { + if (headers == null) + { + writer.WriteMapHeader(0); + return; + } + + writer.WriteMapHeader(headers.Count); + foreach (var header in headers) + { + writer.Write(header.Key); + writer.Write(header.Value); + } + } + + private static T ApplyHeaders(IDictionary source, T destination) where T : HubInvocationMessage + { + if (source != null && source.Count > 0) + { + destination.Headers = source; + } + + return destination; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int ReadInt32(ref MessagePackReader reader, string field) + { + if (reader.End || reader.NextMessagePackType != MessagePackType.Integer) + { + ThrowInvalidDataException(field, "Int32"); + } + + return reader.ReadInt32(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static string ReadString(ref MessagePackReader reader, string field) + { + if (reader.End) + { + ThrowInvalidDataException(field, "String"); + } + + if (reader.IsNil) + { + reader.ReadNil(); + return null; + } + else if (reader.NextMessagePackType == MessagePackType.String) + { + return reader.ReadString(); + } + + ThrowInvalidDataException(field, "String"); + return null; //This should never be reached. + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int ReadArrayHeader(ref MessagePackReader reader, string field) + { + if (reader.End || reader.NextMessagePackType != MessagePackType.Array) + { + ThrowInvalidCollectionLengthException(field, "array"); + } + + return reader.ReadArrayHeader(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int ReadMapHeader(ref MessagePackReader reader, string field) + { + if (reader.End || reader.NextMessagePackType != MessagePackType.Map) + { + ThrowInvalidCollectionLengthException(field, "map"); + } + + return reader.ReadMapHeader(); + } + + private static void ThrowInvalidDataException(string field, string targetType) + { + throw new InvalidDataException($"Reading '{field}' as {targetType} failed."); + } + + private static void ThrowInvalidCollectionLengthException(string field, string collection) + { + throw new InvalidDataException($"Reading {collection} length for '{field}' failed."); + } + } +} diff --git a/src/Components/Server/src/BlazorPack/NativeDateTimeFormatter.cs b/src/Components/Server/src/BlazorPack/NativeDateTimeFormatter.cs new file mode 100644 index 000000000000..f45c61926c34 --- /dev/null +++ b/src/Components/Server/src/BlazorPack/NativeDateTimeFormatter.cs @@ -0,0 +1,9 @@ +// 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. + +namespace MessagePack.Formatters +{ + internal class NativeDateTimeFormatter + { + } +} diff --git a/src/Components/Server/src/Circuits/MessagePackBufferStream.cs b/src/Components/Server/src/Circuits/MessagePackBufferStream.cs deleted file mode 100644 index aec2593d9995..000000000000 --- a/src/Components/Server/src/Circuits/MessagePackBufferStream.cs +++ /dev/null @@ -1,65 +0,0 @@ -// 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 MessagePack; -using System; -using System.IO; - -namespace Microsoft.AspNetCore.Components.Server.Circuits -{ - /// - /// Provides Stream APIs for writing to a MessagePack-supplied expandable buffer. - /// - internal class MessagePackBufferStream : Stream - { - private byte[] _buffer; - private int _headerStartOffset; - private int _bodyLength; - - public MessagePackBufferStream(byte[] buffer, int offset) - { - _buffer = buffer ?? throw new ArgumentNullException(nameof(buffer)); - _headerStartOffset = offset; - _bodyLength = 0; - } - - public byte[] Buffer => _buffer; - - public override bool CanRead => false; - public override bool CanSeek => false; - public override bool CanWrite => true; - - // Length is the complete number of bytes being output - public override long Length => _bodyLength; - - // Position is the index into the writable body (i.e., so position zero - // is the first byte you can actually write a value to) - public override long Position - { - get => _bodyLength; - set => throw new NotSupportedException(); - } - - public override void Flush() - { - // Nothing to do, as we're not buffering separately anyway - } - - public override int Read(byte[] buffer, int offset, int count) - => throw new NotImplementedException(); - - public override long Seek(long offset, SeekOrigin origin) - => throw new NotImplementedException(); - - public override void SetLength(long value) - => throw new NotImplementedException(); - - public override void Write(byte[] src, int srcOffset, int count) - { - var outputOffset = _headerStartOffset + _bodyLength; - MessagePackBinary.EnsureCapacity(ref _buffer, outputOffset, count); - System.Buffer.BlockCopy(src, srcOffset, _buffer, outputOffset, count); - _bodyLength += count; - } - } -} diff --git a/src/Components/Server/src/Circuits/RemoteRenderer.cs b/src/Components/Server/src/Circuits/RemoteRenderer.cs index 9edd2e791a15..1b0d9d798d18 100644 --- a/src/Components/Server/src/Circuits/RemoteRenderer.cs +++ b/src/Components/Server/src/Circuits/RemoteRenderer.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.IO; using System.Text.Encodings.Web; using System.Threading; using System.Threading.Tasks; @@ -114,7 +115,16 @@ protected override Task UpdateDisplayAsync(in RenderBatch batch) // snapshot its contents now. // TODO: Consider using some kind of array pool instead of allocating a new // buffer on every render. - var batchBytes = MessagePackSerializer.Serialize(batch, RenderBatchFormatterResolver.Instance); + byte[] batchBytes; + using (var memoryStream = new MemoryStream()) + { + using (var renderBatchWriter = new RenderBatchWriter(memoryStream, false)) + { + renderBatchWriter.Write(in batch); + } + + batchBytes = memoryStream.ToArray(); + } if (!_client.Connected) { diff --git a/src/Components/Server/src/Circuits/RenderBatchFormatterResolver.cs b/src/Components/Server/src/Circuits/RenderBatchFormatterResolver.cs deleted file mode 100644 index 6e5be49bf835..000000000000 --- a/src/Components/Server/src/Circuits/RenderBatchFormatterResolver.cs +++ /dev/null @@ -1,49 +0,0 @@ -// 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 MessagePack; -using MessagePack.Formatters; -using Microsoft.AspNetCore.Components.Rendering; -using System; - -namespace Microsoft.AspNetCore.Components.Server.Circuits -{ - /// - /// A MessagePack IFormatterResolver that provides an efficient binary serialization - /// of . The client-side code knows how to walk through this - /// binary representation directly, without it first being parsed as an object graph. - /// - internal class RenderBatchFormatterResolver : IFormatterResolver - { - public static readonly RenderBatchFormatterResolver Instance = new RenderBatchFormatterResolver(); - - public IMessagePackFormatter GetFormatter() - => typeof(T) == typeof(RenderBatch) ? (IMessagePackFormatter)RenderBatchFormatter.Instance : null; - - private class RenderBatchFormatter : IMessagePackFormatter - { - public static readonly RenderBatchFormatter Instance = new RenderBatchFormatter(); - - // No need to accept incoming RenderBatch - public RenderBatch Deserialize(byte[] bytes, int offset, IFormatterResolver formatterResolver, out int readSize) - => throw new NotImplementedException(); - - public int Serialize(ref byte[] bytes, int offset, RenderBatch value, IFormatterResolver formatterResolver) - { - // Instead of using MessagePackBinary.WriteBytes, we write into a stream that - // knows how to write the data using MessagePack writer APIs. The benefit - // is that we don't have to allocate a second large buffer to capture the - // RenderBatchWriter output - we can just write directly to the underlying - // output buffer. - using (var bufferStream = new MessagePackBufferStream(bytes, offset)) - using (var renderBatchWriter = new RenderBatchWriter(bufferStream, leaveOpen: false)) - { - renderBatchWriter.Write(value); - - bytes = bufferStream.Buffer; // In case the buffer was expanded - return (int)bufferStream.Length; - } - } - } - } -} diff --git a/src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs b/src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs index cbb666ef4736..1d3cec0820e2 100644 --- a/src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs +++ b/src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs @@ -3,8 +3,10 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Components.Server; +using Microsoft.AspNetCore.Components.Server.BlazorPack; using Microsoft.AspNetCore.Components.Server.Circuits; using Microsoft.AspNetCore.Components.Services; +using Microsoft.AspNetCore.SignalR.Protocol; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; using Microsoft.JSInterop; @@ -23,7 +25,15 @@ public static class ComponentServiceCollectionExtensions /// The . public static IServiceCollection AddRazorComponents(this IServiceCollection services) { - services.AddSignalR().AddMessagePackProtocol(); + services.AddSignalR() + .AddHubOptions(options => + { + options.SupportedProtocols.Clear(); + options.SupportedProtocols.Add(BlazorPackHubProtocol.ProtocolName); + }); + + // Register the Blazor specific hub protocol + services.TryAddEnumerable(ServiceDescriptor.Singleton()); // Here we add a bunch of services that don't vary in any way based on the // user's configuration. So even if the user has multiple independent server-side diff --git a/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj b/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj index f5b9695752d2..2a46cccf277d 100644 --- a/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj +++ b/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj @@ -1,4 +1,4 @@ - + netcoreapp3.0 @@ -6,6 +6,7 @@ true true false + true @@ -21,7 +22,6 @@ - @@ -33,6 +33,31 @@ + + $(RepositoryRoot)src\submodules\MessagePack-CSharp\src\MessagePack\ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Components/Server/test/BlazorPack/BlazorPackHubProtocolTest.cs b/src/Components/Server/test/BlazorPack/BlazorPackHubProtocolTest.cs new file mode 100644 index 000000000000..e41c045d651f --- /dev/null +++ b/src/Components/Server/test/BlazorPack/BlazorPackHubProtocolTest.cs @@ -0,0 +1,13 @@ +// 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 Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol; +using Microsoft.AspNetCore.SignalR.Protocol; + +namespace Microsoft.AspNetCore.Components.Server.BlazorPack +{ + public class BlazorPackHubProtocolTest : MessagePackHubProtocolTestBase + { + protected override IHubProtocol HubProtocol { get; } = new BlazorPackHubProtocol(); + } +} diff --git a/src/Components/Server/test/Circuits/MessagePackBufferStreamTest.cs b/src/Components/Server/test/Circuits/MessagePackBufferStreamTest.cs deleted file mode 100644 index 44ab3abcbaf1..000000000000 --- a/src/Components/Server/test/Circuits/MessagePackBufferStreamTest.cs +++ /dev/null @@ -1,99 +0,0 @@ -// 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 Microsoft.AspNetCore.Components.Server.Circuits; -using System; -using Xunit; - -namespace Microsoft.AspNetCore.Components.Server -{ - public class MessagePackBufferStreamTest - { - [Fact] - public void NullBuffer_Throws() - { - var ex = Assert.Throws(() => - { - new MessagePackBufferStream(null, 0); - }); - - Assert.Equal("buffer", ex.ParamName); - } - - [Fact] - public void WithWrites_WritesToUnderlyingBuffer() - { - // Arrange - var buffer = new byte[100]; - var offset = 58; // Arbitrary - - // Act/Assert - using (var stream = new MessagePackBufferStream(buffer, offset)) - { - stream.Write(new byte[] { 10, 20, 30, 40 }, 1, 2); // Write 2 bytes - stream.Write(new byte[] { 101 }, 0, 1); // Write another 1 byte - stream.Close(); - - Assert.Equal(20, buffer[offset]); - Assert.Equal(30, buffer[offset + 1]); - Assert.Equal(101, buffer[offset + 2]); - } - } - - [Fact] - public void LengthAndPositionAreEquivalent() - { - // Arrange - var buffer = new byte[20]; - var offset = 3; - - // Act/Assert - using (var stream = new MessagePackBufferStream(buffer, offset)) - { - stream.Write(new byte[] { 0x01, 0x02 }, 0, 2); - Assert.Equal(2, stream.Length); - Assert.Equal(2, stream.Position); - } - } - - [Fact] - public void WithWrites_ExpandsBufferWhenNeeded() - { - // Arrange - var origBuffer = new byte[10]; - var offset = 6; - origBuffer[0] = 123; // So we can check it was retained during expansion - - // Act/Assert - using (var stream = new MessagePackBufferStream(origBuffer, offset)) - { - // We can fit the 6-byte offset plus 3 written bytes - // into the original 10-byte buffer - stream.Write(new byte[] { 10, 20, 30 }, 0, 3); - Assert.Same(origBuffer, stream.Buffer); - - // Trying to add two more exceeds the capacity, so the buffer expands - stream.Write(new byte[] { 40, 50 }, 0, 2); - Assert.NotSame(origBuffer, stream.Buffer); - Assert.True(stream.Buffer.Length > origBuffer.Length); - - // Check the expanded buffer has the expected contents - stream.Close(); - Assert.Equal(123, stream.Buffer[0]); // Retains other values from original buffer - Assert.Equal(10, stream.Buffer[offset]); - Assert.Equal(20, stream.Buffer[offset + 1]); - Assert.Equal(30, stream.Buffer[offset + 2]); - Assert.Equal(40, stream.Buffer[offset + 3]); - Assert.Equal(50, stream.Buffer[offset + 4]); - } - } - - int ReadBigEndianInt32(byte[] buffer, int startOffset) - { - return (buffer[startOffset] << 24) - + (buffer[startOffset + 1] << 16) - + (buffer[startOffset + 2] << 8) - + (buffer[startOffset + 3]); - } - } -} diff --git a/src/Components/Server/test/Microsoft.AspNetCore.Components.Server.Tests.csproj b/src/Components/Server/test/Microsoft.AspNetCore.Components.Server.Tests.csproj index 2d6e6018e83a..46ae7b0e4cc1 100644 --- a/src/Components/Server/test/Microsoft.AspNetCore.Components.Server.Tests.csproj +++ b/src/Components/Server/test/Microsoft.AspNetCore.Components.Server.Tests.csproj @@ -11,8 +11,16 @@ + + $(RepositoryRoot)src\SignalR\common\SignalR.Common\test\Internal\Protocol\ + + + + + + diff --git a/src/SignalR/common/SignalR.Common/test/Internal/Protocol/MessagePackHubProtocolTestBase.cs b/src/SignalR/common/SignalR.Common/test/Internal/Protocol/MessagePackHubProtocolTestBase.cs new file mode 100644 index 000000000000..f8260708e677 --- /dev/null +++ b/src/SignalR/common/SignalR.Common/test/Internal/Protocol/MessagePackHubProtocolTestBase.cs @@ -0,0 +1,440 @@ +// 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.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using Microsoft.AspNetCore.Internal; +using Microsoft.AspNetCore.SignalR.Protocol; +using Xunit; + +namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol +{ + using static HubMessageHelpers; + + /// + /// Common MessagePack-based hub protocol tests that is shared by MessagePackHubProtocol and Blazor's internal messagepack based-hub protocol. + /// Since the latter only supports simple data types such as ints, strings, bools, and bytes for serialization, only tests that + /// require no serialization (or deserialization), or tests that serialize simple data types should go here. + /// Tests that verify deserialization of complex data types should go in MessagePackHubProtocolTests. + /// + public abstract class MessagePackHubProtocolTestBase + { + protected static readonly IDictionary TestHeaders = new Dictionary + { + { "Foo", "Bar" }, + { "KeyWith\nNew\r\nLines", "Still Works" }, + { "ValueWithNewLines", "Also\nWorks\r\nFine" }, + }; + + protected abstract IHubProtocol HubProtocol { get; } + + public enum TestEnum + { + Zero = 0, + One + } + + // Test Data for Parse/WriteMessages: + // * Name: A string name that is used when reporting the test (it's the ToString value for ProtocolTestData) + // * Message: The HubMessage that is either expected (in Parse) or used as input (in Write) + // * Binary: Base64-encoded binary "baseline" to sanity-check MessagePack-CSharp behavior + // + // When changing the tests/message pack parsing if you get test failures look at the base64 encoding and + // use a tool like https://sugendran.github.io/msgpack-visualizer/ to verify that the MsgPack is correct and then just replace the Base64 value. + + public static IEnumerable BaseTestDataNames + { + get + { + foreach (var k in BaseTestData.Keys) + { + yield return new object[] { k }; + } + } + } + + public static IDictionary BaseTestData => new[] + { + // Invocation messages + new ProtocolTestData( + name: "InvocationWithNoHeadersAndNoArgs", + message: new InvocationMessage("xyz", "method", Array.Empty()), + binary: "lgGAo3h5eqZtZXRob2SQkA=="), + new ProtocolTestData( + name: "InvocationWithNoHeadersNoIdAndNoArgs", + message: new InvocationMessage("method", Array.Empty()), + binary: "lgGAwKZtZXRob2SQkA=="), + new ProtocolTestData( + name: "InvocationWithNoHeadersNoIdAndSingleIntArg", + message: new InvocationMessage("method", new object[] { 42 }), + binary: "lgGAwKZtZXRob2SRKpA="), + new ProtocolTestData( + name: "InvocationWithNoHeadersNoIdIntAndStringArgs", + message: new InvocationMessage("method", new object[] { 42, "string" }), + binary: "lgGAwKZtZXRob2SSKqZzdHJpbmeQ"), + new ProtocolTestData( + name: "InvocationWithStreamArgument", + message: new InvocationMessage(null, "Target", Array.Empty(), new string[] { "__test_id__" }), + binary: "lgGAwKZUYXJnZXSQkatfX3Rlc3RfaWRfXw=="), + new ProtocolTestData( + name: "InvocationWithStreamAndNormalArgument", + message: new InvocationMessage(null, "Target", new object[] { 42 }, new string[] { "__test_id__" }), + binary: "lgGAwKZUYXJnZXSRKpGrX190ZXN0X2lkX18="), + new ProtocolTestData( + name: "InvocationWithMulitpleStreams", + message: new InvocationMessage(null, "Target", Array.Empty(), new string[] { "__test_id__", "__test_id2__" }), + binary: "lgGAwKZUYXJnZXSQkqtfX3Rlc3RfaWRfX6xfX3Rlc3RfaWQyX18="), + + // StreamItem Messages + new ProtocolTestData( + name: "StreamItemWithNoHeadersAndIntItem", + message: new StreamItemMessage("xyz", item: 42), + binary: "lAKAo3h5eio="), + new ProtocolTestData( + name: "StreamItemWithNoHeadersAndFloatItem", + message: new StreamItemMessage("xyz", item: 42.0f), + binary: "lAKAo3h5espCKAAA"), + new ProtocolTestData( + name: "StreamItemWithNoHeadersAndStringItem", + message: new StreamItemMessage("xyz", item: "string"), + binary: "lAKAo3h5eqZzdHJpbmc="), + new ProtocolTestData( + name: "StreamItemWithNoHeadersAndBoolItem", + message: new StreamItemMessage("xyz", item: true), + binary: "lAKAo3h5esM="), + + // Completion Messages + new ProtocolTestData( + name: "CompletionWithNoHeadersAndError", + message: CompletionMessage.WithError("xyz", error: "Error not found!"), + binary: "lQOAo3h5egGwRXJyb3Igbm90IGZvdW5kIQ=="), + new ProtocolTestData( + name: "CompletionWithHeadersAndError", + message: AddHeaders(TestHeaders, CompletionMessage.WithError("xyz", error: "Error not found!")), + binary: "lQODo0Zvb6NCYXKyS2V5V2l0aApOZXcNCkxpbmVzq1N0aWxsIFdvcmtzsVZhbHVlV2l0aE5ld0xpbmVzsEFsc28KV29ya3MNCkZpbmWjeHl6AbBFcnJvciBub3QgZm91bmQh"), + new ProtocolTestData( + name: "CompletionWithNoHeadersAndNoResult", + message: CompletionMessage.Empty("xyz"), + binary: "lAOAo3h5egI="), + new ProtocolTestData( + name: "CompletionWithHeadersAndNoResult", + message: AddHeaders(TestHeaders, CompletionMessage.Empty("xyz")), + binary: "lAODo0Zvb6NCYXKyS2V5V2l0aApOZXcNCkxpbmVzq1N0aWxsIFdvcmtzsVZhbHVlV2l0aE5ld0xpbmVzsEFsc28KV29ya3MNCkZpbmWjeHl6Ag=="), + new ProtocolTestData( + name: "CompletionWithNoHeadersAndIntResult", + message: CompletionMessage.WithResult("xyz", payload: 42), + binary: "lQOAo3h5egMq"), + new ProtocolTestData( + name: "CompletionWithNoHeadersAndFloatResult", + message: CompletionMessage.WithResult("xyz", payload: 42.0f), + binary: "lQOAo3h5egPKQigAAA=="), + new ProtocolTestData( + name: "CompletionWithNoHeadersAndStringResult", + message: CompletionMessage.WithResult("xyz", payload: "string"), + binary: "lQOAo3h5egOmc3RyaW5n"), + new ProtocolTestData( + name: "CompletionWithNoHeadersAndBooleanResult", + message: CompletionMessage.WithResult("xyz", payload: true), + binary: "lQOAo3h5egPD"), + + // StreamInvocation Messages + new ProtocolTestData( + name: "StreamInvocationWithNoHeadersAndNoArgs", + message: new StreamInvocationMessage("xyz", "method", Array.Empty()), + binary: "lgSAo3h5eqZtZXRob2SQkA=="), + new ProtocolTestData( + name: "StreamInvocationWithNoHeadersAndIntArg", + message: new StreamInvocationMessage("xyz", "method", new object[] { 42 }), + binary: "lgSAo3h5eqZtZXRob2SRKpA="), + new ProtocolTestData( + name: "StreamInvocationWithStreamArgument", + message: new StreamInvocationMessage("xyz", "method", Array.Empty(), new string[] { "__test_id__" }), + binary: "lgSAo3h5eqZtZXRob2SQkatfX3Rlc3RfaWRfXw=="), + new ProtocolTestData( + name: "StreamInvocationWithStreamAndNormalArgument", + message: new StreamInvocationMessage("xyz", "method", new object[] { 42 }, new string[] { "__test_id__" }), + binary: "lgSAo3h5eqZtZXRob2SRKpGrX190ZXN0X2lkX18="), + new ProtocolTestData( + name: "StreamInvocationWithNoHeadersAndIntAndStringArgs", + message: new StreamInvocationMessage("xyz", "method", new object[] { 42, "string" }), + binary: "lgSAo3h5eqZtZXRob2SSKqZzdHJpbmeQ"), + + // CancelInvocation Messages + new ProtocolTestData( + name: "CancelInvocationWithNoHeaders", + message: new CancelInvocationMessage("xyz"), + binary: "kwWAo3h5eg=="), + new ProtocolTestData( + name: "CancelInvocationWithHeaders", + message: AddHeaders(TestHeaders, new CancelInvocationMessage("xyz")), + binary: "kwWDo0Zvb6NCYXKyS2V5V2l0aApOZXcNCkxpbmVzq1N0aWxsIFdvcmtzsVZhbHVlV2l0aE5ld0xpbmVzsEFsc28KV29ya3MNCkZpbmWjeHl6"), + + // Ping Messages + new ProtocolTestData( + name: "Ping", + message: PingMessage.Instance, + binary: "kQY="), + }.ToDictionary(t => t.Name); + + [Theory] + [MemberData(nameof(BaseTestDataNames))] + public void BaseParseMessages(string testDataName) + { + var testData = BaseTestData[testDataName]; + + TestParseMessages(testData); + } + + protected void TestParseMessages(ProtocolTestData testData) + { + // Verify that the input binary string decodes to the expected MsgPack primitives + var bytes = Convert.FromBase64String(testData.Binary); + + // Parse the input fully now. + bytes = Frame(bytes); + var data = new ReadOnlySequence(bytes); + Assert.True(HubProtocol.TryParseMessage(ref data, new TestBinder(testData.Message), out var message)); + + Assert.NotNull(message); + Assert.Equal(testData.Message, message, TestHubMessageEqualityComparer.Instance); + } + + [Fact] + public void ParseMessageWithExtraData() + { + var expectedMessage = new InvocationMessage("xyz", "method", Array.Empty()); + + // Verify that the input binary string decodes to the expected MsgPack primitives + var bytes = new byte[] { ArrayBytes(8), + 1, + 0x80, + StringBytes(3), (byte)'x', (byte)'y', (byte)'z', + StringBytes(6), (byte)'m', (byte)'e', (byte)'t', (byte)'h', (byte)'o', (byte)'d', + ArrayBytes(0), // Arguments + ArrayBytes(0), // Streams + 0xc3, + StringBytes(2), (byte)'e', (byte)'x' }; + + // Parse the input fully now. + bytes = Frame(bytes); + var data = new ReadOnlySequence(bytes); + Assert.True(HubProtocol.TryParseMessage(ref data, new TestBinder(expectedMessage), out var message)); + + Assert.NotNull(message); + Assert.Equal(expectedMessage, message, TestHubMessageEqualityComparer.Instance); + } + + [Theory] + [MemberData(nameof(BaseTestDataNames))] + public void BaseWriteMessages(string testDataName) + { + var testData = BaseTestData[testDataName]; + + TestWriteMessages(testData); + } + + protected void TestWriteMessages(ProtocolTestData testData) + { + var bytes = Write(testData.Message); + + // Unframe the message to check the binary encoding + var byteSpan = new ReadOnlySequence(bytes); + Assert.True(BinaryMessageParser.TryParseMessage(ref byteSpan, out var unframed)); + + // Check the baseline binary encoding, use Assert.True in order to configure the error message + var actual = Convert.ToBase64String(unframed.ToArray()); + Assert.True(string.Equals(actual, testData.Binary, StringComparison.Ordinal), $"Binary encoding changed from{Environment.NewLine} [{testData.Binary}]{Environment.NewLine} to{Environment.NewLine} [{actual}]{Environment.NewLine}Please verify the MsgPack output and update the baseline"); + } + + public static IDictionary BaseInvalidPayloads => new[] + { + // Message Type + new InvalidMessageData("MessageTypeString", new byte[] { 0x91, 0xa3, (byte)'f', (byte)'o', (byte)'o' }, "Reading 'messageType' as Int32 failed."), + + // Headers + new InvalidMessageData("HeadersNotAMap", new byte[] { 0x92, 1, 0xa3, (byte)'f', (byte)'o', (byte)'o' }, "Reading map length for 'headers' failed."), + new InvalidMessageData("HeaderKeyInt", new byte[] { 0x92, 1, 0x82, 0x2a, 0xa3, (byte)'f', (byte)'o', (byte)'o' }, "Reading 'headers[0].Key' as String failed."), + new InvalidMessageData("HeaderValueInt", new byte[] { 0x92, 1, 0x82, 0xa3, (byte)'f', (byte)'o', (byte)'o', 42 }, "Reading 'headers[0].Value' as String failed."), + new InvalidMessageData("HeaderKeyArray", new byte[] { 0x92, 1, 0x84, 0xa3, (byte)'f', (byte)'o', (byte)'o', 0xa3, (byte)'f', (byte)'o', (byte)'o', 0x90, 0xa3, (byte)'f', (byte)'o', (byte)'o' }, "Reading 'headers[1].Key' as String failed."), + new InvalidMessageData("HeaderValueArray", new byte[] { 0x92, 1, 0x84, 0xa3, (byte)'f', (byte)'o', (byte)'o', 0xa3, (byte)'f', (byte)'o', (byte)'o', 0xa3, (byte)'f', (byte)'o', (byte)'o', 0x90 }, "Reading 'headers[1].Value' as String failed."), + + // InvocationMessage + new InvalidMessageData("InvocationMissingId", new byte[] { 0x92, 1, 0x80 }, "Reading 'invocationId' as String failed."), + new InvalidMessageData("InvocationIdBoolean", new byte[] { 0x91, 1, 0x80, 0xc2 }, "Reading 'invocationId' as String failed."), + new InvalidMessageData("InvocationTargetMissing", new byte[] { 0x93, 1, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c' }, "Reading 'target' as String failed."), + new InvalidMessageData("InvocationTargetInt", new byte[] { 0x94, 1, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 42 }, "Reading 'target' as String failed."), + + // StreamInvocationMessage + new InvalidMessageData("StreamInvocationMissingId", new byte[] { 0x92, 4, 0x80 }, "Reading 'invocationId' as String failed."), + new InvalidMessageData("StreamInvocationIdBoolean", new byte[] { 0x93, 4, 0x80, 0xc2 }, "Reading 'invocationId' as String failed."), + new InvalidMessageData("StreamInvocationTargetMissing", new byte[] { 0x93, 4, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c' }, "Reading 'target' as String failed."), + new InvalidMessageData("StreamInvocationTargetInt", new byte[] { 0x94, 4, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 42 }, "Reading 'target' as String failed."), + + // StreamItemMessage + new InvalidMessageData("StreamItemMissingId", new byte[] { 0x92, 2, 0x80 }, "Reading 'invocationId' as String failed."), + new InvalidMessageData("StreamItemInvocationIdBoolean", new byte[] { 0x93, 2, 0x80, 0xc2 }, "Reading 'invocationId' as String failed."), + new InvalidMessageData("StreamItemMissing", new byte[] { 0x93, 2, 0x80, 0xa3, (byte)'x', (byte)'y', (byte)'z' }, "Deserializing object of the `String` type for 'item' failed."), + new InvalidMessageData("StreamItemTypeMismatch", new byte[] { 0x94, 2, 0x80, 0xa3, (byte)'x', (byte)'y', (byte)'z', 42 }, "Deserializing object of the `String` type for 'item' failed."), + + // CompletionMessage + new InvalidMessageData("CompletionMissingId", new byte[] { 0x92, 3, 0x80 }, "Reading 'invocationId' as String failed."), + new InvalidMessageData("CompletionIdBoolean", new byte[] { 0x93, 3, 0x80, 0xc2 }, "Reading 'invocationId' as String failed."), + new InvalidMessageData("CompletionResultKindString", new byte[] { 0x94, 3, 0x80, 0xa3, (byte)'x', (byte)'y', (byte)'z', 0xa3, (byte)'x', (byte)'y', (byte)'z' }, "Reading 'resultKind' as Int32 failed."), + new InvalidMessageData("CompletionResultKindOutOfRange", new byte[] { 0x94, 3, 0x80, 0xa3, (byte)'x', (byte)'y', (byte)'z', 42 }, "Invalid invocation result kind."), + new InvalidMessageData("CompletionErrorMissing", new byte[] { 0x94, 3, 0x80, 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x01 }, "Reading 'error' as String failed."), + new InvalidMessageData("CompletionErrorInt", new byte[] { 0x95, 3, 0x80, 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x01, 42 }, "Reading 'error' as String failed."), + new InvalidMessageData("CompletionResultMissing", new byte[] { 0x94, 3, 0x80, 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x03 }, "Deserializing object of the `String` type for 'argument' failed."), + new InvalidMessageData("CompletionResultTypeMismatch", new byte[] { 0x95, 3, 0x80, 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x03, 42 }, "Deserializing object of the `String` type for 'argument' failed."), + }.ToDictionary(t => t.Name); + + public static IEnumerable BaseInvalidPayloadNames => BaseInvalidPayloads.Keys.Select(name => new object[] { name }); + + [Theory] + [MemberData(nameof(BaseInvalidPayloadNames))] + public void ParserThrowsForInvalidMessages(string invalidPayloadName) + { + var testData = BaseInvalidPayloads[invalidPayloadName]; + + TestInvalidMessageDate(testData); + } + + protected void TestInvalidMessageDate(InvalidMessageData testData) + { + var buffer = Frame(testData.Encoded); + var binder = new TestBinder(new[] { typeof(string) }, typeof(string)); + var data = new ReadOnlySequence(buffer); + var exception = Assert.Throws(() => HubProtocol.TryParseMessage(ref data, binder, out _)); + + Assert.Equal(testData.ErrorMessage, exception.Message); + } + + public static IDictionary ArgumentBindingErrors => new[] + { + // InvocationMessage + new InvalidMessageData("InvocationArgumentArrayMissing", new byte[] { 0x94, 1, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z' }, "Reading array length for 'arguments' failed."), + new InvalidMessageData("InvocationArgumentArrayNotAnArray", new byte[] { 0x95, 1, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z', 42 }, "Reading array length for 'arguments' failed."), + new InvalidMessageData("InvocationArgumentArraySizeMismatchEmpty", new byte[] { 0x95, 1, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x90 }, "Invocation provides 0 argument(s) but target expects 1."), + new InvalidMessageData("InvocationArgumentArraySizeMismatchTooLarge", new byte[] { 0x95, 1, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x92, 0xa1, (byte)'a', 0xa1, (byte)'b' }, "Invocation provides 2 argument(s) but target expects 1."), + new InvalidMessageData("InvocationArgumentTypeMismatch", new byte[] { 0x95, 1, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x91, 42 }, "Error binding arguments. Make sure that the types of the provided values match the types of the hub method being invoked."), + + // StreamInvocationMessage + new InvalidMessageData("StreamInvocationArgumentArrayMissing", new byte[] { 0x94, 4, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z' }, "Reading array length for 'arguments' failed."), // array is missing + new InvalidMessageData("StreamInvocationArgumentArrayNotAnArray", new byte[] { 0x95, 4, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z', 42 }, "Reading array length for 'arguments' failed."), // arguments isn't an array + new InvalidMessageData("StreamInvocationArgumentArraySizeMismatchEmpty", new byte[] { 0x95, 4, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x90 }, "Invocation provides 0 argument(s) but target expects 1."), // array is missing elements + new InvalidMessageData("StreamInvocationArgumentArraySizeMismatchTooLarge", new byte[] { 0x95, 4, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x92, 0xa1, (byte)'a', 0xa1, (byte)'b' }, "Invocation provides 2 argument(s) but target expects 1."), // argument count does not match binder argument count + new InvalidMessageData("StreamInvocationArgumentTypeMismatch", new byte[] { 0x95, 4, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x91, 42 }, "Error binding arguments. Make sure that the types of the provided values match the types of the hub method being invoked."), // argument type mismatch + }.ToDictionary(t => t.Name); + + public static IEnumerable ArgumentBindingErrorNames => ArgumentBindingErrors.Keys.Select(name => new object[] { name }); + + [Theory] + [MemberData(nameof(ArgumentBindingErrorNames))] + public void GettingArgumentsThrowsIfBindingFailed(string argumentBindingErrorName) + { + var testData = ArgumentBindingErrors[argumentBindingErrorName]; + + var buffer = Frame(testData.Encoded); + var binder = new TestBinder(new[] { typeof(string) }, typeof(string)); + var data = new ReadOnlySequence(buffer); + HubProtocol.TryParseMessage(ref data, binder, out var message); + var bindingFailure = Assert.IsType(message); + Assert.Equal(testData.ErrorMessage, bindingFailure.BindingFailure.SourceException.Message); + } + + [Theory] + [InlineData(new byte[] { 0x05, 0x01 })] + public void ParserDoesNotConsumePartialData(byte[] payload) + { + var binder = new TestBinder(new[] { typeof(string) }, typeof(string)); + var data = new ReadOnlySequence(payload); + var result = HubProtocol.TryParseMessage(ref data, binder, out var message); + Assert.Null(message); + } + + protected byte ArrayBytes(int size) + { + Debug.Assert(size < 16, "Test code doesn't support array sizes greater than 15"); + + return (byte)(0x90 | size); + } + + protected byte StringBytes(int size) + { + Debug.Assert(size < 16, "Test code doesn't support string sizes greater than 15"); + + return (byte)(0xa0 | size); + } + + protected static void AssertMessages(byte[] expectedOutput, ReadOnlyMemory bytes) + { + var data = new ReadOnlySequence(bytes); + Assert.True(BinaryMessageParser.TryParseMessage(ref data, out var message)); + Assert.Equal(expectedOutput, message.ToArray()); + } + + protected static byte[] Frame(byte[] input) + { + var stream = MemoryBufferWriter.Get(); + try + { + BinaryMessageFormatter.WriteLengthPrefix(input.Length, stream); + stream.Write(input); + return stream.ToArray(); + } + finally + { + MemoryBufferWriter.Return(stream); + } + } + + protected byte[] Write(HubMessage message) + { + var writer = MemoryBufferWriter.Get(); + try + { + HubProtocol.WriteMessage(message, writer); + return writer.ToArray(); + } + finally + { + MemoryBufferWriter.Return(writer); + } + } + + public class InvalidMessageData + { + public string Name { get; private set; } + public byte[] Encoded { get; private set; } + public string ErrorMessage { get; private set; } + + public InvalidMessageData(string name, byte[] encoded, string errorMessage) + { + Name = name; + Encoded = encoded; + ErrorMessage = errorMessage; + } + + public override string ToString() => Name; + } + + public class ProtocolTestData + { + public string Name { get; } + public string Binary { get; } + public HubMessage Message { get; } + + public ProtocolTestData(string name, HubMessage message, string binary) + { + Name = name; + Message = message; + Binary = binary; + } + + public override string ToString() => Name; + } + } +} diff --git a/src/SignalR/common/SignalR.Common/test/Internal/Protocol/MessagePackHubProtocolTests.cs b/src/SignalR/common/SignalR.Common/test/Internal/Protocol/MessagePackHubProtocolTests.cs index 35fe3c04fcca..8134f8e9bf86 100644 --- a/src/SignalR/common/SignalR.Common/test/Internal/Protocol/MessagePackHubProtocolTests.cs +++ b/src/SignalR/common/SignalR.Common/test/Internal/Protocol/MessagePackHubProtocolTests.cs @@ -8,38 +8,72 @@ using System.IO; using System.Linq; using Microsoft.AspNetCore.Internal; +using Microsoft.AspNetCore.SignalR.Protocol; using Xunit; namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol { - using Microsoft.AspNetCore.SignalR.Protocol; using static HubMessageHelpers; - public class MessagePackHubProtocolTests + public class MessagePackHubProtocolTests : MessagePackHubProtocolTestBase { - private static readonly IDictionary TestHeaders = new Dictionary - { - { "Foo", "Bar" }, - { "KeyWith\nNew\r\nLines", "Still Works" }, - { "ValueWithNewLines", "Also\nWorks\r\nFine" }, - }; + protected override IHubProtocol HubProtocol => new MessagePackHubProtocol(); - private static readonly MessagePackHubProtocol _hubProtocol - = new MessagePackHubProtocol(); + [Fact] + public void SerializerCanSerializeTypesWithNoDefaultCtor() + { + var result = Write(CompletionMessage.WithResult("0", new List { 42 }.AsReadOnly())); + AssertMessages(new byte[] { ArrayBytes(5), 3, 0x80, StringBytes(1), (byte)'0', 0x03, ArrayBytes(1), 42 }, result); + } - public enum TestEnum + [Fact] + public void WriteAndParseDateTimeConvertsToUTC() { - Zero = 0, - One + var dateTime = new DateTime(2018, 4, 9); + var writer = MemoryBufferWriter.Get(); + + try + { + HubProtocol.WriteMessage(CompletionMessage.WithResult("xyz", dateTime), writer); + var bytes = new ReadOnlySequence(writer.ToArray()); + HubProtocol.TryParseMessage(ref bytes, new TestBinder(typeof(DateTime)), out var hubMessage); + + var completionMessage = Assert.IsType(hubMessage); + + var resultDateTime = (DateTime)completionMessage.Result; + // The messagepack Timestamp format specifies that time is stored as seconds since 1970-01-01 UTC + // so the library has no choice but to store the time as UTC + // https://github.com/msgpack/msgpack/blob/master/spec.md#timestamp-extension-type + Assert.Equal(dateTime.ToUniversalTime(), resultDateTime); + } + finally + { + MemoryBufferWriter.Return(writer); + } } - // Test Data for Parse/WriteMessages: - // * Name: A string name that is used when reporting the test (it's the ToString value for ProtocolTestData) - // * Message: The HubMessage that is either expected (in Parse) or used as input (in Write) - // * Binary: Base64-encoded binary "baseline" to sanity-check MessagePack-CSharp behavior - // - // When changing the tests/message pack parsing if you get test failures look at the base64 encoding and - // use a tool like https://sugendran.github.io/msgpack-visualizer/ to verify that the MsgPack is correct and then just replace the Base64 value. + [Fact] + public void WriteAndParseDateTimeOffset() + { + var dateTimeOffset = new DateTimeOffset(new DateTime(2018, 4, 9), TimeSpan.FromHours(10)); + var writer = MemoryBufferWriter.Get(); + + try + { + HubProtocol.WriteMessage(CompletionMessage.WithResult("xyz", dateTimeOffset), writer); + var bytes = new ReadOnlySequence(writer.ToArray()); + HubProtocol.TryParseMessage(ref bytes, new TestBinder(typeof(DateTimeOffset)), out var hubMessage); + + var completionMessage = Assert.IsType(hubMessage); + + var resultDateTimeOffset = (DateTimeOffset)completionMessage.Result; + Assert.Equal(dateTimeOffset, resultDateTimeOffset); + } + finally + { + MemoryBufferWriter.Return(writer); + } + } public static IEnumerable TestDataNames { @@ -52,29 +86,36 @@ public static IEnumerable TestDataNames } } - public static IDictionary TestData => new[] + // TestData that requires object serialization + public static IDictionary TestData => new[] { - // Invocation messages + // Completion messages new ProtocolTestData( - name: "InvocationWithNoHeadersAndNoArgs", - message: new InvocationMessage("xyz", "method", Array.Empty()), - binary: "lgGAo3h5eqZtZXRob2SQkA=="), + name: "CompletionWithNoHeadersAndNullResult", + message: CompletionMessage.WithResult("xyz", payload: null), + binary: "lQOAo3h5egPA"), + new ProtocolTestData( + name: "CompletionWithNoHeadersAndCustomObjectResult", + message: CompletionMessage.WithResult("xyz", payload: new CustomObject()), + binary: "lQOAo3h5egOGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgM="), + new ProtocolTestData( + name: "CompletionWithNoHeadersAndCustomObjectArrayResult", + message: CompletionMessage.WithResult("xyz", payload: new[] { new CustomObject(), new CustomObject() }), + binary: "lQOAo3h5egOShqpTdHJpbmdQcm9wqFNpZ25hbFIhqkRvdWJsZVByb3DLQBkh+1RCzxKnSW50UHJvcCqsRGF0ZVRpbWVQcm9w1v9Y7ByAqE51bGxQcm9wwKtCeXRlQXJyUHJvcMQDAQIDhqpTdHJpbmdQcm9wqFNpZ25hbFIhqkRvdWJsZVByb3DLQBkh+1RCzxKnSW50UHJvcCqsRGF0ZVRpbWVQcm9w1v9Y7ByAqE51bGxQcm9wwKtCeXRlQXJyUHJvcMQDAQID"), + new ProtocolTestData( + name: "CompletionWithHeadersAndCustomObjectArrayResult", + message: AddHeaders(TestHeaders, CompletionMessage.WithResult("xyz", payload: new[] { new CustomObject(), new CustomObject() })), + binary: "lQODo0Zvb6NCYXKyS2V5V2l0aApOZXcNCkxpbmVzq1N0aWxsIFdvcmtzsVZhbHVlV2l0aE5ld0xpbmVzsEFsc28KV29ya3MNCkZpbmWjeHl6A5KGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgOGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgM="), new ProtocolTestData( - name: "InvocationWithNoHeadersNoIdAndNoArgs", - message: new InvocationMessage("method", Array.Empty()), - binary: "lgGAwKZtZXRob2SQkA=="), + name: "CompletionWithNoHeadersAndEnumResult", + message: CompletionMessage.WithResult("xyz", payload: TestEnum.One), + binary: "lQOAo3h5egOjT25l"), + + // Invocation messages new ProtocolTestData( name: "InvocationWithNoHeadersNoIdAndSingleNullArg", message: new InvocationMessage("method", new object[] { null }), binary: "lgGAwKZtZXRob2SRwJA="), - new ProtocolTestData( - name: "InvocationWithNoHeadersNoIdAndSingleIntArg", - message: new InvocationMessage("method", new object[] { 42 }), - binary: "lgGAwKZtZXRob2SRKpA="), - new ProtocolTestData( - name: "InvocationWithNoHeadersNoIdIntAndStringArgs", - message: new InvocationMessage("method", new object[] { 42, "string" }), - binary: "lgGAwKZtZXRob2SSKqZzdHJpbmeQ"), new ProtocolTestData( name: "InvocationWithNoHeadersNoIdIntAndEnumArgs", message: new InvocationMessage("method", new object[] { 42, TestEnum.One }), @@ -91,40 +132,12 @@ public static IEnumerable TestDataNames name: "InvocationWithHeadersNoIdAndArrayOfCustomObjectArgs", message: AddHeaders(TestHeaders, new InvocationMessage("method", new object[] { new CustomObject(), new CustomObject() })), binary: "lgGDo0Zvb6NCYXKyS2V5V2l0aApOZXcNCkxpbmVzq1N0aWxsIFdvcmtzsVZhbHVlV2l0aE5ld0xpbmVzsEFsc28KV29ya3MNCkZpbmXApm1ldGhvZJKGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgOGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgOQ"), - new ProtocolTestData( - name: "InvocationWithStreamArgument", - message: new InvocationMessage(null, "Target", Array.Empty(), new string[] { "__test_id__" }), - binary: "lgGAwKZUYXJnZXSQkatfX3Rlc3RfaWRfXw=="), - new ProtocolTestData( - name: "InvocationWithStreamAndNormalArgument", - message: new InvocationMessage(null, "Target", new object[] { 42 }, new string[] { "__test_id__" }), - binary: "lgGAwKZUYXJnZXSRKpGrX190ZXN0X2lkX18="), - new ProtocolTestData( - name: "InvocationWithMulitpleStreams", - message: new InvocationMessage(null, "Target", Array.Empty(), new string[] { "__test_id__", "__test_id2__" }), - binary: "lgGAwKZUYXJnZXSQkqtfX3Rlc3RfaWRfX6xfX3Rlc3RfaWQyX18="), // StreamItem Messages new ProtocolTestData( name: "StreamItemWithNoHeadersAndNullItem", message: new StreamItemMessage("xyz", item: null), binary: "lAKAo3h5esA="), - new ProtocolTestData( - name: "StreamItemWithNoHeadersAndIntItem", - message: new StreamItemMessage("xyz", item: 42), - binary: "lAKAo3h5eio="), - new ProtocolTestData( - name: "StreamItemWithNoHeadersAndFloatItem", - message: new StreamItemMessage("xyz", item: 42.0f), - binary: "lAKAo3h5espCKAAA"), - new ProtocolTestData( - name: "StreamItemWithNoHeadersAndStringItem", - message: new StreamItemMessage("xyz", item: "string"), - binary: "lAKAo3h5eqZzdHJpbmc="), - new ProtocolTestData( - name: "StreamItemWithNoHeadersAndBoolItem", - message: new StreamItemMessage("xyz", item: true), - binary: "lAKAo3h5esM="), new ProtocolTestData( name: "StreamItemWithNoHeadersAndEnumItem", message: new StreamItemMessage("xyz", item: TestEnum.One), @@ -142,89 +155,15 @@ public static IEnumerable TestDataNames message: AddHeaders(TestHeaders, new StreamItemMessage("xyz", item: new[] { new CustomObject(), new CustomObject() })), binary: "lAKDo0Zvb6NCYXKyS2V5V2l0aApOZXcNCkxpbmVzq1N0aWxsIFdvcmtzsVZhbHVlV2l0aE5ld0xpbmVzsEFsc28KV29ya3MNCkZpbmWjeHl6koaqU3RyaW5nUHJvcKhTaWduYWxSIapEb3VibGVQcm9wy0AZIftUQs8Sp0ludFByb3AqrERhdGVUaW1lUHJvcNb/WOwcgKhOdWxsUHJvcMCrQnl0ZUFyclByb3DEAwECA4aqU3RyaW5nUHJvcKhTaWduYWxSIapEb3VibGVQcm9wy0AZIftUQs8Sp0ludFByb3AqrERhdGVUaW1lUHJvcNb/WOwcgKhOdWxsUHJvcMCrQnl0ZUFyclByb3DEAwECAw=="), - // Completion Messages - new ProtocolTestData( - name: "CompletionWithNoHeadersAndError", - message: CompletionMessage.WithError("xyz", error: "Error not found!"), - binary: "lQOAo3h5egGwRXJyb3Igbm90IGZvdW5kIQ=="), - new ProtocolTestData( - name: "CompletionWithHeadersAndError", - message: AddHeaders(TestHeaders, CompletionMessage.WithError("xyz", error: "Error not found!")), - binary: "lQODo0Zvb6NCYXKyS2V5V2l0aApOZXcNCkxpbmVzq1N0aWxsIFdvcmtzsVZhbHVlV2l0aE5ld0xpbmVzsEFsc28KV29ya3MNCkZpbmWjeHl6AbBFcnJvciBub3QgZm91bmQh"), - new ProtocolTestData( - name: "CompletionWithNoHeadersAndNoResult", - message: CompletionMessage.Empty("xyz"), - binary: "lAOAo3h5egI="), - new ProtocolTestData( - name: "CompletionWithHeadersAndNoResult", - message: AddHeaders(TestHeaders, CompletionMessage.Empty("xyz")), - binary: "lAODo0Zvb6NCYXKyS2V5V2l0aApOZXcNCkxpbmVzq1N0aWxsIFdvcmtzsVZhbHVlV2l0aE5ld0xpbmVzsEFsc28KV29ya3MNCkZpbmWjeHl6Ag=="), - new ProtocolTestData( - name: "CompletionWithNoHeadersAndNullResult", - message: CompletionMessage.WithResult("xyz", payload: null), - binary: "lQOAo3h5egPA"), - new ProtocolTestData( - name: "CompletionWithNoHeadersAndIntResult", - message: CompletionMessage.WithResult("xyz", payload: 42), - binary: "lQOAo3h5egMq"), - new ProtocolTestData( - name: "CompletionWithNoHeadersAndEnumResult", - message: CompletionMessage.WithResult("xyz", payload: TestEnum.One), - binary: "lQOAo3h5egOjT25l"), - new ProtocolTestData( - name: "CompletionWithNoHeadersAndFloatResult", - message: CompletionMessage.WithResult("xyz", payload: 42.0f), - binary: "lQOAo3h5egPKQigAAA=="), - new ProtocolTestData( - name: "CompletionWithNoHeadersAndStringResult", - message: CompletionMessage.WithResult("xyz", payload: "string"), - binary: "lQOAo3h5egOmc3RyaW5n"), - new ProtocolTestData( - name: "CompletionWithNoHeadersAndBooleanResult", - message: CompletionMessage.WithResult("xyz", payload: true), - binary: "lQOAo3h5egPD"), - new ProtocolTestData( - name: "CompletionWithNoHeadersAndCustomObjectResult", - message: CompletionMessage.WithResult("xyz", payload: new CustomObject()), - binary: "lQOAo3h5egOGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgM="), - new ProtocolTestData( - name: "CompletionWithNoHeadersAndCustomObjectArrayResult", - message: CompletionMessage.WithResult("xyz", payload: new[] { new CustomObject(), new CustomObject() }), - binary: "lQOAo3h5egOShqpTdHJpbmdQcm9wqFNpZ25hbFIhqkRvdWJsZVByb3DLQBkh+1RCzxKnSW50UHJvcCqsRGF0ZVRpbWVQcm9w1v9Y7ByAqE51bGxQcm9wwKtCeXRlQXJyUHJvcMQDAQIDhqpTdHJpbmdQcm9wqFNpZ25hbFIhqkRvdWJsZVByb3DLQBkh+1RCzxKnSW50UHJvcCqsRGF0ZVRpbWVQcm9w1v9Y7ByAqE51bGxQcm9wwKtCeXRlQXJyUHJvcMQDAQID"), - new ProtocolTestData( - name: "CompletionWithHeadersAndCustomObjectArrayResult", - message: AddHeaders(TestHeaders, CompletionMessage.WithResult("xyz", payload: new[] { new CustomObject(), new CustomObject() })), - binary: "lQODo0Zvb6NCYXKyS2V5V2l0aApOZXcNCkxpbmVzq1N0aWxsIFdvcmtzsVZhbHVlV2l0aE5ld0xpbmVzsEFsc28KV29ya3MNCkZpbmWjeHl6A5KGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgOGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgM="), - // StreamInvocation Messages - new ProtocolTestData( - name: "StreamInvocationWithNoHeadersAndNoArgs", - message: new StreamInvocationMessage("xyz", "method", Array.Empty()), - binary: "lgSAo3h5eqZtZXRob2SQkA=="), - new ProtocolTestData( - name: "StreamInvocationWithNoHeadersAndNullArg", - message: new StreamInvocationMessage("xyz", "method", new object[] { null }), - binary: "lgSAo3h5eqZtZXRob2SRwJA="), - new ProtocolTestData( - name: "StreamInvocationWithNoHeadersAndIntArg", - message: new StreamInvocationMessage("xyz", "method", new object[] { 42 }), - binary: "lgSAo3h5eqZtZXRob2SRKpA="), new ProtocolTestData( name: "StreamInvocationWithNoHeadersAndEnumArg", message: new StreamInvocationMessage("xyz", "method", new object[] { TestEnum.One }), binary: "lgSAo3h5eqZtZXRob2SRo09uZZA="), new ProtocolTestData( - name: "StreamInvocationWithStreamArgument", - message: new StreamInvocationMessage("xyz", "method", Array.Empty(), new string[] { "__test_id__" }), - binary: "lgSAo3h5eqZtZXRob2SQkatfX3Rlc3RfaWRfXw=="), - new ProtocolTestData( - name: "StreamInvocationWithStreamAndNormalArgument", - message: new StreamInvocationMessage("xyz", "method", new object[] { 42 }, new string[] { "__test_id__" }), - binary: "lgSAo3h5eqZtZXRob2SRKpGrX190ZXN0X2lkX18="), - new ProtocolTestData( - name: "StreamInvocationWithNoHeadersAndIntAndStringArgs", - message: new StreamInvocationMessage("xyz", "method", new object[] { 42, "string" }), - binary: "lgSAo3h5eqZtZXRob2SSKqZzdHJpbmeQ"), + name: "StreamInvocationWithNoHeadersAndNullArg", + message: new StreamInvocationMessage("xyz", "method", new object[] { null }), + binary: "lgSAo3h5eqZtZXRob2SRwJA="), new ProtocolTestData( name: "StreamInvocationWithNoHeadersAndIntStringAndCustomObjectArgs", message: new StreamInvocationMessage("xyz", "method", new object[] { 42, "string", new CustomObject() }), @@ -237,22 +176,6 @@ public static IEnumerable TestDataNames name: "StreamInvocationWithHeadersAndCustomObjectArrayArg", message: AddHeaders(TestHeaders, new StreamInvocationMessage("xyz", "method", new object[] { new CustomObject(), new CustomObject() })), binary: "lgSDo0Zvb6NCYXKyS2V5V2l0aApOZXcNCkxpbmVzq1N0aWxsIFdvcmtzsVZhbHVlV2l0aE5ld0xpbmVzsEFsc28KV29ya3MNCkZpbmWjeHl6pm1ldGhvZJKGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgOGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgOQ"), - - // CancelInvocation Messages - new ProtocolTestData( - name: "CancelInvocationWithNoHeaders", - message: new CancelInvocationMessage("xyz"), - binary: "kwWAo3h5eg=="), - new ProtocolTestData( - name: "CancelInvocationWithHeaders", - message: AddHeaders(TestHeaders, new CancelInvocationMessage("xyz")), - binary: "kwWDo0Zvb6NCYXKyS2V5V2l0aApOZXcNCkxpbmVzq1N0aWxsIFdvcmtzsVZhbHVlV2l0aE5ld0xpbmVzsEFsc28KV29ya3MNCkZpbmWjeHl6"), - - // Ping Messages - new ProtocolTestData( - name: "Ping", - message: PingMessage.Instance, - binary: "kQY="), }.ToDictionary(t => t.Name); [Theory] @@ -261,41 +184,7 @@ public void ParseMessages(string testDataName) { var testData = TestData[testDataName]; - // Verify that the input binary string decodes to the expected MsgPack primitives - var bytes = Convert.FromBase64String(testData.Binary); - - // Parse the input fully now. - bytes = Frame(bytes); - var data = new ReadOnlySequence(bytes); - Assert.True(_hubProtocol.TryParseMessage(ref data, new TestBinder(testData.Message), out var message)); - - Assert.NotNull(message); - Assert.Equal(testData.Message, message, TestHubMessageEqualityComparer.Instance); - } - - [Fact] - public void ParseMessageWithExtraData() - { - var expectedMessage = new InvocationMessage("xyz", "method", Array.Empty()); - - // Verify that the input binary string decodes to the expected MsgPack primitives - var bytes = new byte[] { ArrayBytes(8), - 1, - 0x80, - StringBytes(3), (byte)'x', (byte)'y', (byte)'z', - StringBytes(6), (byte)'m', (byte)'e', (byte)'t', (byte)'h', (byte)'o', (byte)'d', - ArrayBytes(0), // Arguments - ArrayBytes(0), // Streams - 0xc3, - StringBytes(2), (byte)'e', (byte)'x' }; - - // Parse the input fully now. - bytes = Frame(bytes); - var data = new ReadOnlySequence(bytes); - Assert.True(_hubProtocol.TryParseMessage(ref data, new TestBinder(expectedMessage), out var message)); - - Assert.NotNull(message); - Assert.Equal(expectedMessage, message, TestHubMessageEqualityComparer.Instance); + TestParseMessages(testData); } [Theory] @@ -304,253 +193,7 @@ public void WriteMessages(string testDataName) { var testData = TestData[testDataName]; - var bytes = Write(testData.Message); - - // Unframe the message to check the binary encoding - var byteSpan = new ReadOnlySequence(bytes); - Assert.True(BinaryMessageParser.TryParseMessage(ref byteSpan, out var unframed)); - - // Check the baseline binary encoding, use Assert.True in order to configure the error message - var actual = Convert.ToBase64String(unframed.ToArray()); - Assert.True(string.Equals(actual, testData.Binary, StringComparison.Ordinal), $"Binary encoding changed from{Environment.NewLine} [{testData.Binary}]{Environment.NewLine} to{Environment.NewLine} [{actual}]{Environment.NewLine}Please verify the MsgPack output and update the baseline"); - } - - [Fact] - public void WriteAndParseDateTimeConvertsToUTC() - { - var dateTime = new DateTime(2018, 4, 9); - var writer = MemoryBufferWriter.Get(); - - try - { - _hubProtocol.WriteMessage(CompletionMessage.WithResult("xyz", dateTime), writer); - var bytes = new ReadOnlySequence(writer.ToArray()); - _hubProtocol.TryParseMessage(ref bytes, new TestBinder(typeof(DateTime)), out var hubMessage); - - var completionMessage = Assert.IsType(hubMessage); - - var resultDateTime = (DateTime)completionMessage.Result; - // The messagepack Timestamp format specifies that time is stored as seconds since 1970-01-01 UTC - // so the library has no choice but to store the time as UTC - // https://github.com/msgpack/msgpack/blob/master/spec.md#timestamp-extension-type - Assert.Equal(dateTime.ToUniversalTime(), resultDateTime); - } - finally - { - MemoryBufferWriter.Return(writer); - } - } - - [Fact] - public void WriteAndParseDateTimeOffset() - { - var dateTimeOffset = new DateTimeOffset(new DateTime(2018, 4, 9), TimeSpan.FromHours(10)); - var writer = MemoryBufferWriter.Get(); - - try - { - _hubProtocol.WriteMessage(CompletionMessage.WithResult("xyz", dateTimeOffset), writer); - var bytes = new ReadOnlySequence(writer.ToArray()); - _hubProtocol.TryParseMessage(ref bytes, new TestBinder(typeof(DateTimeOffset)), out var hubMessage); - - var completionMessage = Assert.IsType(hubMessage); - - var resultDateTimeOffset = (DateTimeOffset)completionMessage.Result; - Assert.Equal(dateTimeOffset, resultDateTimeOffset); - } - finally - { - MemoryBufferWriter.Return(writer); - } - } - - public static IDictionary InvalidPayloads => new[] - { - // Message Type - new InvalidMessageData("MessageTypeString", new byte[] { 0x91, 0xa3, (byte)'f', (byte)'o', (byte)'o' }, "Reading 'messageType' as Int32 failed."), - - // Headers - new InvalidMessageData("HeadersNotAMap", new byte[] { 0x92, 1, 0xa3, (byte)'f', (byte)'o', (byte)'o' }, "Reading map length for 'headers' failed."), - new InvalidMessageData("HeaderKeyInt", new byte[] { 0x92, 1, 0x82, 0x2a, 0xa3, (byte)'f', (byte)'o', (byte)'o' }, "Reading 'headers[0].Key' as String failed."), - new InvalidMessageData("HeaderValueInt", new byte[] { 0x92, 1, 0x82, 0xa3, (byte)'f', (byte)'o', (byte)'o', 42 }, "Reading 'headers[0].Value' as String failed."), - new InvalidMessageData("HeaderKeyArray", new byte[] { 0x92, 1, 0x84, 0xa3, (byte)'f', (byte)'o', (byte)'o', 0xa3, (byte)'f', (byte)'o', (byte)'o', 0x90, 0xa3, (byte)'f', (byte)'o', (byte)'o' }, "Reading 'headers[1].Key' as String failed."), - new InvalidMessageData("HeaderValueArray", new byte[] { 0x92, 1, 0x84, 0xa3, (byte)'f', (byte)'o', (byte)'o', 0xa3, (byte)'f', (byte)'o', (byte)'o', 0xa3, (byte)'f', (byte)'o', (byte)'o', 0x90 }, "Reading 'headers[1].Value' as String failed."), - - // InvocationMessage - new InvalidMessageData("InvocationMissingId", new byte[] { 0x92, 1, 0x80 }, "Reading 'invocationId' as String failed."), - new InvalidMessageData("InvocationIdBoolean", new byte[] { 0x91, 1, 0x80, 0xc2 }, "Reading 'invocationId' as String failed."), - new InvalidMessageData("InvocationTargetMissing", new byte[] { 0x93, 1, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c' }, "Reading 'target' as String failed."), - new InvalidMessageData("InvocationTargetInt", new byte[] { 0x94, 1, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 42 }, "Reading 'target' as String failed."), - - // StreamInvocationMessage - new InvalidMessageData("StreamInvocationMissingId", new byte[] { 0x92, 4, 0x80 }, "Reading 'invocationId' as String failed."), - new InvalidMessageData("StreamInvocationIdBoolean", new byte[] { 0x93, 4, 0x80, 0xc2 }, "Reading 'invocationId' as String failed."), - new InvalidMessageData("StreamInvocationTargetMissing", new byte[] { 0x93, 4, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c' }, "Reading 'target' as String failed."), - new InvalidMessageData("StreamInvocationTargetInt", new byte[] { 0x94, 4, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 42 }, "Reading 'target' as String failed."), - - // StreamItemMessage - new InvalidMessageData("StreamItemMissingId", new byte[] { 0x92, 2, 0x80 }, "Reading 'invocationId' as String failed."), - new InvalidMessageData("StreamItemInvocationIdBoolean", new byte[] { 0x93, 2, 0x80, 0xc2 }, "Reading 'invocationId' as String failed."), - new InvalidMessageData("StreamItemMissing", new byte[] { 0x93, 2, 0x80, 0xa3, (byte)'x', (byte)'y', (byte)'z' }, "Deserializing object of the `String` type for 'item' failed."), - new InvalidMessageData("StreamItemTypeMismatch", new byte[] { 0x94, 2, 0x80, 0xa3, (byte)'x', (byte)'y', (byte)'z', 42 }, "Deserializing object of the `String` type for 'item' failed."), - - // CompletionMessage - new InvalidMessageData("CompletionMissingId", new byte[] { 0x92, 3, 0x80 }, "Reading 'invocationId' as String failed."), - new InvalidMessageData("CompletionIdBoolean", new byte[] { 0x93, 3, 0x80, 0xc2 }, "Reading 'invocationId' as String failed."), - new InvalidMessageData("CompletionResultKindString", new byte[] { 0x94, 3, 0x80, 0xa3, (byte)'x', (byte)'y', (byte)'z', 0xa3, (byte)'x', (byte)'y', (byte)'z' }, "Reading 'resultKind' as Int32 failed."), - new InvalidMessageData("CompletionResultKindOutOfRange", new byte[] { 0x94, 3, 0x80, 0xa3, (byte)'x', (byte)'y', (byte)'z', 42 }, "Invalid invocation result kind."), - new InvalidMessageData("CompletionErrorMissing", new byte[] { 0x94, 3, 0x80, 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x01 }, "Reading 'error' as String failed."), - new InvalidMessageData("CompletionErrorInt", new byte[] { 0x95, 3, 0x80, 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x01, 42 }, "Reading 'error' as String failed."), - new InvalidMessageData("CompletionResultMissing", new byte[] { 0x94, 3, 0x80, 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x03 }, "Deserializing object of the `String` type for 'argument' failed."), - new InvalidMessageData("CompletionResultTypeMismatch", new byte[] { 0x95, 3, 0x80, 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x03, 42 }, "Deserializing object of the `String` type for 'argument' failed."), - }.ToDictionary(t => t.Name); - - public static IEnumerable InvalidPayloadNames => InvalidPayloads.Keys.Select(name => new object[] { name }); - - [Theory] - [MemberData(nameof(InvalidPayloadNames))] - public void ParserThrowsForInvalidMessages(string invalidPayloadName) - { - var testData = InvalidPayloads[invalidPayloadName]; - - var buffer = Frame(testData.Encoded); - var binder = new TestBinder(new[] { typeof(string) }, typeof(string)); - var data = new ReadOnlySequence(buffer); - var exception = Assert.Throws(() => _hubProtocol.TryParseMessage(ref data, binder, out _)); - - Assert.Equal(testData.ErrorMessage, exception.Message); - } - - public static IDictionary ArgumentBindingErrors => new[] - { - // InvocationMessage - new InvalidMessageData("InvocationArgumentArrayMissing", new byte[] { 0x94, 1, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z' }, "Reading array length for 'arguments' failed."), - new InvalidMessageData("InvocationArgumentArrayNotAnArray", new byte[] { 0x95, 1, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z', 42 }, "Reading array length for 'arguments' failed."), - new InvalidMessageData("InvocationArgumentArraySizeMismatchEmpty", new byte[] { 0x95, 1, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x90 }, "Invocation provides 0 argument(s) but target expects 1."), - new InvalidMessageData("InvocationArgumentArraySizeMismatchTooLarge", new byte[] { 0x95, 1, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x92, 0xa1, (byte)'a', 0xa1, (byte)'b' }, "Invocation provides 2 argument(s) but target expects 1."), - new InvalidMessageData("InvocationArgumentTypeMismatch", new byte[] { 0x95, 1, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x91, 42 }, "Error binding arguments. Make sure that the types of the provided values match the types of the hub method being invoked."), - - // StreamInvocationMessage - new InvalidMessageData("StreamInvocationArgumentArrayMissing", new byte[] { 0x94, 4, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z' }, "Reading array length for 'arguments' failed."), // array is missing - new InvalidMessageData("StreamInvocationArgumentArrayNotAnArray", new byte[] { 0x95, 4, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z', 42 }, "Reading array length for 'arguments' failed."), // arguments isn't an array - new InvalidMessageData("StreamInvocationArgumentArraySizeMismatchEmpty", new byte[] { 0x95, 4, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x90 }, "Invocation provides 0 argument(s) but target expects 1."), // array is missing elements - new InvalidMessageData("StreamInvocationArgumentArraySizeMismatchTooLarge", new byte[] { 0x95, 4, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x92, 0xa1, (byte)'a', 0xa1, (byte)'b' }, "Invocation provides 2 argument(s) but target expects 1."), // argument count does not match binder argument count - new InvalidMessageData("StreamInvocationArgumentTypeMismatch", new byte[] { 0x95, 4, 0x80, 0xa3, (byte)'a', (byte)'b', (byte)'c', 0xa3, (byte)'x', (byte)'y', (byte)'z', 0x91, 42 }, "Error binding arguments. Make sure that the types of the provided values match the types of the hub method being invoked."), // argument type mismatch - }.ToDictionary(t => t.Name); - - public static IEnumerable ArgumentBindingErrorNames => ArgumentBindingErrors.Keys.Select(name => new object[] { name }); - - [Theory] - [MemberData(nameof(ArgumentBindingErrorNames))] - public void GettingArgumentsThrowsIfBindingFailed(string argumentBindingErrorName) - { - var testData = ArgumentBindingErrors[argumentBindingErrorName]; - - var buffer = Frame(testData.Encoded); - var binder = new TestBinder(new[] { typeof(string) }, typeof(string)); - var data = new ReadOnlySequence(buffer); - _hubProtocol.TryParseMessage(ref data, binder, out var message); - var bindingFailure = Assert.IsType(message); - Assert.Equal(testData.ErrorMessage, bindingFailure.BindingFailure.SourceException.Message); - } - - [Theory] - [InlineData(new byte[] { 0x05, 0x01 })] - public void ParserDoesNotConsumePartialData(byte[] payload) - { - var binder = new TestBinder(new[] { typeof(string) }, typeof(string)); - var data = new ReadOnlySequence(payload); - var result = _hubProtocol.TryParseMessage(ref data, binder, out var message); - Assert.Null(message); - } - - [Fact] - public void SerializerCanSerializeTypesWithNoDefaultCtor() - { - var result = Write(CompletionMessage.WithResult("0", new List { 42 }.AsReadOnly())); - AssertMessages(new byte[] { ArrayBytes(5), 3, 0x80, StringBytes(1), (byte)'0', 0x03, ArrayBytes(1), 42 }, result); - } - - private byte ArrayBytes(int size) - { - Debug.Assert(size < 16, "Test code doesn't support array sizes greater than 15"); - - return (byte)(0x90 | size); - } - - private byte StringBytes(int size) - { - Debug.Assert(size < 16, "Test code doesn't support string sizes greater than 15"); - - return (byte)(0xa0 | size); - } - - private static void AssertMessages(byte[] expectedOutput, ReadOnlyMemory bytes) - { - var data = new ReadOnlySequence(bytes); - Assert.True(BinaryMessageParser.TryParseMessage(ref data, out var message)); - Assert.Equal(expectedOutput, message.ToArray()); - } - - private static byte[] Frame(byte[] input) - { - var stream = MemoryBufferWriter.Get(); - try - { - BinaryMessageFormatter.WriteLengthPrefix(input.Length, stream); - stream.Write(input); - return stream.ToArray(); - } - finally - { - MemoryBufferWriter.Return(stream); - } - } - - private static byte[] Write(HubMessage message) - { - var writer = MemoryBufferWriter.Get(); - try - { - _hubProtocol.WriteMessage(message, writer); - return writer.ToArray(); - } - finally - { - MemoryBufferWriter.Return(writer); - } - } - - public class InvalidMessageData - { - public string Name { get; private set; } - public byte[] Encoded { get; private set; } - public string ErrorMessage { get; private set; } - - public InvalidMessageData(string name, byte[] encoded, string errorMessage) - { - Name = name; - Encoded = encoded; - ErrorMessage = errorMessage; - } - - public override string ToString() => Name; - } - - public class ProtocolTestData - { - public string Name { get; } - public string Binary { get; } - public HubMessage Message { get; } - - public ProtocolTestData(string name, HubMessage message, string binary) - { - Name = name; - Message = message; - Binary = binary; - } - - public override string ToString() => Name; + TestWriteMessages(testData); } } } diff --git a/src/submodules/MessagePack-CSharp b/src/submodules/MessagePack-CSharp new file mode 160000 index 000000000000..f2dc12bf749e --- /dev/null +++ b/src/submodules/MessagePack-CSharp @@ -0,0 +1 @@ +Subproject commit f2dc12bf749e6f708563ac5175c772c025344ebc