From 08edaf09308c38b050fedc273f267f4194a54f1c Mon Sep 17 00:00:00 2001 From: BrennanConroy Date: Thu, 25 Mar 2021 21:41:59 -0700 Subject: [PATCH 1/6] [Http] Remove some unsafe code and save a string allocation --- .../src/ContentDispositionHeaderValue.cs | 16 ++--- src/Http/Headers/src/HeaderUtilities.cs | 72 +++---------------- src/Http/Headers/src/MediaTypeHeaderValue.cs | 14 +++- .../src/Microsoft.Net.Http.Headers.csproj | 3 +- .../src/Matching/SingleEntryAsciiJumpTable.cs | 4 +- .../src/Microsoft.AspNetCore.Routing.csproj | 1 - 6 files changed, 30 insertions(+), 80 deletions(-) diff --git a/src/Http/Headers/src/ContentDispositionHeaderValue.cs b/src/Http/Headers/src/ContentDispositionHeaderValue.cs index b0a51887ff71..fc2c5e5e8ca9 100644 --- a/src/Http/Headers/src/ContentDispositionHeaderValue.cs +++ b/src/Http/Headers/src/ContentDispositionHeaderValue.cs @@ -532,19 +532,11 @@ private bool RequiresEncoding(StringSegment input) } // Encode using MIME encoding - private unsafe string EncodeMime(StringSegment input) + private string EncodeMime(StringSegment input) { - fixed (char* chars = input.Buffer) - { - var byteCount = Encoding.UTF8.GetByteCount(chars + input.Offset, input.Length); - var buffer = new byte[byteCount]; - fixed (byte* bytes = buffer) - { - Encoding.UTF8.GetBytes(chars + input.Offset, input.Length, bytes, byteCount); - } - var encodedName = Convert.ToBase64String(buffer); - return "=?utf-8?B?" + encodedName + "?="; - } + var buffer = Encoding.UTF8.GetBytes(input.Buffer); + var encodedName = Convert.ToBase64String(buffer); + return string.Concat("=?utf-8?B?", encodedName, "?="); } // Attempt to decode MIME encoded strings diff --git a/src/Http/Headers/src/HeaderUtilities.cs b/src/Http/Headers/src/HeaderUtilities.cs index 1c621dffc5f7..4e8902d40371 100644 --- a/src/Http/Headers/src/HeaderUtilities.cs +++ b/src/Http/Headers/src/HeaderUtilities.cs @@ -325,7 +325,7 @@ public static bool ContainsCacheDirective(StringValues cacheControlDirectives, s return false; } - private static unsafe bool TryParseNonNegativeInt64FromHeaderValue(int startIndex, string headerValue, out long result) + private static bool TryParseNonNegativeInt64FromHeaderValue(int startIndex, string headerValue, out long result) { // Trim leading whitespace startIndex += HttpRuleParser.GetWhitespaceLength(headerValue, startIndex); @@ -366,7 +366,7 @@ private static unsafe bool TryParseNonNegativeInt64FromHeaderValue(int startInde /// result will be overwritten. /// /// if parsing succeeded; otherwise, . - public static unsafe bool TryParseNonNegativeInt32(StringSegment value, out int result) + public static bool TryParseNonNegativeInt32(StringSegment value, out int result) { if (string.IsNullOrEmpty(value.Buffer) || value.Length == 0) { @@ -374,32 +374,7 @@ public static unsafe bool TryParseNonNegativeInt32(StringSegment value, out int return false; } - result = 0; - fixed (char* ptr = value.Buffer) - { - var ch = (ushort*)ptr + value.Offset; - var end = ch + value.Length; - - ushort digit = 0; - while (ch < end && (digit = (ushort)(*ch - 0x30)) <= 9) - { - // Check for overflow - if ((result = result * 10 + digit) < 0) - { - result = 0; - return false; - } - - ch++; - } - - if (ch != end) - { - result = 0; - return false; - } - return true; - } + return int.TryParse(value.Buffer.AsSpan().Slice(value.Offset, value.Length), NumberStyles.None, NumberFormatInfo.InvariantInfo, out result); } /// @@ -417,40 +392,14 @@ public static unsafe bool TryParseNonNegativeInt32(StringSegment value, out int /// originally supplied in result will be overwritten. /// /// if parsing succeeded; otherwise, . - public static unsafe bool TryParseNonNegativeInt64(StringSegment value, out long result) + public static bool TryParseNonNegativeInt64(StringSegment value, out long result) { if (string.IsNullOrEmpty(value.Buffer) || value.Length == 0) { result = 0; return false; } - - result = 0; - fixed (char* ptr = value.Buffer) - { - var ch = (ushort*)ptr + value.Offset; - var end = ch + value.Length; - - ushort digit = 0; - while (ch < end && (digit = (ushort)(*ch - 0x30)) <= 9) - { - // Check for overflow - if ((result = result * 10 + digit) < 0) - { - result = 0; - return false; - } - - ch++; - } - - if (ch != end) - { - result = 0; - return false; - } - return true; - } + return long.TryParse(value.Buffer.AsSpan().Slice(value.Offset, value.Length), NumberStyles.None, NumberFormatInfo.InvariantInfo, out result); } // Strict and fast RFC7231 5.3.1 Quality value parser (and without memory allocation) @@ -553,7 +502,7 @@ internal static bool TryParseQualityDouble(StringSegment input, int startIndex, /// /// The string representation of the value of this instance, consisting of a sequence of digits ranging from 0 to 9 with no leading zeroes. /// - public unsafe static string FormatNonNegativeInt64(long value) + public static string FormatNonNegativeInt64(long value) { if (value < 0) { @@ -566,18 +515,17 @@ public unsafe static string FormatNonNegativeInt64(long value) } var position = _int64MaxStringLength; - char* charBuffer = stackalloc char[_int64MaxStringLength]; + Span charBuffer = stackalloc char[_int64MaxStringLength]; do { - // Consider using Math.DivRem() if available - var quotient = value / 10; - charBuffer[--position] = (char)(0x30 + (value - quotient * 10)); // 0x30 = '0' + var (quotient, rem) = Math.DivRem(value, 10); + charBuffer[--position] = (char)(0x30 + rem); // 0x30 = '0' value = quotient; } while (value != 0); - return new string(charBuffer, position, _int64MaxStringLength - position); + return new string(charBuffer.Slice(position)); } /// diff --git a/src/Http/Headers/src/MediaTypeHeaderValue.cs b/src/Http/Headers/src/MediaTypeHeaderValue.cs index 547b017f753e..bbf376422feb 100644 --- a/src/Http/Headers/src/MediaTypeHeaderValue.cs +++ b/src/Http/Headers/src/MediaTypeHeaderValue.cs @@ -645,7 +645,19 @@ private static int GetMediaTypeExpressionLength(StringSegment input, int startIn } else { - mediaType = input.Substring(startIndex, typeLength) + ForwardSlashCharacter + input.Substring(current, subtypeLength); + // Equivalent to: input.Substring(startIndex, typeLength) + ForwardSlashCharacter + input.Substring(current, subtypeLength); + // but saves an unnecessary string allocation + mediaType = string.Create(typeLength + subtypeLength + 1, (input, startIndex, typeLength, subtypeLength, current), static (span, state) => + { + var (input, startIndex, typeLength, subtypeLength, current) = state; + var segment = input.Buffer.AsSpan(input.Offset); + segment.Slice(startIndex, typeLength).CopyTo(span); + + span[typeLength] = ForwardSlashCharacter; + + span = span.Slice(typeLength + 1); + segment.Slice(current, subtypeLength).CopyTo(span); + }); } return mediaTypeLength; diff --git a/src/Http/Headers/src/Microsoft.Net.Http.Headers.csproj b/src/Http/Headers/src/Microsoft.Net.Http.Headers.csproj index 1bd100563caa..fef806c126a7 100644 --- a/src/Http/Headers/src/Microsoft.Net.Http.Headers.csproj +++ b/src/Http/Headers/src/Microsoft.Net.Http.Headers.csproj @@ -1,10 +1,9 @@ - + HTTP header parser implementations. $(DefaultNetCoreTargetFramework) true - true true http false diff --git a/src/Http/Routing/src/Matching/SingleEntryAsciiJumpTable.cs b/src/Http/Routing/src/Matching/SingleEntryAsciiJumpTable.cs index 5ca0bab566cc..e385e1885977 100644 --- a/src/Http/Routing/src/Matching/SingleEntryAsciiJumpTable.cs +++ b/src/Http/Routing/src/Matching/SingleEntryAsciiJumpTable.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; @@ -27,7 +27,7 @@ public SingleEntryAsciiJumpTable( _destination = destination; } - public unsafe override int GetDestination(string path, PathSegment segment) + public override int GetDestination(string path, PathSegment segment) { var length = segment.Length; if (length == 0) diff --git a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj index 67723bd1d175..66eab5c0cd61 100644 --- a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj +++ b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj @@ -9,7 +9,6 @@ Microsoft.AspNetCore.Routing.RouteCollection true true aspnetcore;routing - true false enable From 6d984cef0f86f75fc770f39a6041cc16c406425e Mon Sep 17 00:00:00 2001 From: BrennanConroy Date: Thu, 25 Mar 2021 22:27:13 -0700 Subject: [PATCH 2/6] fb --- src/Http/Headers/src/ContentDispositionHeaderValue.cs | 2 +- src/Http/Headers/src/HeaderUtilities.cs | 2 +- src/Http/Headers/src/MediaTypeHeaderValue.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Http/Headers/src/ContentDispositionHeaderValue.cs b/src/Http/Headers/src/ContentDispositionHeaderValue.cs index fc2c5e5e8ca9..dd36f0194c99 100644 --- a/src/Http/Headers/src/ContentDispositionHeaderValue.cs +++ b/src/Http/Headers/src/ContentDispositionHeaderValue.cs @@ -534,7 +534,7 @@ private bool RequiresEncoding(StringSegment input) // Encode using MIME encoding private string EncodeMime(StringSegment input) { - var buffer = Encoding.UTF8.GetBytes(input.Buffer); + var buffer = Encoding.UTF8.GetBytes(new ReadOnlySequence(input.AsMemory())); var encodedName = Convert.ToBase64String(buffer); return string.Concat("=?utf-8?B?", encodedName, "?="); } diff --git a/src/Http/Headers/src/HeaderUtilities.cs b/src/Http/Headers/src/HeaderUtilities.cs index 4e8902d40371..9f2294aca0bc 100644 --- a/src/Http/Headers/src/HeaderUtilities.cs +++ b/src/Http/Headers/src/HeaderUtilities.cs @@ -374,7 +374,7 @@ public static bool TryParseNonNegativeInt32(StringSegment value, out int result) return false; } - return int.TryParse(value.Buffer.AsSpan().Slice(value.Offset, value.Length), NumberStyles.None, NumberFormatInfo.InvariantInfo, out result); + return int.TryParse(value.AsSpan(), NumberStyles.None, NumberFormatInfo.InvariantInfo, out result); } /// diff --git a/src/Http/Headers/src/MediaTypeHeaderValue.cs b/src/Http/Headers/src/MediaTypeHeaderValue.cs index bbf376422feb..ef2bce99c5e7 100644 --- a/src/Http/Headers/src/MediaTypeHeaderValue.cs +++ b/src/Http/Headers/src/MediaTypeHeaderValue.cs @@ -650,7 +650,7 @@ private static int GetMediaTypeExpressionLength(StringSegment input, int startIn mediaType = string.Create(typeLength + subtypeLength + 1, (input, startIndex, typeLength, subtypeLength, current), static (span, state) => { var (input, startIndex, typeLength, subtypeLength, current) = state; - var segment = input.Buffer.AsSpan(input.Offset); + var segment = input.AsSpan(); segment.Slice(startIndex, typeLength).CopyTo(span); span[typeLength] = ForwardSlashCharacter; From 2455646f18a4714e262b2a6edf207520e1f8a397 Mon Sep 17 00:00:00 2001 From: BrennanConroy Date: Thu, 25 Mar 2021 22:49:26 -0700 Subject: [PATCH 3/6] fb --- src/Http/Headers/src/HeaderUtilities.cs | 2 +- src/Http/Headers/src/MediaTypeHeaderValue.cs | 17 ++++------------- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/src/Http/Headers/src/HeaderUtilities.cs b/src/Http/Headers/src/HeaderUtilities.cs index 9f2294aca0bc..c41820a0bb72 100644 --- a/src/Http/Headers/src/HeaderUtilities.cs +++ b/src/Http/Headers/src/HeaderUtilities.cs @@ -399,7 +399,7 @@ public static bool TryParseNonNegativeInt64(StringSegment value, out long result result = 0; return false; } - return long.TryParse(value.Buffer.AsSpan().Slice(value.Offset, value.Length), NumberStyles.None, NumberFormatInfo.InvariantInfo, out result); + return long.TryParse(value.AsSpan(), NumberStyles.None, NumberFormatInfo.InvariantInfo, out result); } // Strict and fast RFC7231 5.3.1 Quality value parser (and without memory allocation) diff --git a/src/Http/Headers/src/MediaTypeHeaderValue.cs b/src/Http/Headers/src/MediaTypeHeaderValue.cs index ef2bce99c5e7..3db03883d5a2 100644 --- a/src/Http/Headers/src/MediaTypeHeaderValue.cs +++ b/src/Http/Headers/src/MediaTypeHeaderValue.cs @@ -7,6 +7,7 @@ using System.Diagnostics.Contracts; using System.Globalization; using System.Linq; +using System.Runtime.InteropServices; using System.Text; using Microsoft.Extensions.Primitives; @@ -645,19 +646,9 @@ private static int GetMediaTypeExpressionLength(StringSegment input, int startIn } else { - // Equivalent to: input.Substring(startIndex, typeLength) + ForwardSlashCharacter + input.Substring(current, subtypeLength); - // but saves an unnecessary string allocation - mediaType = string.Create(typeLength + subtypeLength + 1, (input, startIndex, typeLength, subtypeLength, current), static (span, state) => - { - var (input, startIndex, typeLength, subtypeLength, current) = state; - var segment = input.AsSpan(); - segment.Slice(startIndex, typeLength).CopyTo(span); - - span[typeLength] = ForwardSlashCharacter; - - span = span.Slice(typeLength + 1); - segment.Slice(current, subtypeLength).CopyTo(span); - }); + var forwardSlashCharacter = ForwardSlashCharacter; + var forwardSlash = MemoryMarshal.CreateReadOnlySpan(ref forwardSlashCharacter, 1); + mediaType = string.Concat(input.AsSpan().Slice(startIndex, typeLength), forwardSlash, input.AsSpan().Slice(current, subtypeLength)); } return mediaTypeLength; From a03d204aa8e47c493aa5977db1a6aa4b0d505d35 Mon Sep 17 00:00:00 2001 From: BrennanConroy Date: Fri, 26 Mar 2021 08:07:55 -0700 Subject: [PATCH 4/6] fb --- .../Headers/src/ContentDispositionHeaderValue.cs | 2 +- src/Http/Headers/src/HeaderUtilities.cs | 14 +------------- src/Http/Headers/src/MediaTypeHeaderValue.cs | 4 +--- 3 files changed, 3 insertions(+), 17 deletions(-) diff --git a/src/Http/Headers/src/ContentDispositionHeaderValue.cs b/src/Http/Headers/src/ContentDispositionHeaderValue.cs index dd36f0194c99..dc32e1477280 100644 --- a/src/Http/Headers/src/ContentDispositionHeaderValue.cs +++ b/src/Http/Headers/src/ContentDispositionHeaderValue.cs @@ -536,7 +536,7 @@ private string EncodeMime(StringSegment input) { var buffer = Encoding.UTF8.GetBytes(new ReadOnlySequence(input.AsMemory())); var encodedName = Convert.ToBase64String(buffer); - return string.Concat("=?utf-8?B?", encodedName, "?="); + return "=?utf-8?B?" + encodedName + "?="; } // Attempt to decode MIME encoded strings diff --git a/src/Http/Headers/src/HeaderUtilities.cs b/src/Http/Headers/src/HeaderUtilities.cs index c41820a0bb72..9d5fe5179674 100644 --- a/src/Http/Headers/src/HeaderUtilities.cs +++ b/src/Http/Headers/src/HeaderUtilities.cs @@ -16,7 +16,6 @@ namespace Microsoft.Net.Http.Headers /// public static class HeaderUtilities { - private static readonly int _int64MaxStringLength = 19; private static readonly int _qualityValueMaxCharCount = 10; // Little bit more permissive than RFC7231 5.3.1 private const string QualityName = "q"; internal const string BytesUnit = "bytes"; @@ -514,18 +513,7 @@ public static string FormatNonNegativeInt64(long value) return "0"; } - var position = _int64MaxStringLength; - Span charBuffer = stackalloc char[_int64MaxStringLength]; - - do - { - var (quotient, rem) = Math.DivRem(value, 10); - charBuffer[--position] = (char)(0x30 + rem); // 0x30 = '0' - value = quotient; - } - while (value != 0); - - return new string(charBuffer.Slice(position)); + return ((ulong)value).ToString(NumberFormatInfo.InvariantInfo); } /// diff --git a/src/Http/Headers/src/MediaTypeHeaderValue.cs b/src/Http/Headers/src/MediaTypeHeaderValue.cs index 3db03883d5a2..3dd35d32f2c1 100644 --- a/src/Http/Headers/src/MediaTypeHeaderValue.cs +++ b/src/Http/Headers/src/MediaTypeHeaderValue.cs @@ -646,9 +646,7 @@ private static int GetMediaTypeExpressionLength(StringSegment input, int startIn } else { - var forwardSlashCharacter = ForwardSlashCharacter; - var forwardSlash = MemoryMarshal.CreateReadOnlySpan(ref forwardSlashCharacter, 1); - mediaType = string.Concat(input.AsSpan().Slice(startIndex, typeLength), forwardSlash, input.AsSpan().Slice(current, subtypeLength)); + mediaType = string.Concat(input.AsSpan().Slice(startIndex, typeLength), "/", input.AsSpan().Slice(current, subtypeLength)); } return mediaTypeLength; From 58e6752a77a662552209b4750c1fc691499265bd Mon Sep 17 00:00:00 2001 From: BrennanConroy Date: Fri, 26 Mar 2021 11:06:24 -0700 Subject: [PATCH 5/6] stackalloc --- .../src/ContentDispositionHeaderValue.cs | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/Http/Headers/src/ContentDispositionHeaderValue.cs b/src/Http/Headers/src/ContentDispositionHeaderValue.cs index dc32e1477280..96c5b0709e7e 100644 --- a/src/Http/Headers/src/ContentDispositionHeaderValue.cs +++ b/src/Http/Headers/src/ContentDispositionHeaderValue.cs @@ -3,6 +3,7 @@ using System; using System.Buffers; +using System.Buffers.Text; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; @@ -30,6 +31,8 @@ public class ContentDispositionHeaderValue private const string SizeString = "size"; private static readonly char[] QuestionMark = new char[] { '?' }; private static readonly char[] SingleQuote = new char[] { '\'' }; + private static ReadOnlySpan MimePrefix => new byte[] { (byte)'=', (byte)'?', (byte)'u', (byte)'t', (byte)'f', (byte)'-', (byte)'8', (byte)'?', (byte)'B', (byte)'?' }; + private static ReadOnlySpan MimeSuffix => new byte[] { (byte)'?', (byte)'=' }; private static readonly HttpHeaderParser Parser = new GenericHeaderParser(false, GetDispositionTypeLength); @@ -534,9 +537,22 @@ private bool RequiresEncoding(StringSegment input) // Encode using MIME encoding private string EncodeMime(StringSegment input) { - var buffer = Encoding.UTF8.GetBytes(new ReadOnlySequence(input.AsMemory())); - var encodedName = Convert.ToBase64String(buffer); - return "=?utf-8?B?" + encodedName + "?="; + var requiredLength = MimePrefix.Length + + Base64.GetMaxEncodedToUtf8Length(Encoding.UTF8.GetByteCount(input.AsSpan())) + + MimeSuffix.Length; + Span buffer = requiredLength <= 256 + ? (stackalloc byte[256]).Slice(0, requiredLength) + : new byte[requiredLength]; + + MimePrefix.CopyTo(buffer); + var bufferContent = buffer.Slice(MimePrefix.Length); + var contentLength = Encoding.UTF8.GetBytes(input.AsSpan(), bufferContent); + + Base64.EncodeToUtf8InPlace(bufferContent, contentLength, out var base64ContentLength); + + MimeSuffix.CopyTo(bufferContent.Slice(base64ContentLength)); + + return Encoding.UTF8.GetString(buffer.Slice(0, MimePrefix.Length + base64ContentLength + MimeSuffix.Length)); } // Attempt to decode MIME encoded strings From a6e5563b00c2dc807b93980d674faa6c60dca1b5 Mon Sep 17 00:00:00 2001 From: BrennanConroy Date: Mon, 29 Mar 2021 15:32:35 -0700 Subject: [PATCH 6/6] Get exact base64 length, and remove another allocation --- .../src/ContentDispositionHeaderValue.cs | 39 +++++++++++++++---- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/src/Http/Headers/src/ContentDispositionHeaderValue.cs b/src/Http/Headers/src/ContentDispositionHeaderValue.cs index 96c5b0709e7e..f4f0378908d3 100644 --- a/src/Http/Headers/src/ContentDispositionHeaderValue.cs +++ b/src/Http/Headers/src/ContentDispositionHeaderValue.cs @@ -9,6 +9,7 @@ using System.Diagnostics.Contracts; using System.Globalization; using System.Linq; +using System.Runtime.CompilerServices; using System.Text; using Microsoft.Extensions.Primitives; @@ -31,8 +32,9 @@ public class ContentDispositionHeaderValue private const string SizeString = "size"; private static readonly char[] QuestionMark = new char[] { '?' }; private static readonly char[] SingleQuote = new char[] { '\'' }; - private static ReadOnlySpan MimePrefix => new byte[] { (byte)'=', (byte)'?', (byte)'u', (byte)'t', (byte)'f', (byte)'-', (byte)'8', (byte)'?', (byte)'B', (byte)'?' }; - private static ReadOnlySpan MimeSuffix => new byte[] { (byte)'?', (byte)'=' }; + private static readonly char[] EscapeChars = new char[] { '\\', '"' }; + private static ReadOnlySpan MimePrefix => new byte[] { (byte)'"', (byte)'=', (byte)'?', (byte)'u', (byte)'t', (byte)'f', (byte)'-', (byte)'8', (byte)'?', (byte)'B', (byte)'?' }; + private static ReadOnlySpan MimeSuffix => new byte[] { (byte)'?', (byte)'=', (byte)'"' }; private static readonly HttpHeaderParser Parser = new GenericHeaderParser(false, GetDispositionTypeLength); @@ -469,8 +471,10 @@ private StringSegment EncodeAndQuoteMime(StringSegment input) if (RequiresEncoding(result)) { - needsQuotes = true; // Encoded data must always be quoted, the equals signs are invalid in tokens - result = EncodeMime(result); // =?utf-8?B?asdfasdfaesdf?= + // EncodeMimeWithQuotes will Base64 encode any quotes in the input, and surround the payload in quotes + // so there is no need to add quotes + needsQuotes = false; + result = EncodeMimeWithQuotes(result); // "=?utf-8?B?asdfasdfaesdf?=" } else if (!needsQuotes && HttpRuleParser.GetTokenLength(result, 0) != result.Length) { @@ -479,8 +483,11 @@ private StringSegment EncodeAndQuoteMime(StringSegment input) if (needsQuotes) { - // '\' and '"' must be escaped in a quoted string - result = result.ToString().Replace(@"\", @"\\").Replace(@"""", @"\"""); + if (result.IndexOfAny(EscapeChars) != -1) + { + // '\' and '"' must be escaped in a quoted string + result = result.ToString().Replace(@"\", @"\\").Replace(@"""", @"\"""); + } // Re-add quotes "value" result = string.Format(CultureInfo.InvariantCulture, "\"{0}\"", result); } @@ -534,11 +541,27 @@ private bool RequiresEncoding(StringSegment input) return false; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetBase64Length(int inputLength) + { + // Copied from https://github.com/dotnet/runtime/blob/82ca681cbac89d813a3ce397e0c665e6c051ed67/src/libraries/System.Private.CoreLib/src/System/Convert.cs#L2530 + long outlen = ((long)inputLength) / 3 * 4; // the base length - we want integer division here. + outlen += ((inputLength % 3) != 0) ? 4 : 0; // at most 4 more chars for the remainder + + if (outlen > int.MaxValue) + { + throw new OutOfMemoryException(); + } + + return (int)outlen; + } + // Encode using MIME encoding - private string EncodeMime(StringSegment input) + // And adds surrounding quotes, Encoded data must always be quoted, the equals signs are invalid in tokens + private string EncodeMimeWithQuotes(StringSegment input) { var requiredLength = MimePrefix.Length + - Base64.GetMaxEncodedToUtf8Length(Encoding.UTF8.GetByteCount(input.AsSpan())) + + GetBase64Length(Encoding.UTF8.GetByteCount(input.AsSpan())) + MimeSuffix.Length; Span buffer = requiredLength <= 256 ? (stackalloc byte[256]).Slice(0, requiredLength)