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

Add Uri : ISpanFormattable #88012

Merged
merged 2 commits into from
Jun 25, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,14 @@ public override string ToString()
sb.Append(_method);

sb.Append(", RequestUri: '");
sb.Append(_requestUri == null ? "<null>" : _requestUri.ToString());
if (_requestUri is null)
{
sb.Append("<null>");
}
else
{
sb.Append($"{_requestUri}");
}

sb.Append("', Version: ");
sb.Append(_version);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1673,17 +1673,27 @@ private static void CheckExplicitDataContractNamespaceUri(string dataContractNs,
{
string trimmedNs = dataContractNs.Trim();
// Code similar to XmlConvert.ToUri (string.Empty is a valid uri but not " ")
if (trimmedNs.Length == 0 || trimmedNs.IndexOf("##", StringComparison.Ordinal) != -1)
if (trimmedNs.Length == 0 || trimmedNs.Contains("##", StringComparison.Ordinal))
{
ThrowInvalidDataContractException(SR.Format(SR.DataContractNamespaceIsNotValid, dataContractNs), type);
}

dataContractNs = trimmedNs;
}
if (Uri.TryCreate(dataContractNs, UriKind.RelativeOrAbsolute, out Uri? uri))
{
if (uri.ToString() == Globals.SerializationNamespace)
Span<char> formatted = stackalloc char[Globals.SerializationNamespace.Length];
if (uri.TryFormat(formatted, out int charsWritten) &&
charsWritten == Globals.SerializationNamespace.Length &&
formatted.SequenceEqual(Globals.SerializationNamespace))
{
ThrowInvalidDataContractException(SR.Format(SR.DataContractNamespaceReserved, Globals.SerializationNamespace), type);
}
}
else
{
ThrowInvalidDataContractException(SR.Format(SR.DataContractNamespaceIsNotValid, dataContractNs), type);
}
}

internal static string GetClrTypeFullName(Type type)
Expand Down
143 changes: 123 additions & 20 deletions src/libraries/System.Private.Uri/src/System/Uri.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ namespace System
{
[Serializable]
[System.Runtime.CompilerServices.TypeForwardedFrom("System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")]
public partial class Uri : ISerializable
public partial class Uri : ISpanFormattable, ISerializable
{
public static readonly string UriSchemeFile = UriParser.FileUri.SchemeName;
public static readonly string UriSchemeFtp = UriParser.FtpUri.SchemeName;
Expand Down Expand Up @@ -46,7 +46,7 @@ public partial class Uri : ISerializable
// or idn is on and we have unicode host or idn host
// In that case, this string is normalized, stripped of bidi chars, and validated
// with char limits
private string _string = null!; // initialized early in ctor via a helper
private string _string;

// untouched user string if string has unicode with iri on or unicode/idn host with idn on
private string _originalUnicodeString = null!; // initialized in ctor via helper
Expand Down Expand Up @@ -319,6 +319,7 @@ private static bool StaticInFact(Flags allFlags, Flags checkFlags)
return (allFlags & checkFlags) != 0;
}

[MemberNotNull(nameof(_info))]
private UriInfo EnsureUriInfo()
{
Flags cF = _flags;
Expand All @@ -338,6 +339,7 @@ private void EnsureParseRemaining()
}
}

[MemberNotNull(nameof(_info))]
private void EnsureHostString(bool allowDnsOptimization)
{
UriInfo info = EnsureUriInfo();
Expand Down Expand Up @@ -497,6 +499,7 @@ protected void GetObjectData(SerializationInfo serializationInfo, StreamingConte
}
}

