Skip to content

Commit

Permalink
Cache Uri.IdnHost and Uri.PathAndQuery (dotnet#36460)
Browse files Browse the repository at this point in the history
* Cache Uri.IdnHost and Uri.PathAndQuery

* Continue caching DnsSafeHost

* Move PathAndQuery cache from MoreInfo to UriInfo

* Move DnsSafeHost and IdnHost logic to IdnHost getter

* Add more Host tests
  • Loading branch information
MihaZupan authored May 20, 2020
1 parent 66e6fda commit 54db4a3
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 52 deletions.
122 changes: 70 additions & 52 deletions src/libraries/System.Private.Uri/src/System/Uri.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,11 +141,16 @@ internal void DebugAssertInCtor()

private class UriInfo
{
public string? Host;
public string? ScopeId; //only IP v6 may need this
public string? String;
public Offset Offset;
public string? DnsSafeHost; // stores dns safe host when idn is on and we have unicode or idn host
public string? String;
public string? Host;
public string? IdnHost;
public string? PathAndQuery;

/// <summary>
/// Only IP v6 may need this
/// </summary>
public string? ScopeId;

private MoreInfo? _moreInfo;
public MoreInfo MoreInfo
Expand Down Expand Up @@ -321,8 +326,9 @@ private void EnsureParseRemaining()

private void EnsureHostString(bool allowDnsOptimization)
{
EnsureUriInfo();
if ((object?)_info.Host == null)
UriInfo info = EnsureUriInfo();

if (info.Host is null)
{
if (allowDnsOptimization && InFact(Flags.CanonicalDnsHost))
{
Expand Down Expand Up @@ -838,16 +844,23 @@ public string PathAndQuery
throw new InvalidOperationException(SR.net_uri_NotAbsolute);
}

string result = GetParts(UriComponents.PathAndQuery, UriFormat.UriEscaped);
//
// Compatibility:
// Remove the first slash from a Dos Path if it's present
//
if (IsDosPath && result[0] == '/')
UriInfo info = EnsureUriInfo();

if (info.PathAndQuery is null)
{
result = result.Substring(1);
string result = GetParts(UriComponents.PathAndQuery, UriFormat.UriEscaped);

// Compatibility:
// Remove the first slash from a Dos Path if it's present
if (IsDosPath && result[0] == '/')
{
result = result.Substring(1);
}

info.PathAndQuery = result;
}
return result;

return info.PathAndQuery;
}
}

Expand Down Expand Up @@ -1129,44 +1142,15 @@ public string DnsSafeHost

EnsureHostString(false);

if (_info.DnsSafeHost != null)
{
// Cached
return _info.DnsSafeHost;
}
else if (_info.Host!.Length == 0)
{
// Empty host, no possible processing
return string.Empty;
}

// Special case, will include ScopeID and strip [] around IPv6
// This will also unescape the host string
string ret = _info.Host;

if (HostType == Flags.IPv6HostType)
Flags hostType = HostType;
if (hostType == Flags.IPv6HostType || (hostType == Flags.BasicHostType && InFact(Flags.HostNotCanonical | Flags.E_HostNotCanonical)))
{
ret = _info.ScopeId != null ?
string.Concat(ret.AsSpan(1, ret.Length - 2), _info.ScopeId) :
ret.Substring(1, ret.Length - 2);
return IdnHost;
}
// Validate that this basic host qualifies as Dns safe,
// It has looser parsing rules that might allow otherwise.
// It might be a registry-based host from RFC 2396 Section 3.2.1
else if (HostType == Flags.BasicHostType
&& InFact(Flags.HostNotCanonical | Flags.E_HostNotCanonical))
else
{
// Unescape everything
char[] dest = new char[ret.Length];
int count = 0;
UriHelper.UnescapeString(ret, 0, ret.Length, dest, ref count, c_DummyChar, c_DummyChar,
c_DummyChar, UnescapeMode.Unescape | UnescapeMode.UnescapeAll, _syntax, false);
ret = new string(dest, 0, count);
return _info.Host!;
}

_info.DnsSafeHost = ret;

return ret;
}
}

