Skip to content

Commit 4c8169b

Browse files
authored
Merge branch 'main' into tte/fix-semantic-non-null-with-interfaces
2 parents 5c357bd + c6c658d commit 4c8169b

File tree

2 files changed

+119
-15
lines changed

2 files changed

+119
-15
lines changed

src/HotChocolate/Core/src/Types/Types/Relay/Serialization/CompositeNodeIdValueSerializer.cs

Lines changed: 101 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ namespace HotChocolate.Types.Relay;
1616
public abstract class CompositeNodeIdValueSerializer<T> : INodeIdValueSerializer
1717
{
1818
private const byte _partSeparator = (byte)':';
19+
private const byte _escape = (byte)'\\';
1920
private static readonly Encoding _utf8 = Encoding.UTF8;
2021

2122
public virtual bool IsSupported(Type type) => type == typeof(T) || type == typeof(T?);
@@ -87,19 +88,21 @@ public NodeIdFormatterResult Format(Span<byte> buffer, object value, out int wri
8788
/// </returns>
8889
protected static bool TryFormatIdPart(Span<byte> buffer, string value, out int written)
8990
{
90-
var requiredCapacity = _utf8.GetByteCount(value) + 1;
91+
var requiredCapacity = _utf8.GetByteCount(value) * 2 + 1; // * 2 to allow for escaping.
9192
if (buffer.Length < requiredCapacity)
9293
{
9394
written = 0;
9495
return false;
9596
}
9697

97-
var stringBytes = buffer;
98-
Utf8GraphQLParser.ConvertToBytes(value, ref stringBytes);
98+
Span<byte> utf8Bytes = stackalloc byte[_utf8.GetByteCount(value)];
99+
_utf8.GetBytes(value, utf8Bytes);
99100

100-
buffer = buffer.Slice(stringBytes.Length);
101+
var bytesWritten = WriteEscapedBytes(utf8Bytes, buffer);
102+
103+
buffer = buffer[bytesWritten..];
101104
buffer[0] = _partSeparator;
102-
written = stringBytes.Length + 1;
105+
written = bytesWritten + 1;
103106
return true;
104107
}
105108

@@ -125,7 +128,8 @@ protected static bool TryFormatIdPart(Span<byte> buffer, Guid value, out int wri
125128
{
126129
if (compress)
127130
{
128-
if (buffer.Length < 17)
131+
const int requiredCapacity = 16 * 2 + 1; // * 2 to allow for escaping.
132+
if (buffer.Length < requiredCapacity)
129133
{
130134
written = 0;
131135
return false;
@@ -135,16 +139,17 @@ protected static bool TryFormatIdPart(Span<byte> buffer, Guid value, out int wri
135139
#pragma warning disable CS9191
136140
MemoryMarshal.TryWrite(span, ref value);
137141
#pragma warning restore CS9191
138-
span.CopyTo(buffer);
139-
buffer = buffer.Slice(16);
142+
var bytesWritten = WriteEscapedBytes(span, buffer);
143+
144+
buffer = buffer[bytesWritten..];
140145
buffer[0] = _partSeparator;
141-
written = 17;
146+
written = bytesWritten + 1;
142147
return true;
143148
}
144149

145150
if (Utf8Formatter.TryFormat(value, buffer, out written, format: 'N'))
146151
{
147-
buffer = buffer.Slice(written);
152+
buffer = buffer[written..];
148153
if (buffer.Length < 1)
149154
{
150155
return false;
@@ -344,8 +349,9 @@ protected static unsafe bool TryParseIdPart(
344349
[NotNullWhen(true)] out string? value,
345350
out int consumed)
346351
{
347-
var index = buffer.IndexOf(_partSeparator);
348-
var valueSpan = index == -1 ? buffer : buffer.Slice(0, index);
352+
var index = IndexOfPartSeparator(buffer);
353+
var valueSpan = index == -1 ? buffer : buffer[..index];
354+
valueSpan = Unescape(valueSpan);
349355
fixed (byte* b = valueSpan)
350356
{
351357
value = _utf8.GetString(b, valueSpan.Length);
@@ -379,11 +385,13 @@ protected static bool TryParseIdPart(
379385
out int consumed,
380386
bool compress = true)
381387
{
382-
var index = buffer.IndexOf(_partSeparator);
383-
var valueSpan = index == -1 ? buffer : buffer.Slice(0, index);
388+
var index = IndexOfPartSeparator(buffer);
389+
var valueSpan = index == -1 ? buffer : buffer[..index];
384390

385391
if (compress)
386392
{
393+
valueSpan = Unescape(valueSpan);
394+
387395
if (valueSpan.Length != 16)
388396
{
389397
value = default;
@@ -396,7 +404,7 @@ protected static bool TryParseIdPart(
396404
return true;
397405
}
398406

399-
if (Utf8Parser.TryParse(valueSpan, out Guid parsedValue, out _))
407+
if (Utf8Parser.TryParse(valueSpan, out Guid parsedValue, out _, standardFormat: 'N'))
400408
{
401409
value = parsedValue;
402410
consumed = index + 1;
@@ -547,4 +555,82 @@ protected static bool TryParseIdPart(
547555
consumed = 0;
548556
return false;
549557
}
558+
559+
/// <summary>
560+
/// Writes the given unescaped bytes with the part separator (<c>:</c>) escaped, into the given
561+
/// span.
562+
/// </summary>
563+
/// <param name="unescapedBytes">The unescaped bytes to write as escaped.</param>
564+
/// <param name="escapedBytes">The span into which the escaped bytes should be written.</param>
565+
/// <returns>The number of bytes written.</returns>
566+
private static int WriteEscapedBytes(ReadOnlySpan<byte> unescapedBytes, Span<byte> escapedBytes)
567+
{
568+
var index = 0;
569+
570+
foreach (var b in unescapedBytes)
571+
{
572+
if (b == _partSeparator)
573+
{
574+
escapedBytes[index++] = _escape;
575+
}
576+
577+
escapedBytes[index++] = b;
578+
}
579+
580+
return index;
581+
}
582+
583+
/// <summary>
584+
/// Unescapes part separators (<c>:</c>) in the given span of bytes.
585+
/// </summary>
586+
/// <param name="escapedBytes">A span with the bytes to be unescaped.</param>
587+
/// <returns>A span with the unescaped bytes.</returns>
588+
private static ReadOnlySpan<byte> Unescape(ReadOnlySpan<byte> escapedBytes)
589+
{
590+
Span<byte> unescapedBytes = new byte[escapedBytes.Length];
591+
592+
var index = 0;
593+
var skipNext = false;
594+
595+
for (var i = 0; i < escapedBytes.Length; i++)
596+
{
597+
if (skipNext)
598+
{
599+
skipNext = false;
600+
continue;
601+
}
602+
603+
if (escapedBytes[i] == _escape
604+
&& i + 1 < escapedBytes.Length
605+
&& escapedBytes[i + 1] == _partSeparator)
606+
{
607+
unescapedBytes[index++] = _partSeparator;
608+
skipNext = true;
609+
}
610+
else
611+
{
612+
unescapedBytes[index++] = escapedBytes[i];
613+
}
614+
}
615+
616+
return unescapedBytes[..index];
617+
}
618+
619+
/// <summary>
620+
/// Finds the index of the first non-escaped part separator (<c>:</c>) in the given buffer.
621+
/// </summary>
622+
/// <param name="buffer">The buffer to search.</param>
623+
/// <returns>The index of the non-escaped part separator.</returns>
624+
private static int IndexOfPartSeparator(ReadOnlySpan<byte> buffer)
625+
{
626+
for (var i = 0; i < buffer.Length; i++)
627+
{
628+
if (buffer[i] == _partSeparator && (i == 0 || buffer[i - 1] != _escape))
629+
{
630+
return i;
631+
}
632+
}
633+
634+
return -1;
635+
}
550636
}

src/HotChocolate/Core/test/Types.Tests/Types/Relay/DefaultNodeIdSerializerTests.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,24 @@ public void Parse_CompositeId2()
506506
Assert.Equal(compositeId, parsed.InternalId);
507507
}
508508

509+
[Fact]
510+
public void Parse_CompositeId_With_Escaping()
511+
{
512+
var compositeId =
513+
new CompositeId(
514+
":foo:bar:",
515+
42,
516+
// The bytes of this GUID contain a part separator (colon).
517+
new Guid("3bc83a67-b494-4c0c-a31a-d1921b077a32"),
518+
true);
519+
var serializer = CreateSerializer(new CompositeIdNodeIdValueSerializer());
520+
var id = serializer.Format("Foo", compositeId);
521+
522+
var parsed = serializer.Parse(id, typeof(CompositeId));
523+
524+
Assert.Equal(compositeId, parsed.InternalId);
525+
}
526+
509527
[Fact]
510528
public void Parse_Legacy_StronglyTypedId()
511529
{

0 commit comments

Comments
 (0)