@@ -30,6 +30,7 @@ public class ContentDispositionHeaderValue
3030 private const string ModificationDateString = "modification-date" ;
3131 private const string ReadDateString = "read-date" ;
3232 private const string SizeString = "size" ;
33+ private const int MaxStackAllocSizeBytes = 256 ;
3334 private static readonly char [ ] QuestionMark = new char [ ] { '?' } ;
3435 private static readonly char [ ] SingleQuote = new char [ ] { '\' ' } ;
3536 private static readonly char [ ] EscapeChars = new char [ ] { '\\ ' , '"' } ;
@@ -543,14 +544,17 @@ private static bool RequiresEncoding(StringSegment input)
543544
544545 // Encode using MIME encoding
545546 // And adds surrounding quotes, Encoded data must always be quoted, the equals signs are invalid in tokens
547+ [ SkipLocalsInit ]
546548 private string EncodeMimeWithQuotes ( StringSegment input )
547549 {
548550 var requiredLength = MimePrefix . Length +
549551 Base64 . GetMaxEncodedToUtf8Length ( Encoding . UTF8 . GetByteCount ( input . AsSpan ( ) ) ) +
550552 MimeSuffix . Length ;
551- Span < byte > buffer = requiredLength <= 256
552- ? ( stackalloc byte [ 256 ] ) . Slice ( 0 , requiredLength )
553- : new byte [ requiredLength ] ;
553+ byte [ ] ? bufferFromPool = null ;
554+ Span < byte > buffer = requiredLength <= MaxStackAllocSizeBytes
555+ ? stackalloc byte [ MaxStackAllocSizeBytes ]
556+ : bufferFromPool = ArrayPool < byte > . Shared . Rent ( requiredLength ) ;
557+ buffer = buffer [ ..requiredLength ] ;
554558
555559 MimePrefix . CopyTo ( buffer ) ;
556560 var bufferContent = buffer . Slice ( MimePrefix . Length ) ;
@@ -560,7 +564,14 @@ private string EncodeMimeWithQuotes(StringSegment input)
560564
561565 MimeSuffix . CopyTo ( bufferContent . Slice ( base64ContentLength ) ) ;
562566
563- return Encoding . UTF8 . GetString ( buffer . Slice ( 0 , MimePrefix . Length + base64ContentLength + MimeSuffix . Length ) ) ;
567+ var result = Encoding . UTF8 . GetString ( buffer . Slice ( 0 , MimePrefix . Length + base64ContentLength + MimeSuffix . Length ) ) ;
568+
569+ if ( bufferFromPool is not null )
570+ {
571+ ArrayPool < byte > . Shared . Return ( bufferFromPool ) ;
572+ }
573+
574+ return result ;
564575 }
565576
566577 // Attempt to decode MIME encoded strings
@@ -607,32 +618,60 @@ private bool TryDecodeMime(StringSegment input, [NotNullWhen(true)] out string?
607618
608619 // Encode a string using RFC 5987 encoding
609620 // encoding'lang'PercentEncodedSpecials
621+ [ SkipLocalsInit ]
610622 private static string Encode5987 ( StringSegment input )
611623 {
612624 var builder = new StringBuilder ( "UTF-8\' \' " ) ;
613- for ( int i = 0 ; i < input . Length ; i ++ )
625+
626+ var maxInputBytes = Encoding . UTF8 . GetMaxByteCount ( input . Length ) ;
627+ byte [ ] ? bufferFromPool = null ;
628+ Span < byte > inputBytes = maxInputBytes <= MaxStackAllocSizeBytes
629+ ? stackalloc byte [ MaxStackAllocSizeBytes ]
630+ : bufferFromPool = ArrayPool < byte > . Shared . Rent ( maxInputBytes ) ;
631+
632+ var bytesWritten = Encoding . UTF8 . GetBytes ( input , inputBytes ) ;
633+ inputBytes = inputBytes [ ..bytesWritten ] ;
634+
635+ int totalBytesConsumed = 0 ;
636+ while ( totalBytesConsumed < inputBytes . Length )
614637 {
615- var c = input [ i ] ;
616- // attr-char = ALPHA / DIGIT / "!" / "#" / "$" / "&" / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
617- // ; token except ( "*" / "'" / "%" )
618- if ( c > 0x7F ) // Encodes as multiple utf-8 bytes
638+ if ( inputBytes [ totalBytesConsumed ] <= 0x7F )
619639 {
620- var bytes = Encoding . UTF8 . GetBytes ( c . ToString ( ) ) ;
621- foreach ( byte b in bytes )
640+ // This is an ASCII char. Let's handle it ourselves.
641+
642+ char c = ( char ) inputBytes [ totalBytesConsumed ] ;
643+ if ( ! HttpRuleParser . IsTokenChar ( c ) || c == '*' || c == '\' ' || c == '%' )
622644 {
623- HexEscape ( builder , ( char ) b ) ;
645+ HexEscape ( builder , c ) ;
624646 }
625- }
626- else if ( ! HttpRuleParser . IsTokenChar ( c ) || c == '*' || c == '\' ' || c == '%' )
627- {
628- // ASCII - Only one encoded byte
629- HexEscape ( builder , c ) ;
647+ else
648+ {
649+ builder . Append ( c ) ;
650+ }
651+
652+ totalBytesConsumed ++ ;
630653 }
631654 else
632655 {
633- builder . Append ( c ) ;
656+ // Non-ASCII, let's rely on Rune to decode it.
657+
658+ Rune . DecodeFromUtf8 ( inputBytes . Slice ( totalBytesConsumed ) , out Rune r , out int bytesConsumedForRune ) ;
659+ Contract . Assert ( ! r . IsAscii , "We shouldn't have gotten here if the Rune is ASCII." ) ;
660+
661+ for ( int i = 0 ; i < bytesConsumedForRune ; i ++ )
662+ {
663+ HexEscape ( builder , ( char ) inputBytes [ totalBytesConsumed + i ] ) ;
664+ }
665+
666+ totalBytesConsumed += bytesConsumedForRune ;
634667 }
635668 }
669+
670+ if ( bufferFromPool is not null )
671+ {
672+ ArrayPool < byte > . Shared . Return ( bufferFromPool ) ;
673+ }
674+
636675 return builder . ToString ( ) ;
637676 }
638677
0 commit comments