Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use string.Create to build absolute URI #29448

Merged
2 commits merged into from
Jan 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 72 additions & 15 deletions src/Http/Http.Extensions/src/UriHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Text;

namespace Microsoft.AspNetCore.Http.Extensions
Expand All @@ -15,6 +17,7 @@ public static class UriHelper
private const char Hash = '#';
private const char QuestionMark = '?';
private static readonly string SchemeDelimiter = Uri.SchemeDelimiter;
private static readonly SpanAction<char, (string scheme, string host, string pathBase, string path, string query, string fragment)> InitializeAbsoluteUriStringSpanAction = new(InitializeAbsoluteUriString);

/// <summary>
/// Combines the given URI components into a string that is properly encoded for use in HTTP headers.
Expand Down Expand Up @@ -58,24 +61,36 @@ public static string BuildAbsolute(
throw new ArgumentNullException(nameof(scheme));
}

var combinedPath = (pathBase.HasValue || path.HasValue) ? (pathBase + path).ToString() : "/";
paulomorgado marked this conversation as resolved.
Show resolved Hide resolved
var hostText = host.ToUriComponent();
var pathBaseText = pathBase.ToUriComponent();
var pathText = path.ToUriComponent();
var queryText = query.ToUriComponent();
var fragmentText = fragment.ToUriComponent();

var encodedHost = host.ToString();
var encodedQuery = query.ToString();
var encodedFragment = fragment.ToString();
// PERF: Calculate string length to allocate correct buffer size for string.Create.
var length =
scheme.Length +
Uri.SchemeDelimiter.Length +
hostText.Length +
pathBaseText.Length +
pathText.Length +
queryText.Length +
fragmentText.Length;

// PERF: Calculate string length to allocate correct buffer size for StringBuilder.
var length = scheme.Length + SchemeDelimiter.Length + encodedHost.Length
+ combinedPath.Length + encodedQuery.Length + encodedFragment.Length;
if (string.IsNullOrEmpty(pathBaseText) && string.IsNullOrEmpty(pathText))
{
pathText = "/";
length++;
}
else if (pathBaseText.Length > 0 && pathBaseText[^1] == '/')
paulomorgado marked this conversation as resolved.
Show resolved Hide resolved
{
// If the path string has a trailing slash and the other string has a leading slash, we need
// to trim one of them.
// Just decrement the total lenght, for now.
length--;
}

return new StringBuilder(length)
.Append(scheme)
.Append(SchemeDelimiter)
.Append(encodedHost)
.Append(combinedPath)
.Append(encodedQuery)
.Append(encodedFragment)
.ToString();
return string.Create(length, (scheme, hostText, pathBaseText, pathText, queryText, fragmentText), InitializeAbsoluteUriStringSpanAction);
}

/// <summary>
Expand Down Expand Up @@ -214,5 +229,47 @@ public static string GetDisplayUrl(this HttpRequest request)
.Append(queryString)
.ToString();
}

/// <summary>
/// Copies the specified <paramref name="text"/> to the specified <paramref name="buffer"/> starting at the specified <paramref name="index"/>.
/// </summary>
/// <param name="buffer">The buffer to copy text to.</param>
/// <param name="index">The buffer start index.</param>
/// <param name="text">The text to copy.</param>
/// <returns></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int CopyTextToBuffer(Span<char> buffer, int index, ReadOnlySpan<char> text)
{
text.CopyTo(buffer.Slice(index, text.Length));
return index + text.Length;
}

