Skip to content

Commit eaa9717

Browse files
authored
Add Uri : ISpanFormattable (#88012)
* 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
1 parent 0633ecf commit eaa9717

File tree

7 files changed

+207
-25
lines changed

7 files changed

+207
-25
lines changed

src/libraries/System.Net.Http/src/System/Net/Http/HttpRequestMessage.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,14 @@ public override string ToString()
147147
sb.Append(_method);
148148

149149
sb.Append(", RequestUri: '");
150-
sb.Append(_requestUri == null ? "<null>" : _requestUri.ToString());
150+
if (_requestUri is null)
151+
{
152+
sb.Append("<null>");
153+
}
154+
else
155+
{
156+
sb.Append($"{_requestUri}");
157+
}
151158

152159
sb.Append("', Version: ");
153160
sb.Append(_version);

src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/DataContract.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1673,17 +1673,27 @@ private static void CheckExplicitDataContractNamespaceUri(string dataContractNs,
16731673
{
16741674
string trimmedNs = dataContractNs.Trim();
16751675
// Code similar to XmlConvert.ToUri (string.Empty is a valid uri but not " ")
1676-
if (trimmedNs.Length == 0 || trimmedNs.IndexOf("##", StringComparison.Ordinal) != -1)
1676+
if (trimmedNs.Length == 0 || trimmedNs.Contains("##", StringComparison.Ordinal))
1677+
{
16771678
ThrowInvalidDataContractException(SR.Format(SR.DataContractNamespaceIsNotValid, dataContractNs), type);
1679+
}
1680+
16781681
dataContractNs = trimmedNs;
16791682
}
16801683
if (Uri.TryCreate(dataContractNs, UriKind.RelativeOrAbsolute, out Uri? uri))
16811684
{
1682-
if (uri.ToString() == Globals.SerializationNamespace)
1685+
Span<char> formatted = stackalloc char[Globals.SerializationNamespace.Length];
1686+
if (uri.TryFormat(formatted, out int charsWritten) &&
1687+
charsWritten == Globals.SerializationNamespace.Length &&
1688+
formatted.SequenceEqual(Globals.SerializationNamespace))
1689+
{
16831690
ThrowInvalidDataContractException(SR.Format(SR.DataContractNamespaceReserved, Globals.SerializationNamespace), type);
1691+
}
16841692
}
16851693
else
1694+
{
16861695
ThrowInvalidDataContractException(SR.Format(SR.DataContractNamespaceIsNotValid, dataContractNs), type);
1696+
}
16871697
}
16881698

16891699
internal static string GetClrTypeFullName(Type type)

src/libraries/System.Private.Uri/src/System/Uri.cs

Lines changed: 123 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ namespace System
1717
{
1818
[Serializable]
1919
[System.Runtime.CompilerServices.TypeForwardedFrom("System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")]
20-
public partial class Uri : ISerializable
20+
public partial class Uri : ISpanFormattable, ISerializable
2121
{
2222
public static readonly string UriSchemeFile = UriParser.FileUri.SchemeName;
2323
public static readonly string UriSchemeFtp = UriParser.FtpUri.SchemeName;
@@ -46,7 +46,7 @@ public partial class Uri : ISerializable
4646
// or idn is on and we have unicode host or idn host
4747
// In that case, this string is normalized, stripped of bidi chars, and validated
4848
// with char limits
49-
private string _string = null!; // initialized early in ctor via a helper
49+
private string _string;
5050

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

322+
[MemberNotNull(nameof(_info))]
322323
private UriInfo EnsureUriInfo()
323324
{
324325
Flags cF = _flags;
@@ -338,6 +339,7 @@ private void EnsureParseRemaining()
338339
}
339340
}
340341

342+
[MemberNotNull(nameof(_info))]
341343
private void EnsureHostString(bool allowDnsOptimization)
342344
{
343345
UriInfo info = EnsureUriInfo();
@@ -497,6 +499,7 @@ protected void GetObjectData(SerializationInfo serializationInfo, StreamingConte
497499
}
498500
}
499501

502+
[MemberNotNull(nameof(_string))]
500503
private void CreateUri(Uri baseUri, string? relativeUri, bool dontEscape)
501504
{
502505
DebugAssertInCtor();
@@ -1173,7 +1176,7 @@ public string IdnHost
11731176
{
11741177
EnsureHostString(false);
11751178

1176-
string host = _info!.Host!;
1179+
string host = _info.Host!;
11771180

11781181
Flags hostType = HostType;
11791182
if (hostType == Flags.DnsHostType)
@@ -1538,8 +1541,6 @@ public override int GetHashCode()
15381541
//
15391542
// ToString
15401543
//
1541-
// The better implementation would be just
1542-
//
15431544
private const UriFormat V1ToStringUnescape = (UriFormat)0x7FFF;
15441545

15451546
public override string ToString()
@@ -1550,16 +1551,93 @@ public override string ToString()
15501551
}
15511552

15521553
EnsureUriInfo();
1553-
if (_info.String is null)
1554+
return _info.String ??=
1555+
_syntax.IsSimple ?
1556+
GetComponentsHelper(UriComponents.AbsoluteUri, V1ToStringUnescape) :
1557+
GetParts(UriComponents.AbsoluteUri, UriFormat.SafeUnescaped);
1558+
}
1559+
1560+
/// <summary>
1561+
/// Attempts to format a canonical string representation for the <see cref="Uri"/> instance into the specified span.
1562+
/// </summary>
1563+
/// <param name="destination">The span into which to write this instance's value formatted as a span of characters.</param>
1564+
/// <param name="charsWritten">When this method returns, contains the number of characters that were written in <paramref name="destination"/>.</param>
1565+
/// <returns><see langword="true"/> if the formatting was successful; otherwise, <see langword="false"/>.</returns>
1566+
public bool TryFormat(Span<char> destination, out int charsWritten)
1567+
{
1568+
ReadOnlySpan<char> result;
1569+
1570+
if (_syntax is null)
15541571
{
1555-
if (_syntax.IsSimple)
1556-
_info.String = GetComponentsHelper(UriComponents.AbsoluteUri, V1ToStringUnescape);
1572+
result = _string;
1573+
}
1574+
else
1575+
{
1576+
EnsureUriInfo();
1577+
if (_info.String is not null)
1578+
{
1579+
result = _info.String;
1580+
}
15571581
else
1558-
_info.String = GetParts(UriComponents.AbsoluteUri, UriFormat.SafeUnescaped);
1582+
{
1583+
UriFormat uriFormat = V1ToStringUnescape;
1584+
if (!_syntax.IsSimple)
1585+
{
1586+
if (IsNotAbsoluteUri)
1587+
{
1588+
throw new InvalidOperationException(SR.net_uri_NotAbsolute);
1589+
}
1590+
1591+
if (UserDrivenParsing)
1592+
{
1593+
throw new InvalidOperationException(SR.Format(SR.net_uri_UserDrivenParsing, GetType()));
1594+
}
1595+
1596+
if (DisablePathAndQueryCanonicalization)
1597+
{
1598+
throw new InvalidOperationException(SR.net_uri_GetComponentsCalledWhenCanonicalizationDisabled);
1599+
}
1600+
1601+
uriFormat = UriFormat.SafeUnescaped;
1602+
}
1603+
1604+
EnsureParseRemaining();
1605+
EnsureHostString(allowDnsOptimization: true);
1606+
1607+
ushort nonCanonical = (ushort)((ushort)_flags & (ushort)Flags.CannotDisplayCanonical);
1608+
if (((_flags & (Flags.ShouldBeCompressed | Flags.FirstSlashAbsent | Flags.BackslashInPath)) != 0) ||
1609+
(IsDosPath && _string[_info.Offset.Path + SecuredPathIndex - 1] == '|')) // A rare case of c|\
1610+
{
1611+
nonCanonical |= (ushort)Flags.PathNotCanonical;
1612+
}
1613+
1614+
if (((ushort)UriComponents.AbsoluteUri & nonCanonical) != 0)
1615+
{
1616+
return TryRecreateParts(destination, out charsWritten, UriComponents.AbsoluteUri, nonCanonical, uriFormat);
1617+
}
1618+
1619+
result = _string.AsSpan(_info.Offset.Scheme, _info.Offset.End - _info.Offset.Scheme);
1620+
}
1621+
}
1622+
1623+
if (result.TryCopyTo(destination))
1624+
{
1625+
charsWritten = result.Length;
1626+
return true;
15591627
}
1560-
return _info.String;
1628+
1629+
charsWritten = 0;
1630+
return false;
15611631
}
15621632

1633+
/// <inheritdoc/>
1634+
bool ISpanFormattable.TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, IFormatProvider? provider) =>
1635+
TryFormat(destination, out charsWritten);
1636+
1637+
/// <inheritdoc/>
1638+
string IFormattable.ToString(string? format, IFormatProvider? formatProvider) =>
1639+
ToString();
1640+
15631641
public static bool operator ==(Uri? uri1, Uri? uri2)
15641642
{
15651643
if (ReferenceEquals(uri1, uri2))
@@ -1664,7 +1742,7 @@ public override bool Equals([NotNullWhen(true)] object? comparand)
16641742
EnsureUriInfo();
16651743
obj.EnsureUriInfo();
16661744

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

2583-
return ReCreateParts(uriParts, nonCanonical, UriFormat.UriEscaped);
2661+
return RecreateParts(uriParts, nonCanonical, UriFormat.UriEscaped);
25842662
}
25852663

25862664
private string GetUnescapedParts(UriComponents uriParts, UriFormat formatAs)
@@ -2615,19 +2693,46 @@ private string GetUnescapedParts(UriComponents uriParts, UriFormat formatAs)
26152693
}
26162694
}
26172695

