Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 68 additions & 4 deletions src/StreamJsonRpc/MessagePackFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,17 @@ private interface IJsonRpcMessagePackRetention
set => base.MultiplexingStream = value;
}

/// <summary>
/// Gets a value indicating whether the W3C <c>traceparent</c> property
/// should be serialized as a string instead of a more compact binary format.
/// </summary>
/// <value>The default value is <see langword="false"/>.</value>
public bool TraceParentAsW3CString { get; init; }

private IMessagePackFormatter<TraceParent> TraceParentFormatter => this.TraceParentAsW3CString
? TraceParentAsStringFormatter.Instance
: TraceParentAsBinaryFormatter.Instance;

/// <summary>
/// Sets the <see cref="MessagePackSerializerOptions"/> to use for serialization of user data.
/// </summary>
Expand Down Expand Up @@ -367,7 +378,7 @@ private IFormatterResolver CreateTopLevelMessageResolver()
new JsonRpcResultFormatter(this),
new JsonRpcErrorFormatter(this),
new JsonRpcErrorDetailFormatter(this),
new TraceParentFormatter(),
new TraceParentDelegatingFormatter(this),
};
var resolvers = new IFormatterResolver[]
{
Expand Down Expand Up @@ -1527,7 +1538,7 @@ public Protocol.JsonRpcRequest Deserialize(ref MessagePackReader reader, Message
}
else if (TraceParentPropertyName.TryRead(stringKey))
{
TraceParent traceParent = options.Resolver.GetFormatterWithVerify<TraceParent>().Deserialize(ref reader, options);
TraceParent traceParent = this.formatter.TraceParentFormatter.Deserialize(ref reader, options);
result.TraceParent = traceParent.ToString();
}
else if (TraceStatePropertyName.TryRead(stringKey))
Expand Down Expand Up @@ -1620,7 +1631,7 @@ public void Serialize(ref MessagePackWriter writer, Protocol.JsonRpcRequest valu
if (value.TraceParent?.Length > 0)
{
TraceParentPropertyName.Write(ref writer);
options.Resolver.GetFormatterWithVerify<TraceParent>().Serialize(ref writer, new TraceParent(value.TraceParent), options);
this.formatter.TraceParentFormatter.Serialize(ref writer, new TraceParent(value.TraceParent), options);

if (value.TraceState?.Length > 0)
{
Expand Down Expand Up @@ -1933,8 +1944,23 @@ public EventArgs Deserialize(ref MessagePackReader reader, MessagePackSerializer
}
}

private class TraceParentFormatter : IMessagePackFormatter<TraceParent>
private class TraceParentDelegatingFormatter(MessagePackFormatter formatter) : IMessagePackFormatter<TraceParent>
{
public TraceParent Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
{
return formatter.TraceParentFormatter.Deserialize(ref reader, options);
}

public void Serialize(ref MessagePackWriter writer, TraceParent value, MessagePackSerializerOptions options)
{
formatter.TraceParentFormatter.Serialize(ref writer, value, options);
}
}

private class TraceParentAsBinaryFormatter : IMessagePackFormatter<TraceParent>
{
internal static readonly TraceParentAsBinaryFormatter Instance = new();

public unsafe TraceParent Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
{
if (reader.ReadArrayHeader() != 2)
Expand Down Expand Up @@ -1983,6 +2009,44 @@ public unsafe void Serialize(ref MessagePackWriter writer, TraceParent value, Me
}
}

private class TraceParentAsStringFormatter : IMessagePackFormatter<TraceParent>
{
internal static readonly TraceParentAsStringFormatter Instance = new();

public TraceParent Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
{
ReadOnlySequence<byte> utf8Sequence = reader.ReadStringSequence() ?? throw new MessagePackSerializationException("Unexpected null value.");
if (utf8Sequence.Length != TraceParent.Length)
{
throw new MessagePackSerializationException("Unexpected length for traceparent string.");
}

Span<byte> utf8Bytes = stackalloc byte[TraceParent.Length];
utf8Sequence.CopyTo(utf8Bytes);

Span<char> chars = stackalloc char[TraceParent.Length];
if (!Encoding.UTF8.TryGetChars(utf8Bytes, chars, out int charsWritten))
{
throw new MessagePackSerializationException("Invalid UTF-8 in traceparent string.");
}

return new TraceParent(chars);
}
Comment thread
AArnott marked this conversation as resolved.

public void Serialize(ref MessagePackWriter writer, TraceParent value, MessagePackSerializerOptions options)
{
if (value.Version != 0)
{
throw new NotSupportedException("traceparent version " + value.Version + " is not supported.");
}

Span<char> chars = stackalloc char[TraceParent.Length];
value.WriteTo(chars);

writer.Write(chars.ToString());
}
}

private class TopLevelPropertyBag : TopLevelPropertyBagBase
{
private readonly MessagePackSerializerOptions serializerOptions;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Buffers;
using System.Text;
using System.Text.Json.Nodes;
using Nerdbank.MessagePack;
using PolyType;
Expand All @@ -14,8 +15,10 @@ namespace StreamJsonRpc;
/// </summary>
public partial class NerdbankMessagePackFormatter
{
internal class TraceParentConverter : MessagePackConverter<TraceParent>
internal class TraceParentAsBinaryConverter : MessagePackConverter<TraceParent>
{
internal static readonly TraceParentAsBinaryConverter Instance = new();

public unsafe override TraceParent Read(ref MessagePackReader reader, SerializationContext context)
{
context.DepthStep();
Expand Down Expand Up @@ -78,4 +81,47 @@ public unsafe override void Write(ref MessagePackWriter writer, in TraceParent v

public override JsonObject? GetJsonSchema(JsonSchemaContext context, ITypeShape typeShape) => null;
}

internal class TraceParentAsStringConverter : MessagePackConverter<TraceParent>
{
internal static readonly TraceParentAsStringConverter Instance = new();

public override TraceParent Read(ref MessagePackReader reader, SerializationContext context)
{
context.DepthStep();

ReadOnlySequence<byte> utf8Sequence = reader.ReadStringSequence() ?? throw new MessagePackSerializationException("Unexpected null value.");
if (utf8Sequence.Length != TraceParent.Length)
{
throw new MessagePackSerializationException("Unexpected length for traceparent string.");
}

Span<byte> utf8Bytes = stackalloc byte[TraceParent.Length];
utf8Sequence.CopyTo(utf8Bytes);

Span<char> chars = stackalloc char[TraceParent.Length];
if (!Encoding.UTF8.TryGetChars(utf8Bytes, chars, out int charsWritten))
{
throw new MessagePackSerializationException("Invalid UTF-8 in traceparent string.");
}

return new TraceParent(chars);
}
Comment thread
AArnott marked this conversation as resolved.

public override void Write(ref MessagePackWriter writer, in TraceParent value, SerializationContext context)
{
if (value.Version != 0)
{
throw new NotSupportedException("traceparent version " + value.Version + " is not supported.");
}

context.DepthStep();

Span<char> chars = stackalloc char[TraceParent.Length];
value.WriteTo(chars);
writer.Write(chars);
}

public override JsonObject? GetJsonSchema(JsonSchemaContext context, ITypeShape typeShape) => null;
}
}
13 changes: 11 additions & 2 deletions src/StreamJsonRpc/NerdbankMessagePackFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,15 @@ public MessagePackSerializer UserDataSerializer
}
}

/// <summary>
/// Gets a value indicating whether the W3C <c>traceparent</c> property
/// should be serialized as a string instead of a more compact binary format.
/// </summary>
/// <value>The default value is <see langword="false"/>.</value>
public bool TraceParentAsW3CString { get; init; }

private MessagePackConverter<TraceParent> TraceParentConverter => this.TraceParentAsW3CString ? TraceParentAsStringConverter.Instance : TraceParentAsBinaryConverter.Instance;

/// <inheritdoc/>
public JsonRpcMessage Deserialize(ReadOnlySequence<byte> contentBuffer)
{
Expand Down Expand Up @@ -470,7 +479,7 @@ internal class JsonRpcRequestConverter : MessagePackConverter<Protocol.JsonRpcRe
}
else if (TraceParentPropertyName.TryRead(ref reader))
{
TraceParent traceParent = context.GetConverter<TraceParent>(null).Read(ref reader, context);
TraceParent traceParent = formatter.TraceParentConverter.Read(ref reader, context);
result.TraceParent = traceParent.ToString();
}
else if (TraceStatePropertyName.TryRead(ref reader))
Expand Down Expand Up @@ -577,7 +586,7 @@ public override void Write(ref MessagePackWriter writer, in Protocol.JsonRpcRequ
if (value.TraceParent?.Length > 0)
{
writer.Write(TraceParentPropertyName);
context.GetConverter<TraceParent>(Witness.GeneratedTypeShapeProvider).Write(ref writer, new TraceParent(value.TraceParent), context);
formatter.TraceParentConverter.Write(ref writer, new TraceParent(value.TraceParent), context);

if (value.TraceState?.Length > 0)
{
Expand Down
12 changes: 0 additions & 12 deletions src/StreamJsonRpc/PolyfillMethods.cs

This file was deleted.

27 changes: 27 additions & 0 deletions src/StreamJsonRpc/Polyfills.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ namespace StreamJsonRpc;

internal static class Polyfills
{
#if NETSTANDARD2_0
internal static void Deconstruct<TKey, TValue>(this KeyValuePair<TKey, TValue> pair, out TKey key, out TValue value)
=> (key, value) = (pair.Key, pair.Value);
#endif

#if !(NETSTANDARD2_1_OR_GREATER || NET)
internal static unsafe string GetString(this Encoding encoding, ReadOnlySpan<byte> utf8Bytes)
{
Expand All @@ -16,4 +21,26 @@ internal static unsafe string GetString(this Encoding encoding, ReadOnlySpan<byt
}
}
#endif

#if !NET
internal static unsafe bool TryGetChars(this Encoding encoding, ReadOnlySpan<byte> utf8Bytes, Span<char> chars, out int charsWritten)
{
fixed (byte* pBytes = utf8Bytes)
{
fixed (char* pChars = chars)
{
try
{
charsWritten = encoding.GetChars(pBytes, utf8Bytes.Length, pChars, chars.Length);
return true;
}
catch (ArgumentException)
{
charsWritten = 0;
return false;
}
}
}
}
#endif
}
58 changes: 41 additions & 17 deletions src/StreamJsonRpc/Protocol/TraceParent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,24 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Diagnostics;
using Nerdbank.MessagePack;

namespace StreamJsonRpc.Protocol;

[MessagePackConverter(typeof(NerdbankMessagePackFormatter.TraceParentConverter))]
internal unsafe struct TraceParent
{
internal const int VersionByteCount = 1;
internal const int ParentIdByteCount = 8;
internal const int TraceIdByteCount = 16;
internal const int FlagsByteCount = 1;

/// <summary>
/// The number of characters in a serialized traceparent value.
/// </summary>
/// <devremarks>
/// When calculating the number of characters required, double each 'byte' we have to encode since we're using hex.
/// </devremarks>
internal const int Length = (VersionByteCount * 2) + 1 + (TraceIdByteCount * 2) + 1 + (ParentIdByteCount * 2) + 1 + (FlagsByteCount * 2);

internal byte Version;

internal fixed byte TraceId[TraceIdByteCount];
Expand All @@ -23,50 +29,55 @@ internal unsafe struct TraceParent
internal TraceFlags Flags;

internal TraceParent(string? traceparent)
: this(traceparent is null ? default : traceparent.AsSpan())
{
if (traceparent is null)
}

internal TraceParent(ReadOnlySpan<char> traceparent)
{
if (traceparent is [])
{
this.Version = 0;
this.Flags = TraceFlags.None;
return;
}

ReadOnlySpan<char> traceparentChars = traceparent.AsSpan();

// Decode version
ReadOnlySpan<char> slice = Consume(ref traceparentChars, VersionByteCount * 2);
ReadOnlySpan<char> slice = Consume(ref traceparent, VersionByteCount * 2);
fixed (byte* pVersion = &this.Version)
{
Hex.Decode(slice, new Span<byte>(pVersion, 1));
}

ConsumeHyphen(ref traceparentChars);
ConsumeHyphen(ref traceparent);

// Decode traceid
slice = Consume(ref traceparentChars, TraceIdByteCount * 2);
slice = Consume(ref traceparent, TraceIdByteCount * 2);
fixed (byte* pTraceId = this.TraceId)
{
Hex.Decode(slice, new Span<byte>(pTraceId, TraceIdByteCount));
}

ConsumeHyphen(ref traceparentChars);
ConsumeHyphen(ref traceparent);

// Decode parentid
slice = Consume(ref traceparentChars, ParentIdByteCount * 2);
slice = Consume(ref traceparent, ParentIdByteCount * 2);
fixed (byte* pParentId = this.ParentId)
{
Hex.Decode(slice, new Span<byte>(pParentId, ParentIdByteCount));
}

ConsumeHyphen(ref traceparentChars);
ConsumeHyphen(ref traceparent);

// Decode flags
slice = Consume(ref traceparentChars, FlagsByteCount * 2);
slice = Consume(ref traceparent, FlagsByteCount * 2);
fixed (TraceFlags* pFlags = &this.Flags)
{
Hex.Decode(slice, new Span<byte>(pFlags, 1));
}

Requires.Argument(traceparent is [], nameof(traceparent), "Expected traceparent to be fully consumed.");

static void ConsumeHyphen(ref ReadOnlySpan<char> value)
{
if (value[0] != '-')
Expand Down Expand Up @@ -112,9 +123,22 @@ internal Guid TraceIdGuid

public override string ToString()
{
// When calculating the number of characters required, double each 'byte' we have to encode since we're using hex.
Span<char> traceparent = stackalloc char[(VersionByteCount * 2) + 1 + (TraceIdByteCount * 2) + 1 + (ParentIdByteCount * 2) + 1 + (FlagsByteCount * 2)];
Span<char> traceParentRemaining = traceparent;
Span<char> chars = stackalloc char[Length];
this.WriteTo(chars);
return chars.ToString();
}

/// <summary>
/// Serializes the <see cref="TraceParent"/> value as a string.
/// </summary>
/// <param name="destination">The span to write to. This must be at least <see cref="Length"/> in length.</param>
/// <returns>The number of characters written to <paramref name="destination"/>. Always equal to <see cref="Length"/>.</returns>
/// <exception cref="ArgumentException">Thrown if <paramref name="destination"/> is shorter than <see cref="Length"/>.</exception>
internal int WriteTo(Span<char> destination)
{
Requires.Argument(destination.Length >= Length, nameof(destination), $"Destination must be at least {Length} characters in length.");

Span<char> traceParentRemaining = destination;
Comment thread
AArnott marked this conversation as resolved.

fixed (byte* pVersion = &this.Version)
{
Expand Down Expand Up @@ -142,9 +166,9 @@ public override string ToString()
Hex.Encode(new ReadOnlySpan<byte>(pFlags, 1), ref traceParentRemaining);
}

Debug.Assert(traceParentRemaining.Length == 0, "Characters were not initialized.");
Debug.Assert(traceParentRemaining is [], "Characters were not initialized.");

return traceparent.ToString();
return Length;

static void AddHyphen(ref Span<char> value)
{
Expand Down
Loading
Loading