[MemberNotNull(nameof(_string))]
private void CreateUri(Uri baseUri, string? relativeUri, bool dontEscape)
{
DebugAssertInCtor();
Expand Down Expand Up @@ -1173,7 +1176,7 @@ public string IdnHost
{
EnsureHostString(false);

string host = _info!.Host!;
string host = _info.Host!;

Flags hostType = HostType;
if (hostType == Flags.DnsHostType)
Expand Down Expand Up @@ -1538,8 +1541,6 @@ public override int GetHashCode()
//
// ToString
//
// The better implementation would be just
//
private const UriFormat V1ToStringUnescape = (UriFormat)0x7FFF;

public override string ToString()
Expand All @@ -1550,16 +1551,93 @@ public override string ToString()
}

EnsureUriInfo();
if (_info.String is null)
return _info.String ??=
_syntax.IsSimple ?
GetComponentsHelper(UriComponents.AbsoluteUri, V1ToStringUnescape) :
GetParts(UriComponents.AbsoluteUri, UriFormat.SafeUnescaped);
}

/// <summary>
/// Attempts to format a canonical string representation for the <see cref="Uri"/> instance into the specified span.
/// </summary>
/// <param name="destination">The span into which to write this instance's value formatted as a span of characters.</param>
/// <param name="charsWritten">When this method returns, contains the number of characters that were written in <paramref name="destination"/>.</param>
/// <returns><see langword="true"/> if the formatting was successful; otherwise, <see langword="false"/>.</returns>
public bool TryFormat(Span<char> destination, out int charsWritten)
{
ReadOnlySpan<char> result;

if (_syntax is null)
{
if (_syntax.IsSimple)
_info.String = GetComponentsHelper(UriComponents.AbsoluteUri, V1ToStringUnescape);
result = _string;
}
else
{
EnsureUriInfo();
if (_info.String is not null)
{
result = _info.String;
}
else
_info.String = GetParts(UriComponents.AbsoluteUri, UriFormat.SafeUnescaped);
{
UriFormat uriFormat = V1ToStringUnescape;
if (!_syntax.IsSimple)
{
if (IsNotAbsoluteUri)
{
throw new InvalidOperationException(SR.net_uri_NotAbsolute);
}

if (UserDrivenParsing)
{
throw new InvalidOperationException(SR.Format(SR.net_uri_UserDrivenParsing, GetType()));
}

if (DisablePathAndQueryCanonicalization)
{
throw new InvalidOperationException(SR.net_uri_GetComponentsCalledWhenCanonicalizationDisabled);
}

uriFormat = UriFormat.SafeUnescaped;
}

EnsureParseRemaining();
EnsureHostString(allowDnsOptimization: true);

ushort nonCanonical = (ushort)((ushort)_flags & (ushort)Flags.CannotDisplayCanonical);
if (((_flags & (Flags.ShouldBeCompressed | Flags.FirstSlashAbsent | Flags.BackslashInPath)) != 0) ||
(IsDosPath && _string[_info.Offset.Path + SecuredPathIndex - 1] == '|')) // A rare case of c|\
{
nonCanonical |= (ushort)Flags.PathNotCanonical;
}

if (((ushort)UriComponents.AbsoluteUri & nonCanonical) != 0)
{
return TryRecreateParts(destination, out charsWritten, UriComponents.AbsoluteUri, nonCanonical, uriFormat);
}

result = _string.AsSpan(_info.Offset.Scheme, _info.Offset.End - _info.Offset.Scheme);
}
}

if (result.TryCopyTo(destination))
{
charsWritten = result.Length;
return true;
}
return _info.String;

charsWritten = 0;
return false;
}

/// <inheritdoc/>
bool ISpanFormattable.TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, IFormatProvider? provider) =>
TryFormat(destination, out charsWritten);

/// <inheritdoc/>
string IFormattable.ToString(string? format, IFormatProvider? formatProvider) =>
ToString();