/// <summary>
/// Initializes the URI <see cref="string"/> for <see cref="BuildAbsolute(string, HostString, PathString, PathString, QueryString, FragmentString)"/>.
/// </summary>
/// <param name="buffer">The URI <see cref="string"/>'s <see cref="char"/> buffer.</param>
/// <param name="uriParts">The URI parts.</param>
private static void InitializeAbsoluteUriString(Span<char> buffer, (string scheme, string host, string pathBase, string path, string query, string fragment) uriParts)
{
var index = 0;

var pathBaseSpan = uriParts.pathBase.AsSpan();

if (uriParts.path.Length > 0 && pathBaseSpan.Length > 0 && pathBaseSpan[^1] == '/')
{
// If the path string has a trailing slash and the other string has a leading slash, we need
// to trim one of them.
// Trim the last slahs from pathBase. The total lenght was decremented before the call to string.Create.
pathBaseSpan = pathBaseSpan[..^1];
}

index = CopyTextToBuffer(buffer, index, uriParts.scheme.AsSpan());
index = CopyTextToBuffer(buffer, index, Uri.SchemeDelimiter.AsSpan());
index = CopyTextToBuffer(buffer, index, uriParts.host.AsSpan());
index = CopyTextToBuffer(buffer, index, pathBaseSpan);
index = CopyTextToBuffer(buffer, index, uriParts.path.AsSpan());
index = CopyTextToBuffer(buffer, index, uriParts.query.AsSpan());
_ = CopyTextToBuffer(buffer, index, uriParts.fragment.AsSpan());
}
}
}
40 changes: 40 additions & 0 deletions src/Http/Http.Extensions/test/UriHelperTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,46 @@ public void EncodeFullUrl()
Assert.Equal("http://my.xn--host-cpd:80/un%3Fescaped/base/un%3Fescaped?name=val%23ue#my%20value", result);
}

[Theory]
[InlineData("http", "example.com", "", "", "", "", "http://example.com/")]
[InlineData("https", "example.com", "", "", "", "", "https://example.com/")]
[InlineData("http", "example.com", "", "/foo/bar", "", "", "http://example.com/foo/bar")]
[InlineData("http", "example.com", "", "/foo/bar", "?baz=1", "", "http://example.com/foo/bar?baz=1")]
[InlineData("http", "example.com", "", "/foo", "", "#col=2", "http://example.com/foo#col=2")]
[InlineData("http", "example.com", "", "/foo", "?bar=1", "#col=2", "http://example.com/foo?bar=1#col=2")]
[InlineData("http", "example.com", "/base", "/foo", "?bar=1", "#col=2", "http://example.com/base/foo?bar=1#col=2")]
[InlineData("http", "example.com", "/base/", "/foo", "?bar=1", "#col=2", "http://example.com/base/foo?bar=1#col=2")]
[InlineData("http", "example.com", "", "", "?bar=1", "#col=2", "http://example.com/?bar=1#col=2")]
[InlineData("http", "example.com", "", "", "", "#frag?stillfrag/stillfrag", "http://example.com/#frag?stillfrag/stillfrag")]
[InlineData("http", "example.com", "", "", "?q/stillq", "#frag?stillfrag/stillfrag", "http://example.com/?q/stillq#frag?stillfrag/stillfrag")]
[InlineData("http", "example.com", "", "/fo#o", "", "#col=2", "http://example.com/fo%23o#col=2")]
[InlineData("http", "example.com", "", "/fo?o", "", "#col=2", "http://example.com/fo%3Fo#col=2")]
[InlineData("ftp", "example.com", "", "/", "", "", "ftp://example.com/")]
[InlineData("ftp", "example.com", "/", "/", "", "", "ftp://example.com/")]
[InlineData("https", "127.0.0.0:80", "", "/bar", "", "", "https://127.0.0.0:80/bar")]
[InlineData("http", "[1080:0:0:0:8:800:200C:417A]", "", "/index.html", "", "", "http://[1080:0:0:0:8:800:200C:417A]/index.html")]
[InlineData("http", "example.com", "", "///", "", "", "http://example.com///")]
[InlineData("http", "example.com", "///", "///", "", "", "http://example.com/////")]
public void BuildAbsoluteGenerationChecks(
string scheme,
string host,
string pathBase,
string path,
string query,
string fragment,
string expectedUri)
{
var uri = UriHelper.BuildAbsolute(
scheme,
new HostString(host),
new PathString(pathBase),
new PathString(path),
new QueryString(query),
new FragmentString(fragment));

Assert.Equal(expectedUri, uri);
}

[Fact]
public void GetEncodedUrlFromRequest()
{
Expand Down