Skip to content

Commit

Permalink
Add Uri : ISpanFormattable (#88012)
Browse files Browse the repository at this point in the history
* Add Uri : ISpanFormattable

Implemented TryFormat by copying in the ToString implementation, manually expanding out each call, deleting all the cruft, and switching return strings to be span copies / writes.

* Update src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/DataContract.cs
  • Loading branch information
stephentoub committed Jun 25, 2023
1 parent 0633ecf commit eaa9717
Show file tree
Hide file tree
Showing 7 changed files with 207 additions and 25 deletions.
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);
}
}
}
}

0 comments on commit eaa9717

Please sign in to comment.