@@ -16,6 +16,7 @@ namespace HotChocolate.Types.Relay;
1616public 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}
0 commit comments