From 8a7b83247ac4d0a39a40fd86e55df63d85a093fa Mon Sep 17 00:00:00 2001 From: David Lievrouw Date: Fri, 28 Aug 2020 12:35:34 +0200 Subject: [PATCH] #12 UrlDecode performance optimization. --- .../ExtensionTests.Uri.cs | 14 ++ src/HttpMessageSigning/Extensions.Uri.cs | 131 +++++++++++++----- 2 files changed, 108 insertions(+), 37 deletions(-) diff --git a/src/HttpMessageSigning.Tests/ExtensionTests.Uri.cs b/src/HttpMessageSigning.Tests/ExtensionTests.Uri.cs index b049996d..da1b1ccc 100644 --- a/src/HttpMessageSigning.Tests/ExtensionTests.Uri.cs +++ b/src/HttpMessageSigning.Tests/ExtensionTests.Uri.cs @@ -64,6 +64,13 @@ public class UrlEncode : ForUri { actual.Should().Be("https://dalion.eu/api/%7BBrooks%7D%20was%20here?query%20string=%7BBrooks%7D"); } + [Fact] + public void CorrectlyEncodesMultipleQueryString() { + var encoded = new Uri("https://dalion.eu/api/{Brooks} was here?query string={Brooks}&id=42?", UriKind.Absolute); + var actual = encoded.UrlEncode(); + actual.Should().Be("https://dalion.eu/api/%7BBrooks%7D%20was%20here?query%20string=%7BBrooks%7D&id=42%3F"); + } + [Fact] public void AlsoEncodesQueryStringWithoutValue() { var encoded = new Uri("https://dalion.eu/api/{Brooks} was here?query string", UriKind.Absolute); @@ -71,6 +78,13 @@ public class UrlEncode : ForUri { actual.Should().Be("https://dalion.eu/api/%7BBrooks%7D%20was%20here?query%20string"); } + [Fact] + public void DropsEmptyQueryString() { + var encoded = new Uri("https://dalion.eu/api/{Brooks} was here?", UriKind.Absolute); + var actual = encoded.UrlEncode(); + actual.Should().Be("https://dalion.eu/api/%7BBrooks%7D%20was%20here"); + } + [Fact] public void DoesNotEncodeEncodedStringAgain() { var encoded = new Uri("https://dalion.eu/api/%7BBrooks%7D%20was%20here/api/David%20%26%20Partners%20%2B%20Siebe%20at%20100%25%20%2A%20co.?query%20string=%7BBrooks%7D", UriKind.Absolute); diff --git a/src/HttpMessageSigning/Extensions.Uri.cs b/src/HttpMessageSigning/Extensions.Uri.cs index e1f016df..49e53914 100644 --- a/src/HttpMessageSigning/Extensions.Uri.cs +++ b/src/HttpMessageSigning/Extensions.Uri.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Text; @@ -13,27 +14,22 @@ public static partial class Extensions { if (decoded == null) return null; var isAbsolute = decoded.IsAbsoluteUri; - var absoluteUri = isAbsolute - ? decoded - : new Uri("https://dalion.eu/" + decoded.OriginalString.TrimStart('/'), UriKind.Absolute); - var decodedPath = absoluteUri.GetComponents(UriComponents.Path, UriFormat.Unescaped); - var decodedQuery = absoluteUri.GetComponents(UriComponents.Query, UriFormat.Unescaped); - - var pathSegments = decodedPath - .Split(new[] {"/"}, StringSplitOptions.None) - .Select(Uri.EscapeDataString); - var path = string.Join("/", pathSegments); - - var queryStringCollection = ExtractQueryString(decodedQuery); - var qsSegments = queryStringCollection - ?.AllKeys - .Select(key => { - var val = queryStringCollection[key]; - return string.IsNullOrEmpty(val) - ? Uri.EscapeDataString(key) - : Uri.EscapeDataString(key) + "=" + Uri.EscapeDataString(val); - }); - var queryString = qsSegments == null ? string.Empty : string.Join("&", qsSegments); + var originalString = decoded.OriginalString; + + var decodedPath = isAbsolute + ? decoded.GetComponents(UriComponents.Path, UriFormat.Unescaped).TrimStart('/') + : FastSplitInTwo(originalString, separator: '?')[0].TrimStart('/'); + + string decodedQuery = null; + if (originalString.IndexOf(value: '?') > -1) { + decodedQuery = isAbsolute + ? decoded.GetComponents(UriComponents.Query, UriFormat.Unescaped) + : FastSplitInTwo(FastSplitInTwo(originalString, separator: '?')[1], separator: '#')[0]; + } + + var pathSegments = FastSplit(decodedPath, separator: '/').Select(Uri.EscapeDataString); + + var queryString = ExtractQueryString(decodedQuery)?.ToString(); var sb = new StringBuilder(); if (isAbsolute) { @@ -41,13 +37,15 @@ public static partial class Extensions { sb.Append("://"); sb.Append(decoded.Host); if (!decoded.IsDefaultPort) { - sb.Append(':'); + sb.Append(value: ':'); sb.Append(decoded.Port); } + sb.Append(value: '/'); } - if (isAbsolute || decoded.OriginalString.StartsWith("/", StringComparison.Ordinal)) sb.Append('/'); - sb.Append(path); + if (!isAbsolute && originalString.StartsWith("/", StringComparison.Ordinal)) sb.Append(value: '/'); + sb.Append(string.Join("/", pathSegments)); + if (!string.IsNullOrEmpty(queryString)) { sb.Append("?" + queryString); } @@ -55,21 +53,80 @@ public static partial class Extensions { return sb.ToString(); } - private static System.Collections.Specialized.NameValueCollection ExtractQueryString(string decodedQuery) { - if (string.IsNullOrEmpty(decodedQuery)) return null; - - System.Collections.Specialized.NameValueCollection result = null; - var query = decodedQuery.Split('#')[0]; - var pairs = query.Split('&'); - foreach (var pair in pairs) { - if (result == null) result = new System.Collections.Specialized.NameValueCollection(); - var parts = pair.Split(new[] {'='}, count: 2); - var name = parts[0]; - var value = parts.Length == 1 ? string.Empty : parts[1]; - result.Add(name, value); + private static StringBuilder ExtractQueryString(string decodedQuery) { + if (decodedQuery == null) return null; + if (decodedQuery == string.Empty) return null; + + var query = decodedQuery.IndexOf(value: '#') < 0 + ? decodedQuery + : FastSplitInTwo(decodedQuery, separator: '#')[0]; + + var pairs = query.IndexOf(value: '&') < 0 + ? (object)query + : (object)FastSplit(query, separator: '&'); + + var sb = new StringBuilder(); + + if (pairs is string str) { + var parts = FastSplitInTwo(str, separator: '='); + + sb.Append(Uri.EscapeDataString(parts[0])); + if (parts.Length > 1) { + sb.Append('='); + sb.Append(Uri.EscapeDataString(parts[1])); + } + + return sb; } - return result; + var isFirst = true; + foreach (var pair in (IEnumerable) pairs) { + var parts = FastSplitInTwo(pair, separator: '='); + + if (!isFirst) sb.Append('&'); + + sb.Append(Uri.EscapeDataString(parts[0])); + if (parts.Length > 1) { + sb.Append('='); + sb.Append(Uri.EscapeDataString(parts[1])); + } + + isFirst = false; + } + + return sb; + } + + private static List FastSplit(string input, char separator) { + var span = input.AsSpan(); + + var index = span.IndexOf(separator); + + var items = new List(); + while (index > -1) { + var item = span.Slice(start: 0, index).ToString(); + span = span.Slice(index + 1); + items.Add(item); + index = span.IndexOf(separator); + } + + items.Add(span.ToString()); + + return items; + } + + private static string[] FastSplitInTwo(string input, char separator) { + var span = input.AsSpan(); + + var index = span.IndexOf(separator); + if (index < 0) return new[] {input}; + + var part1 = span.Slice(start: 0, index).ToString(); + span = span.Slice(index + 1); + + var part2 = span.ToString(); + + return new[] {part1, part2}; } } } \ No newline at end of file