Skip to content
91 changes: 50 additions & 41 deletions src/Http/Http.Extensions/src/UriHelper.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

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

namespace Microsoft.AspNetCore.Http.Extensions;
Expand All @@ -17,7 +15,6 @@ 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 @@ -90,7 +87,41 @@ public static string BuildAbsolute(
length--;
}

return string.Create(length, (scheme, hostText, pathBaseText, pathText, queryText, fragmentText), InitializeAbsoluteUriStringSpanAction);
return string.Create(
length,
new UriComponents(scheme, hostText, pathBaseText, pathText, queryText, fragmentText),
static (buffer, uriComponents) =>
{
uriComponents.Scheme.AsSpan().CopyTo(buffer);
buffer = buffer.Slice(uriComponents.Scheme.Length);

Uri.SchemeDelimiter.CopyTo(buffer);
buffer = buffer.Slice(Uri.SchemeDelimiter.Length);

uriComponents.Host.AsSpan().CopyTo(buffer);
buffer = buffer.Slice(uriComponents.Host.Length);

var pathBaseSpan = uriComponents.PathBase.AsSpan();

if (uriComponents.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 slash from pathBase. The total length was decremented before the call to string.Create.
pathBaseSpan = pathBaseSpan[..^1];
}

pathBaseSpan.CopyTo(buffer);
buffer = buffer.Slice(pathBaseSpan.Length);

uriComponents.Path.CopyTo(buffer);
buffer = buffer.Slice(uriComponents.Path.Length);

uriComponents.Query.CopyTo(buffer);
buffer = buffer.Slice(uriComponents.Query.Length);

uriComponents.Fragment.CopyTo(buffer);
});
}

/// <summary>
Expand Down Expand Up @@ -171,7 +202,7 @@ public static string Encode(Uri uri)
}
else
{
return uri.GetComponents(UriComponents.SerializationInfoString, UriFormat.UriEscaped);
return uri.GetComponents(System.UriComponents.SerializationInfoString, UriFormat.UriEscaped);
}
}

Expand Down Expand Up @@ -224,45 +255,23 @@ public static string GetDisplayUrl(this HttpRequest request)
.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)
private readonly struct UriComponents
{
var index = 0;

var pathBaseSpan = uriParts.pathBase.AsSpan();
public readonly string Scheme;
public readonly string Host;
public readonly string PathBase;
public readonly string Path;
public readonly string Query;
public readonly string Fragment;

if (uriParts.path.Length > 0 && pathBaseSpan.Length > 0 && pathBaseSpan[^1] == '/')
public UriComponents(string scheme, string host, string pathBase, string path, string query, string fragment)
{
// 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 length was decremented before the call to string.Create.
pathBaseSpan = pathBaseSpan[..^1];
Scheme = scheme;
Host = host;
PathBase = pathBase;
Path = path;
Query = query;
Fragment = fragment;
}

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());
}
}
Loading