2618-
return ReCreateParts(uriParts, nonCanonical, formatAs);
2696+
return RecreateParts(uriParts, nonCanonical, formatAs);
26192697
}
26202698

2621-
private string ReCreateParts(UriComponents parts, ushort nonCanonical, UriFormat formatAs)
2699+
private string RecreateParts(UriComponents parts, ushort nonCanonical, UriFormat formatAs)
26222700
{
2623-
EnsureHostString(false);
2701+
EnsureHostString(allowDnsOptimization: false);
26242702

26252703
string str = _string;
26262704

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

2709+
scoped ReadOnlySpan<char> result = RecreateParts(ref dest, str, parts, nonCanonical, formatAs);
2710+
2711+
string s = result.ToString();
2712+
dest.Dispose();
2713+
return s;
2714+
}
2715+
2716+
private bool TryRecreateParts(scoped Span<char> span, out int charsWritten, UriComponents parts, ushort nonCanonical, UriFormat formatAs)
2717+
{
2718+
EnsureHostString(allowDnsOptimization: false);
2719+
2720+
string str = _string;
2721+
2722+
var dest = str.Length <= StackallocThreshold
2723+
? new ValueStringBuilder(stackalloc char[StackallocThreshold])
2724+
: new ValueStringBuilder(str.Length);
2725+
2726+
scoped ReadOnlySpan<char> result = RecreateParts(ref dest, str, parts, nonCanonical, formatAs);
2727+
2728+
bool copied = result.TryCopyTo(span);
2729+
charsWritten = copied ? result.Length : 0;
2730+
dest.Dispose();
2731+
return copied;
2732+
}
2733+
2734+
private ReadOnlySpan<char> RecreateParts(scoped ref ValueStringBuilder dest, string str, UriComponents parts, ushort nonCanonical, UriFormat formatAs)
2735+
{
26312736
//Scheme and slashes
26322737
if ((parts & UriComponents.Scheme) != 0)
26332738
{
@@ -2778,9 +2883,7 @@ private string ReCreateParts(UriComponents parts, ushort nonCanonical, UriFormat
27782883
offset = 0;
27792884
}
27802885

2781-
string result = dest.AsSpan(offset).ToString();
2782-
dest.Dispose();
2783-
return result;
2886+
return dest.AsSpan(offset);
27842887
}
27852888
}
27862889

