33
44using System ;
55using System . Buffers ;
6+ using System . Buffers . Text ;
67using System . Collections . Generic ;
78using System . Diagnostics . CodeAnalysis ;
89using System . Diagnostics . Contracts ;
910using System . Globalization ;
1011using System . Linq ;
12+ using System . Runtime . CompilerServices ;
1113using System . Text ;
1214using Microsoft . Extensions . Primitives ;
1315
@@ -30,6 +32,9 @@ public class ContentDispositionHeaderValue
3032 private const string SizeString = "size" ;
3133 private static readonly char [ ] QuestionMark = new char [ ] { '?' } ;
3234 private static readonly char [ ] SingleQuote = new char [ ] { '\' ' } ;
35+ private static readonly char [ ] EscapeChars = new char [ ] { '\\ ' , '"' } ;
36+ private static ReadOnlySpan < byte > MimePrefix => new byte [ ] { ( byte ) '"' , ( byte ) '=' , ( byte ) '?' , ( byte ) 'u' , ( byte ) 't' , ( byte ) 'f' , ( byte ) '-' , ( byte ) '8' , ( byte ) '?' , ( byte ) 'B' , ( byte ) '?' } ;
37+ private static ReadOnlySpan < byte > MimeSuffix => new byte [ ] { ( byte ) '?' , ( byte ) '=' , ( byte ) '"' } ;
3338
3439 private static readonly HttpHeaderParser < ContentDispositionHeaderValue > Parser
3540 = new GenericHeaderParser < ContentDispositionHeaderValue > ( false , GetDispositionTypeLength ) ;
@@ -466,8 +471,10 @@ private StringSegment EncodeAndQuoteMime(StringSegment input)
466471
467472 if ( RequiresEncoding ( result ) )
468473 {
469- needsQuotes = true ; // Encoded data must always be quoted, the equals signs are invalid in tokens
470- result = EncodeMime ( result ) ; // =?utf-8?B?asdfasdfaesdf?=
474+ // EncodeMimeWithQuotes will Base64 encode any quotes in the input, and surround the payload in quotes
475+ // so there is no need to add quotes
476+ needsQuotes = false ;
477+ result = EncodeMimeWithQuotes ( result ) ; // "=?utf-8?B?asdfasdfaesdf?="
471478 }
472479 else if ( ! needsQuotes && HttpRuleParser . GetTokenLength ( result , 0 ) != result . Length )
473480 {
@@ -476,8 +483,11 @@ private StringSegment EncodeAndQuoteMime(StringSegment input)
476483
477484 if ( needsQuotes )
478485 {
479- // '\' and '"' must be escaped in a quoted string
480- result = result . ToString ( ) . Replace ( @"\" , @"\\" ) . Replace ( @"""" , @"\""" ) ;
486+ if ( result . IndexOfAny ( EscapeChars ) != - 1 )
487+ {
488+ // '\' and '"' must be escaped in a quoted string
489+ result = result . ToString ( ) . Replace ( @"\" , @"\\" ) . Replace ( @"""" , @"\""" ) ;
490+ }
481491 // Re-add quotes "value"
482492 result = string . Format ( CultureInfo . InvariantCulture , "\" {0}\" " , result ) ;
483493 }
@@ -531,20 +541,41 @@ private bool RequiresEncoding(StringSegment input)
531541 return false ;
532542 }
533543
534- // Encode using MIME encoding
535- private unsafe string EncodeMime ( StringSegment input )
544+ [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
545+ private static int GetBase64Length ( int inputLength )
536546 {
537- fixed ( char * chars = input . Buffer )
547+ // Copied from https://github.com/dotnet/runtime/blob/82ca681cbac89d813a3ce397e0c665e6c051ed67/src/libraries/System.Private.CoreLib/src/System/Convert.cs#L2530
548+ long outlen = ( ( long ) inputLength ) / 3 * 4 ; // the base length - we want integer division here.
549+ outlen += ( ( inputLength % 3 ) != 0 ) ? 4 : 0 ; // at most 4 more chars for the remainder
550+
551+ if ( outlen > int . MaxValue )
538552 {
539- var byteCount = Encoding . UTF8 . GetByteCount ( chars + input . Offset , input . Length ) ;
540- var buffer = new byte [ byteCount ] ;
541- fixed ( byte * bytes = buffer )
542- {
543- Encoding . UTF8 . GetBytes ( chars + input . Offset , input . Length , bytes , byteCount ) ;
544- }
545- var encodedName = Convert . ToBase64String ( buffer ) ;
546- return "=?utf-8?B?" + encodedName + "?=" ;
553+ throw new OutOfMemoryException ( ) ;
547554 }
555+
556+ return ( int ) outlen ;
557+ }
558+
559+ // Encode using MIME encoding
560+ // And adds surrounding quotes, Encoded data must always be quoted, the equals signs are invalid in tokens
561+ private string EncodeMimeWithQuotes ( StringSegment input )
562+ {
563+ var requiredLength = MimePrefix . Length +
564+ GetBase64Length ( Encoding . UTF8 . GetByteCount ( input . AsSpan ( ) ) ) +
565+ MimeSuffix . Length ;
566+ Span < byte > buffer = requiredLength <= 256
567+ ? ( stackalloc byte [ 256 ] ) . Slice ( 0 , requiredLength )
568+ : new byte [ requiredLength ] ;
569+
570+ MimePrefix . CopyTo ( buffer ) ;
571+ var bufferContent = buffer . Slice ( MimePrefix . Length ) ;
572+ var contentLength = Encoding . UTF8 . GetBytes ( input . AsSpan ( ) , bufferContent ) ;
573+
574+ Base64 . EncodeToUtf8InPlace ( bufferContent , contentLength , out var base64ContentLength ) ;
575+
576+ MimeSuffix . CopyTo ( bufferContent . Slice ( base64ContentLength ) ) ;
577+
578+ return Encoding . UTF8 . GetString ( buffer . Slice ( 0 , MimePrefix . Length + base64ContentLength + MimeSuffix . Length ) ) ;
548579 }
549580
550581 // Attempt to decode MIME encoded strings
0 commit comments