@@ -16,6 +16,7 @@ namespace HotChocolate.Types.Relay;
16
16
public abstract class CompositeNodeIdValueSerializer < T > : INodeIdValueSerializer
17
17
{
18
18
private const byte _partSeparator = ( byte ) ':' ;
19
+ private const byte _escape = ( byte ) '\\ ' ;
19
20
private static readonly Encoding _utf8 = Encoding . UTF8 ;
20
21
21
22
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
87
88
/// </returns>
88
89
protected static bool TryFormatIdPart ( Span < byte > buffer , string value , out int written )
89
90
{
90
- var requiredCapacity = _utf8 . GetByteCount ( value ) + 1 ;
91
+ var requiredCapacity = _utf8 . GetByteCount ( value ) * 2 + 1 ; // * 2 to allow for escaping.
91
92
if ( buffer . Length < requiredCapacity )
92
93
{
93
94
written = 0 ;
94
95
return false ;
95
96
}
96
97
97
- var stringBytes = buffer ;
98
- Utf8GraphQLParser . ConvertToBytes ( value , ref stringBytes ) ;
98
+ Span < byte > utf8Bytes = stackalloc byte [ _utf8 . GetByteCount ( value ) ] ;
99
+ _utf8 . GetBytes ( value , utf8Bytes ) ;
99
100
100
- buffer = buffer . Slice ( stringBytes . Length ) ;
101
+ var bytesWritten = WriteEscapedBytes ( utf8Bytes , buffer ) ;
102
+
103
+ buffer = buffer [ bytesWritten ..] ;
101
104
buffer [ 0 ] = _partSeparator ;
102
- written = stringBytes . Length + 1 ;
105
+ written = bytesWritten + 1 ;
103
106
return true ;
104
107
}
105
108
@@ -125,7 +128,8 @@ protected static bool TryFormatIdPart(Span<byte> buffer, Guid value, out int wri
125
128
{
126
129
if ( compress )
127
130
{
128
- if ( buffer . Length < 17 )
131
+ const int requiredCapacity = 16 * 2 + 1 ; // * 2 to allow for escaping.
132
+ if ( buffer . Length < requiredCapacity )
129
133
{
130
134
written = 0 ;
131
135
return false ;
@@ -135,16 +139,17 @@ protected static bool TryFormatIdPart(Span<byte> buffer, Guid value, out int wri
135
139
#pragma warning disable CS9191
136
140
MemoryMarshal . TryWrite ( span , ref value ) ;
137
141
#pragma warning restore CS9191
138
- span . CopyTo ( buffer ) ;
139
- buffer = buffer . Slice ( 16 ) ;
142
+ var bytesWritten = WriteEscapedBytes ( span , buffer ) ;
143
+
144
+ buffer = buffer [ bytesWritten ..] ;
140
145
buffer [ 0 ] = _partSeparator ;
141
- written = 17 ;
146
+ written = bytesWritten + 1 ;
142
147
return true ;
143
148
}
144
149
145
150
if ( Utf8Formatter . TryFormat ( value , buffer , out written , format : 'N' ) )
146
151
{
147
- buffer = buffer . Slice ( written ) ;
152
+ buffer = buffer [ written .. ] ;
148
153
if ( buffer . Length < 1 )
149
154
{
150
155
return false ;
@@ -344,8 +349,9 @@ protected static unsafe bool TryParseIdPart(
344
349
[ NotNullWhen ( true ) ] out string ? value ,
345
350
out int consumed )
346
351
{
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 ) ;
349
355
fixed ( byte * b = valueSpan )
350
356
{
351
357
value = _utf8 . GetString ( b , valueSpan . Length ) ;
@@ -379,11 +385,13 @@ protected static bool TryParseIdPart(
379
385
out int consumed ,
380
386
bool compress = true )
381
387
{
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 ] ;
384
390
385
391
if ( compress )
386
392
{
393
+ valueSpan = Unescape ( valueSpan ) ;
394
+
387
395
if ( valueSpan . Length != 16 )
388
396
{
389
397
value = default ;
@@ -396,7 +404,7 @@ protected static bool TryParseIdPart(
396
404
return true ;
397
405
}
398
406
399
- if ( Utf8Parser . TryParse ( valueSpan , out Guid parsedValue , out _ ) )
407
+ if ( Utf8Parser . TryParse ( valueSpan , out Guid parsedValue , out _ , standardFormat : 'N' ) )
400
408
{
401
409
value = parsedValue ;
402
410
consumed = index + 1 ;
@@ -547,4 +555,82 @@ protected static bool TryParseIdPart(
547
555
consumed = 0 ;
548
556
return false ;
549
557
}
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
+ }
550
636
}
0 commit comments