From 5f3bdba584747daee5b41f4ef50fdf4be8caa0f3 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Mon, 22 Apr 2019 15:53:56 -0700 Subject: [PATCH] Spanify path -> string logic (#9628) - Use spans instead of byte[] and array segments when turning the URL into a string --- .../IIS/IIS/src/Core/IISHttpContext.cs | 6 +-- .../RequestProcessing/NativeRequestContext.cs | 9 ++-- .../HttpSys/RequestProcessing/RawUrlHelper.cs | 27 ++++------ .../RequestProcessing/RequestUriBuilder.cs | 52 ++++++++----------- 4 files changed, 39 insertions(+), 55 deletions(-) diff --git a/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs b/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs index c1da56bfc7..7286288427 100644 --- a/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs +++ b/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs @@ -184,7 +184,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core var rawUrlInBytes = GetRawUrlInBytes(); // Pre Windows 10 RS2 applicationInitialization request might not have pRawUrl set, fallback to cocked url - if (rawUrlInBytes == null) + if (rawUrlInBytes.Length == 0) { return GetCookedUrl().GetAbsPath(); } @@ -193,9 +193,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core // check and skip it if (rawUrlInBytes.Length > 0 && rawUrlInBytes[rawUrlInBytes.Length - 1] == 0) { - var newRawUrlInBytes = new byte[rawUrlInBytes.Length - 1]; - Array.Copy(rawUrlInBytes, newRawUrlInBytes, newRawUrlInBytes.Length); - rawUrlInBytes = newRawUrlInBytes; + rawUrlInBytes = rawUrlInBytes.Slice(0, rawUrlInBytes.Length - 1); } var originalPath = RequestUriBuilder.DecodeAndUnescapePath(rawUrlInBytes); diff --git a/src/Shared/HttpSys/RequestProcessing/NativeRequestContext.cs b/src/Shared/HttpSys/RequestProcessing/NativeRequestContext.cs index b19bb1b4a6..26c01e4868 100644 --- a/src/Shared/HttpSys/RequestProcessing/NativeRequestContext.cs +++ b/src/Shared/HttpSys/RequestProcessing/NativeRequestContext.cs @@ -138,17 +138,14 @@ namespace Microsoft.AspNetCore.HttpSys.Internal return null; } - internal byte[] GetRawUrlInBytes() + internal Span GetRawUrlInBytes() { if (NativeRequest->pRawUrl != null && NativeRequest->RawUrlLength > 0) { - var result = new byte[NativeRequest->RawUrlLength]; - Marshal.Copy((IntPtr)NativeRequest->pRawUrl, result, 0, NativeRequest->RawUrlLength); - - return result; + return new Span(NativeRequest->pRawUrl, NativeRequest->RawUrlLength); } - return null; + return default; } internal CookedUrl GetCookedUrl() diff --git a/src/Shared/HttpSys/RequestProcessing/RawUrlHelper.cs b/src/Shared/HttpSys/RequestProcessing/RawUrlHelper.cs index ee0bf4a996..c1eb53997b 100644 --- a/src/Shared/HttpSys/RequestProcessing/RawUrlHelper.cs +++ b/src/Shared/HttpSys/RequestProcessing/RawUrlHelper.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; @@ -8,12 +8,10 @@ namespace Microsoft.AspNetCore.HttpSys.Internal { internal static class RawUrlHelper { - private static readonly byte[] _forwardSlashPath = Encoding.ASCII.GetBytes("/"); - /// /// Find the segment of the URI byte array which represents the path. /// - public static ArraySegment GetPath(byte[] raw) + public static Span GetPath(Span raw) { // performance var pathStartIndex = 0; @@ -38,12 +36,12 @@ namespace Microsoft.AspNetCore.HttpSys.Internal // and http.sys behavior: If the Uri contains a query, there must be at least one '/' // between the authority and the '?' character: It's safe to just look for the first // '/' after the authority to determine the beginning of the path. - pathStartIndex = Find(raw, authorityStartIndex, '/'); + pathStartIndex = Find(raw, authorityStartIndex, (byte)'/'); if (pathStartIndex == -1) { // e.g. for request lines like: 'GET http://myserver' (no final '/') // At this point we can return a path with a slash. - return new ArraySegment(_forwardSlashPath); + return default; } } else @@ -67,7 +65,7 @@ namespace Microsoft.AspNetCore.HttpSys.Internal scan++; } - return new ArraySegment(raw, pathStartIndex, scan - pathStartIndex); + return raw.Slice(pathStartIndex, scan - pathStartIndex); } /// @@ -75,7 +73,7 @@ namespace Microsoft.AspNetCore.HttpSys.Internal /// /// The byte array represents the raw URI /// Length of the matched bytes, 0 if it is not matched. - private static int FindHttpOrHttps(byte[] raw) + private static int FindHttpOrHttps(Span raw) { if (raw.Length < 7) { @@ -135,17 +133,14 @@ namespace Microsoft.AspNetCore.HttpSys.Internal } } - private static int Find(byte[] raw, int begin, char target) + private static int Find(Span raw, int begin, byte target) { - for (var idx = begin; idx < raw.Length; ++idx) + var idx = raw.Slice(begin).IndexOf(target); + if (idx != -1) { - if (raw[idx] == target) - { - return idx; - } + return idx + begin; } - - return -1; + return idx; } } } diff --git a/src/Shared/HttpSys/RequestProcessing/RequestUriBuilder.cs b/src/Shared/HttpSys/RequestProcessing/RequestUriBuilder.cs index 6308d6d8ea..9d3fe80cb9 100644 --- a/src/Shared/HttpSys/RequestProcessing/RequestUriBuilder.cs +++ b/src/Shared/HttpSys/RequestProcessing/RequestUriBuilder.cs @@ -1,7 +1,8 @@ -// 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; +using System.Diagnostics; using System.Text; namespace Microsoft.AspNetCore.HttpSys.Internal @@ -17,23 +18,19 @@ namespace Microsoft.AspNetCore.HttpSys.Internal encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); - public static string DecodeAndUnescapePath(byte[] rawUrlBytes) + public static string DecodeAndUnescapePath(Span rawUrlBytes) { - if (rawUrlBytes == null) - { - throw new ArgumentNullException(nameof(rawUrlBytes)); - } - - if (rawUrlBytes.Length == 0) - { - throw new ArgumentException("Length of the URL cannot be zero.", nameof(rawUrlBytes)); - } - + Debug.Assert(rawUrlBytes.Length == 0, "Length of the URL cannot be zero."); var rawPath = RawUrlHelper.GetPath(rawUrlBytes); + if (rawPath.Length == 0) + { + return "/"; + } + var unescapedPath = Unescape(rawPath); - return UTF8.GetString(unescapedPath.Array, unescapedPath.Offset, unescapedPath.Count); + return UTF8.GetString(unescapedPath); } /// @@ -41,19 +38,16 @@ namespace Microsoft.AspNetCore.HttpSys.Internal /// /// The raw path string to be unescaped /// The unescaped path string - private static ArraySegment Unescape(ArraySegment rawPath) + private static ReadOnlySpan Unescape(Span rawPath) { // the slot to read the input - var reader = rawPath.Offset; + var reader = 0; // the slot to write the unescaped byte - var writer = rawPath.Offset; + var writer = 0; // the end of the path - var end = rawPath.Offset + rawPath.Count; - - // the byte array - var buffer = rawPath.Array; + var end = rawPath.Length; while (true) { @@ -62,7 +56,7 @@ namespace Microsoft.AspNetCore.HttpSys.Internal break; } - if (rawPath.Array[reader] == '%') + if (rawPath[reader] == '%') { var decodeReader = reader; @@ -73,20 +67,20 @@ namespace Microsoft.AspNetCore.HttpSys.Internal // The decodeReader iterator is always moved to the first byte not yet // be scanned after the process. A failed decoding means the chars // between the reader and decodeReader can be copied to output untouched. - if (!DecodeCore(ref decodeReader, ref writer, end, buffer)) + if (!DecodeCore(ref decodeReader, ref writer, end, rawPath)) { - Copy(reader, decodeReader, ref writer, buffer); + Copy(reader, decodeReader, ref writer, rawPath); } reader = decodeReader; } else { - buffer[writer++] = buffer[reader++]; + rawPath[writer++] = rawPath[reader++]; } } - return new ArraySegment(buffer, rawPath.Offset, writer - rawPath.Offset); + return rawPath.Slice(0, writer); } /// @@ -96,7 +90,7 @@ namespace Microsoft.AspNetCore.HttpSys.Internal /// The place to write to /// The end of the buffer /// The byte array - private static bool DecodeCore(ref int reader, ref int writer, int end, byte[] buffer) + private static bool DecodeCore(ref int reader, ref int writer, int end, Span buffer) { // preserves the original head. if the percent-encodings cannot be interpreted as sequence of UTF-8 octets, // bytes from this till the last scanned one will be copied to the memory pointed by writer. @@ -232,7 +226,7 @@ namespace Microsoft.AspNetCore.HttpSys.Internal return true; } - private static void Copy(int begin, int end, ref int writer, byte[] buffer) + private static void Copy(int begin, int end, ref int writer, Span buffer) { while (begin != end) { @@ -260,7 +254,7 @@ namespace Microsoft.AspNetCore.HttpSys.Internal /// The end of the buffer /// The byte array /// The unescaped byte if success. Otherwise return -1. - private static int? UnescapePercentEncoding(ref int scan, int end, byte[] buffer) + private static int? UnescapePercentEncoding(ref int scan, int end, ReadOnlySpan buffer) { if (buffer[scan++] != '%') { @@ -300,7 +294,7 @@ namespace Microsoft.AspNetCore.HttpSys.Internal /// The end of the buffer /// The byte array /// The hexadecimal value if successes, otherwise -1. - private static int? ReadHex(ref int scan, int end, byte[] buffer) + private static int? ReadHex(ref int scan, int end, ReadOnlySpan buffer) { if (scan == end) {