@@ -2860,9 +2963,9 @@ private string ReCreateParts(UriComponents parts, ushort nonCanonical, UriFormat
28602963
ref dest, '#', c_DummyChar, c_DummyChar,
28612964
mode, _syntax, isQuery: false);
28622965
}
2863-
AfterFragment:
28642966

2865-
return dest.ToString();
2967+
AfterFragment:
2968+
return dest.AsSpan();
28662969
}
28672970

28682971
//

src/libraries/System.Private.Uri/src/System/UriExt.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public partial class Uri
1313
//
1414
// All public ctors go through here
1515
//
16+
[MemberNotNull(nameof(_string))]
1617
private void CreateThis(string? uri, bool dontEscape, UriKind uriKind, in UriCreationOptions creationOptions = default)
1718
{
1819
DebugAssertInCtor();
@@ -910,6 +911,7 @@ internal bool IsBaseOfHelper(Uri uriLink)
910911
//
911912
// Only a ctor time call
912913
//
914+
[MemberNotNull(nameof(_string))]
913915
private void CreateThisFromUri(Uri otherUri)
914916
{
915917
DebugAssertInCtor();

src/libraries/System.Private.Uri/src/System/UriHelper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ internal static unsafe void UnescapeString(string input, int start, int end, ref
269269
UnescapeString(pStr, start, end, ref dest, rsvd1, rsvd2, rsvd3, unescapeMode, syntax, isQuery);
270270
}
271271
}
272-
internal static unsafe void UnescapeString(ReadOnlySpan<char> input, ref ValueStringBuilder dest,
272+
internal static unsafe void UnescapeString(scoped ReadOnlySpan<char> input, scoped ref ValueStringBuilder dest,
273273
char rsvd1, char rsvd2, char rsvd3, UnescapeMode unescapeMode, UriParser? syntax, bool isQuery)
274274
{
275275
fixed (char* pStr = &MemoryMarshal.GetReference(input))

src/libraries/System.Private.Uri/tests/FunctionalTests/UriTests.cs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Collections.Generic;
5+
using System.Globalization;
56
using System.Threading;
67
using System.Threading.Tasks;
78
using Xunit;
@@ -862,5 +863,61 @@ public static void ZeroPortIsParsedForBothKnownAndUnknownSchemes(string uriStrin
862863
Assert.Equal(isDefaultPort, uri.IsDefaultPort);
863864
Assert.Equal(uriString + "/", uri.ToString());
864865
}
866+
867+
public static IEnumerable<object[]> ToStringTest_MemberData()
868+
{
869+
// Return funcs rather than Uri instances directly so that:
870+
// a) We can test each method without it being impacted by implicit caching of a previous method's results
871+
// b) xunit's implicit formatting of arguments doesn't similarly disturb the results
872+
873+
yield return new object[] { () => new Uri("http://test"), "http://test/" };
874+
yield return new object[] { () => new Uri(" http://test "), "http://test/" };
875+
yield return new object[] { () => new Uri("/test", UriKind.Relative), "/test" };
876+
yield return new object[] { () => new Uri("test", UriKind.Relative), "test" };
877+
yield return new object[] { () => new Uri("http://foo/bar/baz#frag"), "http://foo/bar/baz#frag" };
878+
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" };
879+
yield return new object[] { () => new Uri("http://test/a/b/c/d/../../e/f"), "http://test/a/b/e/f" };
880+
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" };
881+
}
882+
883+
[Theory]
884+
[MemberData(nameof(ToStringTest_MemberData))]
885+
public static void ToStringTest(Func<Uri> func, string expected)
886+
{
887+
// object.ToString
888+
Assert.Equal(expected, func().ToString());
889+
890+
// IFormattable.ToString
891+
Assert.Equal(expected, ((IFormattable)func()).ToString("asdfasdf", new CultureInfo("fr-FR")));
892+
893+
// TryFormat - Big enough destination
894+
foreach (int length in new[] { expected.Length, expected.Length + 1 })
895+
{
896+
// TryFormat
897+
char[] formatted = new char[length];
898+
Assert.True(func().TryFormat(formatted, out int charsWritten));
899+
AssertExtensions.SequenceEqual(expected, (ReadOnlySpan<char>)formatted.AsSpan(0, charsWritten));
900+
Assert.Equal(expected.Length, charsWritten);
901+
902+
// ISpanFormattable.TryFormat
903+
Array.Clear(formatted);
904+
Assert.True(((ISpanFormattable)func()).TryFormat(formatted, out charsWritten, "asdfasdf", new CultureInfo("fr-FR")));
905+
AssertExtensions.SequenceEqual(expected, (ReadOnlySpan<char>)formatted.AsSpan(0, charsWritten));
906+
Assert.Equal(expected.Length, charsWritten);
907+
}
908+
909+
// TryFormat - Too small destination
910+
{
911+
char[] formatted = new char[expected.Length - 1];
912+
913+
// TryFormat
914+
Assert.False(func().TryFormat(formatted, out int charsWritten));
915+
Assert.Equal(0, charsWritten);
916+
917+
// ISpanFormattable.TryFormat
918+
Assert.False(((ISpanFormattable)func()).TryFormat(formatted, out charsWritten, default, null));
919+
Assert.Equal(0, charsWritten);
920+
}
921+
}
865922
}
866923
}

0 commit comments

Comments
 (0)