public static bool operator ==(Uri? uri1, Uri? uri2)
{
if (ReferenceEquals(uri1, uri2))
Expand Down Expand Up @@ -1664,7 +1742,7 @@ public override bool Equals([NotNullWhen(true)] object? comparand)
EnsureUriInfo();
obj.EnsureUriInfo();

if (!UserDrivenParsing && !obj.UserDrivenParsing && Syntax!.IsSimple && obj.Syntax!.IsSimple)
if (!UserDrivenParsing && !obj.UserDrivenParsing && Syntax!.IsSimple && obj.Syntax.IsSimple)
{
// Optimization of canonical DNS names by avoiding host string creation.
// Note there could be explicit ports specified that would invalidate path offsets
Expand Down Expand Up @@ -2580,7 +2658,7 @@ private string GetEscapedParts(UriComponents uriParts)
}
}

return ReCreateParts(uriParts, nonCanonical, UriFormat.UriEscaped);
return RecreateParts(uriParts, nonCanonical, UriFormat.UriEscaped);
}

private string GetUnescapedParts(UriComponents uriParts, UriFormat formatAs)
Expand Down Expand Up @@ -2615,19 +2693,46 @@ private string GetUnescapedParts(UriComponents uriParts, UriFormat formatAs)
}
}

return ReCreateParts(uriParts, nonCanonical, formatAs);
return RecreateParts(uriParts, nonCanonical, formatAs);
}

private string ReCreateParts(UriComponents parts, ushort nonCanonical, UriFormat formatAs)
private string RecreateParts(UriComponents parts, ushort nonCanonical, UriFormat formatAs)
{
EnsureHostString(false);
EnsureHostString(allowDnsOptimization: false);

string str = _string;

var dest = str.Length <= StackallocThreshold
? new ValueStringBuilder(stackalloc char[StackallocThreshold])
: new ValueStringBuilder(str.Length);

scoped ReadOnlySpan<char> result = RecreateParts(ref dest, str, parts, nonCanonical, formatAs);

string s = result.ToString();
dest.Dispose();
return s;
}

private bool TryRecreateParts(scoped Span<char> span, out int charsWritten, UriComponents parts, ushort nonCanonical, UriFormat formatAs)
{
EnsureHostString(allowDnsOptimization: false);

string str = _string;

var dest = str.Length <= StackallocThreshold
? new ValueStringBuilder(stackalloc char[StackallocThreshold])
: new ValueStringBuilder(str.Length);

scoped ReadOnlySpan<char> result = RecreateParts(ref dest, str, parts, nonCanonical, formatAs);

bool copied = result.TryCopyTo(span);
charsWritten = copied ? result.Length : 0;
dest.Dispose();
return copied;
}

private ReadOnlySpan<char> RecreateParts(scoped ref ValueStringBuilder dest, string str, UriComponents parts, ushort nonCanonical, UriFormat formatAs)
{
//Scheme and slashes
if ((parts & UriComponents.Scheme) != 0)
{
Expand Down Expand Up @@ -2778,9 +2883,7 @@ private string ReCreateParts(UriComponents parts, ushort nonCanonical, UriFormat
offset = 0;
}

string result = dest.AsSpan(offset).ToString();
dest.Dispose();
return result;
return dest.AsSpan(offset);
}
}

Expand Down Expand Up @@ -2860,9 +2963,9 @@ private string ReCreateParts(UriComponents parts, ushort nonCanonical, UriFormat
ref dest, '#', c_DummyChar, c_DummyChar,
mode, _syntax, isQuery: false);
}
AfterFragment:

return dest.ToString();
AfterFragment:
return dest.AsSpan();
}

