Skip to content

Commit

Permalink
#12 UrlDecode performance optimization.
Browse files Browse the repository at this point in the history
  • Loading branch information
David Lievrouw authored and David Lievrouw committed Aug 28, 2020
1 parent f031e82 commit 8a7b832
Show file tree
Hide file tree
Showing 2 changed files with 108 additions and 37 deletions.
14 changes: 14 additions & 0 deletions src/HttpMessageSigning.Tests/ExtensionTests.Uri.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,27 @@ 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);
var actual = encoded.UrlEncode();
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);
Expand Down
131 changes: 94 additions & 37 deletions src/HttpMessageSigning/Extensions.Uri.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

Expand All @@ -13,63 +14,119 @@ 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) {
sb.Append(decoded.Scheme);
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);
}

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<string>) 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<string> FastSplit(string input, char separator) {
var span = input.AsSpan();

var index = span.IndexOf(separator);

var items = new List<string>();
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};
}
}
}

0 comments on commit 8a7b832

Please sign in to comment.