Expand All @@ -1175,14 +1159,48 @@ public string IdnHost
{
get
{
string host = this.DnsSafeHost;
if (IsNotAbsoluteUri)
{
throw new InvalidOperationException(SR.net_uri_NotAbsolute);
}

if (HostType == Flags.DnsHostType)
if (_info?.IdnHost is null)
{
host = DomainNameHelper.IdnEquivalent(host);
EnsureHostString(false);

string host = _info!.Host!;

Flags hostType = HostType;
if (hostType == Flags.DnsHostType)
{
host = DomainNameHelper.IdnEquivalent(host);
}
else if (hostType == Flags.IPv6HostType)
{
host = _info.ScopeId != null ?
string.Concat(host.AsSpan(1, host.Length - 2), _info.ScopeId) :
host.Substring(1, host.Length - 2);
}
// Validate that this basic host qualifies as Dns safe,
// It has looser parsing rules that might allow otherwise.
// It might be a registry-based host from RFC 2396 Section 3.2.1
else if (hostType == Flags.BasicHostType && InFact(Flags.HostNotCanonical | Flags.E_HostNotCanonical))
{
// Unescape everything
ValueStringBuilder dest = new ValueStringBuilder(stackalloc char[256]);

UriHelper.UnescapeString(host, 0, host.Length, ref dest,
c_DummyChar, c_DummyChar, c_DummyChar,
UnescapeMode.Unescape | UnescapeMode.UnescapeAll,
_syntax, isQuery: false);

host = dest.ToString();
}

_info.IdnHost = host;
}

return host;
return _info.IdnHost;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,5 +77,53 @@ public void IdnDnsSafeHost_MultiLabelAllExceptIntranet_Punycode()
Assert.Equal("xn--pck.com", test.IdnHost);
Assert.Equal("https://\u30AF.com/", test.AbsoluteUri);
}

[Theory]
[InlineData("foo", "foo", "foo", "foo")]
[InlineData("BAR", "bar", "bar", "bar")]
[InlineData("\u00FC", "\u00FC", "\u00FC", "xn--tda")]
[InlineData("\u00FC.\u00FC", "\u00FC.\u00FC", "\u00FC.\u00FC", "xn--tda.xn--tda")]
[InlineData("\u00FC.foo.\u00FC", "\u00FC.foo.\u00FC", "\u00FC.foo.\u00FC", "xn--tda.foo.xn--tda")]
[InlineData("xn--tda", "xn--tda", "xn--tda", "xn--tda")]
[InlineData("xn--tda.xn--tda", "xn--tda.xn--tda", "xn--tda.xn--tda", "xn--tda.xn--tda")]
[InlineData("127.0.0.1", "127.0.0.1", "127.0.0.1", "127.0.0.1")]
[InlineData("127.0.o.1", "127.0.o.1", "127.0.o.1", "127.0.o.1")]
[InlineData("127.0.0.1.1", "127.0.0.1.1", "127.0.0.1.1", "127.0.0.1.1")]
[InlineData("[::]", "[::]", "::", "::")]
[InlineData("[123::]", "[123::]", "123::", "123::")]
[InlineData("[123:123::]", "[123:123::]", "123:123::", "123:123::")]
[InlineData("[123:123::%]", "[123:123::]", "123:123::%", "123:123::%")]
[InlineData("[123:123::%foo]", "[123:123::]", "123:123::%foo", "123:123::%foo")]
[InlineData("[123:123::%foo%20bar]", "[123:123::]", "123:123::%foo%20bar", "123:123::%foo%20bar")]
public void Host_DnsSafeHost_IdnHost_ProcessedCorrectly(string hostString, string host, string dnsSafeHost, string idnHost)
{
Asserts($"wss://{hostString}", host, dnsSafeHost, idnHost);
Asserts($"wss://{hostString}:1", host, dnsSafeHost, idnHost);
Asserts($"http://{hostString}", host, dnsSafeHost, idnHost);
Asserts($"http://{hostString}:1", host, dnsSafeHost, idnHost);
Asserts($"https://{hostString}", host, dnsSafeHost, idnHost);
Asserts($"https://{hostString}:1", host, dnsSafeHost, idnHost);

Asserts($"\\\\{hostString}", host, dnsSafeHost, idnHost);
Asserts($"file:////{hostString}", host, dnsSafeHost, idnHost);

static void Asserts(string uriString, string host, string dnsSafeHost, string idnHost)
{
var uri = new Uri(uriString);

Assert.Equal(host, uri.Host);
Assert.Equal(dnsSafeHost, uri.DnsSafeHost);
Assert.Equal(idnHost, uri.IdnHost);

if (host == dnsSafeHost)
{
Assert.Same(uri.Host, uri.DnsSafeHost);
}
else
{
Assert.Same(uri.DnsSafeHost, uri.IdnHost);
}
}
}
}
}
21 changes: 21 additions & 0 deletions src/libraries/System.Private.Uri/tests/FunctionalTests/UriTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,27 @@ public static void Uri_EmptyPort_UsesDefaultPort()
Assert.Equal(80, u.Port);
}

[Fact]
public static void Uri_CachesIdnHost()
{
var uri = new Uri("https://\u00FCnicode/foo");
Assert.Same(uri.IdnHost, uri.IdnHost);
}

[Fact]
public static void Uri_CachesPathAndQuery()
{
var uri = new Uri("https://foo/bar?one=two");
Assert.Same(uri.PathAndQuery, uri.PathAndQuery);
}

[Fact]
public static void Uri_CachesDnsSafeHost()
{
var uri = new Uri("https://[::]/bar");
Assert.Same(uri.DnsSafeHost, uri.DnsSafeHost);
}

[Fact]
public static void Uri_DoesNotLockOnString()
{
Expand Down

0 comments on commit 54db4a3

Please sign in to comment.