//
Expand Down
2 changes: 2 additions & 0 deletions src/libraries/System.Private.Uri/src/System/UriExt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public partial class Uri
//
// All public ctors go through here
//
[MemberNotNull(nameof(_string))]
private void CreateThis(string? uri, bool dontEscape, UriKind uriKind, in UriCreationOptions creationOptions = default)
{
DebugAssertInCtor();
Expand Down Expand Up @@ -910,6 +911,7 @@ internal bool IsBaseOfHelper(Uri uriLink)
//
// Only a ctor time call
//
[MemberNotNull(nameof(_string))]
private void CreateThisFromUri(Uri otherUri)
{
DebugAssertInCtor();
Expand Down
2 changes: 1 addition & 1 deletion src/libraries/System.Private.Uri/src/System/UriHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ internal static string EscapeString(string stringToEscape, bool checkExistingEsc
UnescapeString(pStr, start, end, ref dest, rsvd1, rsvd2, rsvd3, unescapeMode, syntax, isQuery);
}
}
internal static unsafe void UnescapeString(ReadOnlySpan<char> input, ref ValueStringBuilder dest,
internal static unsafe void UnescapeString(scoped ReadOnlySpan<char> input, scoped ref ValueStringBuilder dest,
char rsvd1, char rsvd2, char rsvd3, UnescapeMode unescapeMode, UriParser? syntax, bool isQuery)
{
fixed (char* pStr = &MemoryMarshal.GetReference(input))
Expand Down
57 changes: 57 additions & 0 deletions src/libraries/System.Private.Uri/tests/FunctionalTests/UriTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
Expand Down Expand Up @@ -862,5 +863,61 @@ public static void ZeroPortIsParsedForBothKnownAndUnknownSchemes(string uriStrin
Assert.Equal(isDefaultPort, uri.IsDefaultPort);
Assert.Equal(uriString + "/", uri.ToString());
}

public static IEnumerable<object[]> ToStringTest_MemberData()
{
// Return funcs rather than Uri instances directly so that:
// a) We can test each method without it being impacted by implicit caching of a previous method's results
// b) xunit's implicit formatting of arguments doesn't similarly disturb the results

yield return new object[] { () => new Uri("http://test"), "http://test/" };
yield return new object[] { () => new Uri(" http://test "), "http://test/" };
yield return new object[] { () => new Uri("/test", UriKind.Relative), "/test" };
yield return new object[] { () => new Uri("test", UriKind.Relative), "test" };
yield return new object[] { () => new Uri("http://foo/bar/baz#frag"), "http://foo/bar/baz#frag" };
yield return new object[] { () => new Uri(new Uri(@"http://www.contoso.com/"), "catalog/shownew.htm?date=today"), "http://www.contoso.com/catalog/shownew.htm?date=today" };
yield return new object[] { () => new Uri("http://test/a/b/c/d/../../e/f"), "http://test/a/b/e/f" };
yield return new object[] { () => { var uri = new Uri("http://test/a/b/c/d/../../e/f"); uri.ToString(); return uri; }, "http://test/a/b/e/f" };
}

[Theory]
[MemberData(nameof(ToStringTest_MemberData))]
public static void ToStringTest(Func<Uri> func, string expected)
{
// object.ToString
Assert.Equal(expected, func().ToString());

// IFormattable.ToString
Assert.Equal(expected, ((IFormattable)func()).ToString("asdfasdf", new CultureInfo("fr-FR")));

// TryFormat - Big enough destination
foreach (int length in new[] { expected.Length, expected.Length + 1 })
{
// TryFormat
char[] formatted = new char[length];
Assert.True(func().TryFormat(formatted, out int charsWritten));
AssertExtensions.SequenceEqual(expected, (ReadOnlySpan<char>)formatted.AsSpan(0, charsWritten));
Assert.Equal(expected.Length, charsWritten);

// ISpanFormattable.TryFormat
Array.Clear(formatted);
Assert.True(((ISpanFormattable)func()).TryFormat(formatted, out charsWritten, "asdfasdf", new CultureInfo("fr-FR")));
AssertExtensions.SequenceEqual(expected, (ReadOnlySpan<char>)formatted.AsSpan(0, charsWritten));
Assert.Equal(expected.Length, charsWritten);
}

// TryFormat - Too small destination
{
char[] formatted = new char[expected.Length - 1];

// TryFormat
Assert.False(func().TryFormat(formatted, out int charsWritten));
Assert.Equal(0, charsWritten);

// ISpanFormattable.TryFormat
Assert.False(((ISpanFormattable)func()).TryFormat(formatted, out charsWritten, default, null));
Assert.Equal(0, charsWritten);
}
}
}
}