diff --git a/src/Http/Headers/src/ContentDispositionHeaderValue.cs b/src/Http/Headers/src/ContentDispositionHeaderValue.cs index b0a51887ff71..f4f0378908d3 100644 --- a/src/Http/Headers/src/ContentDispositionHeaderValue.cs +++ b/src/Http/Headers/src/ContentDispositionHeaderValue.cs @@ -3,11 +3,13 @@ using System; using System.Buffers; +using System.Buffers.Text; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Globalization; using System.Linq; +using System.Runtime.CompilerServices; using System.Text; using Microsoft.Extensions.Primitives; @@ -30,6 +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 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); @@ -466,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) { @@ -476,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); } @@ -531,20 +541,41 @@ private bool RequiresEncoding(StringSegment input) return false; } - // Encode using MIME encoding - private unsafe string EncodeMime(StringSegment input) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetBase64Length(int inputLength) { - fixed (char* chars = input.Buffer) + // 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) { - 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 + "?="; + throw new OutOfMemoryException(); } + + return (int)outlen; + } + + // Encode using MIME encoding + // 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 + + GetBase64Length(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 diff --git a/src/Http/Headers/src/HeaderUtilities.cs b/src/Http/Headers/src/HeaderUtilities.cs index 1c621dffc5f7..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"; @@ -325,7 +324,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 +365,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 +373,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.AsSpan(), NumberStyles.None, NumberFormatInfo.InvariantInfo, out result); } /// @@ -417,40 +391,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.AsSpan(), NumberStyles.None, NumberFormatInfo.InvariantInfo, out result); } // Strict and fast RFC7231 5.3.1 Quality value parser (and without memory allocation) @@ -553,7 +501,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) { @@ -565,19 +513,7 @@ public unsafe static string FormatNonNegativeInt64(long value) return "0"; } - var position = _int64MaxStringLength; - char* charBuffer = stackalloc char[_int64MaxStringLength]; - - do - { - // Consider using Math.DivRem() if available - var quotient = value / 10; - charBuffer[--position] = (char)(0x30 + (value - quotient * 10)); // 0x30 = '0' - value = quotient; - } - while (value != 0); - - return new string(charBuffer, position, _int64MaxStringLength - position); + return ((ulong)value).ToString(NumberFormatInfo.InvariantInfo); } /// diff --git a/src/Http/Headers/src/MediaTypeHeaderValue.cs b/src/Http/Headers/src/MediaTypeHeaderValue.cs index 547b017f753e..3dd35d32f2c1 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,7 +646,7 @@ private static int GetMediaTypeExpressionLength(StringSegment input, int startIn } else { - mediaType = input.Substring(startIndex, typeLength) + ForwardSlashCharacter + input.Substring(current, subtypeLength); + mediaType = string.Concat(input.AsSpan().Slice(startIndex, typeLength), "/", input.AsSpan().Slice(current, subtypeLength)); } 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