Skip to content

Commit 6db8fa3

Browse files
MihaZupanrichlander
authored andcommitted
Fix parsing inconsistencies between Uri ctors and TryCreate factories (dotnet#123932)
1 parent 208c590 commit 6db8fa3

3 files changed

Lines changed: 78 additions & 110 deletions

File tree

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

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,8 @@ internal enum Flags : ulong
145145
[Conditional("DEBUG")]
146146
private void DebugSetLeftCtor()
147147
{
148+
DebugAssertInCtor();
149+
148150
_flags |= Flags.Debug_LeftConstructor;
149151

150152
AssertInvariants();
@@ -424,7 +426,6 @@ public Uri([StringSyntax(StringSyntaxAttribute.Uri)] string uriString)
424426
ArgumentNullException.ThrowIfNull(uriString);
425427

426428
CreateThis(uriString, false, UriKind.Absolute);
427-
DebugSetLeftCtor();
428429
}
429430

430431
//
@@ -438,7 +439,6 @@ public Uri([StringSyntax(StringSyntaxAttribute.Uri)] string uriString, bool dont
438439
ArgumentNullException.ThrowIfNull(uriString);
439440

440441
CreateThis(uriString, dontEscape, UriKind.Absolute);
441-
DebugSetLeftCtor();
442442
}
443443

444444
//
@@ -456,7 +456,6 @@ public Uri(Uri baseUri, string? relativeUri, bool dontEscape)
456456
throw new ArgumentOutOfRangeException(nameof(baseUri));
457457

458458
CreateUri(baseUri, relativeUri, dontEscape);
459-
DebugSetLeftCtor();
460459
}
461460

462461
//
@@ -467,7 +466,6 @@ public Uri([StringSyntax(StringSyntaxAttribute.Uri, nameof(uriKind))] string uri
467466
ArgumentNullException.ThrowIfNull(uriString);
468467

469468
CreateThis(uriString, false, uriKind);
470-
DebugSetLeftCtor();
471469
}
472470

473471
/// <summary>
@@ -480,7 +478,6 @@ public Uri([StringSyntax(StringSyntaxAttribute.Uri)] string uriString, in UriCre
480478
ArgumentNullException.ThrowIfNull(uriString);
481479

482480
CreateThis(uriString, false, UriKind.Absolute, in creationOptions);
483-
DebugSetLeftCtor();
484481
}
485482

486483
//
@@ -498,7 +495,6 @@ public Uri(Uri baseUri, string? relativeUri)
498495
throw new ArgumentOutOfRangeException(nameof(baseUri));
499496

500497
CreateUri(baseUri, relativeUri, false);
501-
DebugSetLeftCtor();
502498
}
503499

504500
//
@@ -515,7 +511,6 @@ protected Uri(SerializationInfo serializationInfo, StreamingContext streamingCon
515511
if (uriString!.Length != 0)
516512
{
517513
CreateThis(uriString, false, UriKind.Absolute);
518-
DebugSetLeftCtor();
519514
return;
520515
}
521516

@@ -524,7 +519,6 @@ protected Uri(SerializationInfo serializationInfo, StreamingContext streamingCon
524519
throw new ArgumentException(SR.Format(SR.InvalidNullArgument, "RelativeUri"), nameof(serializationInfo));
525520

526521
CreateThis(uriString, false, UriKind.Relative);
527-
DebugSetLeftCtor();
528522
}
529523

530524
//
@@ -617,7 +611,6 @@ public Uri(Uri baseUri, Uri relativeUri)
617611
if (!ReferenceEquals(this, resolvedRelativeUri))
618612
CreateThisFromUri(resolvedRelativeUri);
619613

620-
DebugSetLeftCtor();
621614
return;
622615
}
623616
}
@@ -634,7 +627,6 @@ public Uri(Uri baseUri, Uri relativeUri)
634627
_syntax = null!;
635628
_originalUnicodeString = null!;
636629
CreateThis(newUriString, dontEscape, UriKind.Absolute);
637-
DebugSetLeftCtor();
638630
}
639631

640632
//

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

Lines changed: 44 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,21 @@ namespace System
1313
{
1414
public partial class Uri
1515
{
16-
//
17-
// All public ctors go through here
18-
//
16+
/// <summary>Helper called by all constructors to construct the Uri.</summary>
1917
[MemberNotNull(nameof(_string))]
2018
private void CreateThis(string? uri, bool dontEscape, UriKind uriKind, in UriCreationOptions creationOptions = default)
19+
{
20+
UriFormatException? e = TryCreateThis(uri, dontEscape, uriKind, in creationOptions);
21+
22+
if (e is not null)
23+
{
24+
throw e;
25+
}
26+
}
27+
28+
/// <summary>Core helper called by all constructors and TryCreate factories to construct the Uri.</summary>
29+
[MemberNotNull(nameof(_string))]
30+
private UriFormatException? TryCreateThis(string? uri, bool dontEscape, UriKind uriKind, in UriCreationOptions creationOptions = default)
2131
{
2232
DebugAssertInCtor();
2333

@@ -37,15 +47,6 @@ private void CreateThis(string? uri, bool dontEscape, UriKind uriKind, in UriCre
3747
_flags |= Flags.DisablePathAndQueryCanonicalization;
3848

3949
ParsingError err = ParseScheme(_string, ref _flags, ref _syntax!);
40-
41-
UriFormatException? e = InitializeUri(err, uriKind);
42-
if (e != null)
43-
throw e;
44-
}
45-
46-
private UriFormatException? InitializeUri(ParsingError err, UriKind uriKind)
47-
{
48-
DebugAssertInCtor();
4950
Debug.Assert((err is ParsingError.None) == (_syntax is not null));
5051

5152
bool hasUnicode = false;
@@ -63,19 +64,10 @@ private void CreateThis(string? uri, bool dontEscape, UriKind uriKind, in UriCre
6364
// and we'll allow relative Uri's, then create one.
6465
if (uriKind != UriKind.Absolute && err <= ParsingError.LastErrorOkayForRelativeUris)
6566
{
66-
_flags &= Flags.UserEscaped | Flags.HasUnicode; // the only flags that makes sense for a relative uri
67-
if (hasUnicode)
68-
{
69-
// Iri'ze and then normalize relative uris
70-
var vsb = new ValueStringBuilder(stackalloc char[StackallocThreshold]);
71-
IriHelper.EscapeUnescapeIri(ref vsb, _originalUnicodeString, isQuery: false);
72-
_string = vsb.ToString();
73-
}
74-
return null;
67+
goto SwitchToRelativeUri;
7568
}
7669

7770
// This is a fatal error based solely on scheme name parsing
78-
_string = null!; // make it be invalid Uri
7971
return GetException(err);
8072
}
8173

@@ -85,9 +77,7 @@ private void CreateThis(string? uri, bool dontEscape, UriKind uriKind, in UriCre
8577
{
8678
if (uriKind == UriKind.Relative)
8779
{
88-
_syntax = null!; // make it be relative Uri
89-
_flags &= Flags.UserEscaped; // the only flag that makes sense for a relative uri
90-
return null;
80+
goto SwitchToRelativeUri;
9181
}
9282

9383
// V1 compat
@@ -98,9 +88,7 @@ private void CreateThis(string? uri, bool dontEscape, UriKind uriKind, in UriCre
9888
((_string.Length >= 2 && (_string[0] != '\\' || _string[1] != '\\'))
9989
|| (!OperatingSystem.IsWindows() && InFact(Flags.UnixPath))))
10090
{
101-
_syntax = null!; //make it be relative Uri
102-
_flags &= Flags.UserEscaped; // the only flag that makes sense for a relative uri
103-
return null;
91+
goto SwitchToRelativeUri;
10492
// Otherwise an absolute file Uri wins when it's of the form "\\something"
10593
}
10694
}
@@ -112,9 +100,7 @@ private void CreateThis(string? uri, bool dontEscape, UriKind uriKind, in UriCre
112100
if (uriKind != UriKind.Absolute && err <= ParsingError.LastErrorOkayForRelativeUris)
113101
{
114102
// RFC 3986 Section 5.4.2 - http:(relativeUri) may be considered a valid relative Uri.
115-
_syntax = null!; // convert to relative uri
116-
_flags &= Flags.UserEscaped; // the only flag that makes sense for a relative uri
117-
return null;
103+
goto SwitchToRelativeUri;
118104
}
119105

120106
return GetException(err);
@@ -164,6 +150,23 @@ private void CreateThis(string? uri, bool dontEscape, UriKind uriKind, in UriCre
164150
}
165151

166152
// We have a valid absolute Uri.
153+
DebugSetLeftCtor();
154+
return null;
155+
156+
SwitchToRelativeUri:
157+
Debug.Assert(uriKind != UriKind.Absolute);
158+
159+
_syntax = null!;
160+
_flags &= Flags.UserEscaped | Flags.HasUnicode; // the only flags that make sense for a relative uri
161+
162+
if (hasUnicode)
163+
{
164+
var vsb = new ValueStringBuilder(stackalloc char[StackallocThreshold]);
165+
IriHelper.EscapeUnescapeIri(ref vsb, _originalUnicodeString, isQuery: false);
166+
_string = vsb.ToString();
167+
}
168+
169+
DebugSetLeftCtor();
167170
return null;
168171
}
169172

@@ -220,7 +223,6 @@ private static bool CheckForUnicodeOrEscapedUnreserved(string data)
220223
public static bool TryCreate([NotNullWhen(true), StringSyntax(StringSyntaxAttribute.Uri, "uriKind")] string? uriString, UriKind uriKind, [NotNullWhen(true)] out Uri? result)
221224
{
222225
result = CreateHelper(uriString, false, uriKind);
223-
result?.DebugSetLeftCtor();
224226
return result is not null;
225227
}
226228

@@ -234,7 +236,6 @@ public static bool TryCreate([NotNullWhen(true), StringSyntax(StringSyntaxAttrib
234236
public static bool TryCreate([NotNullWhen(true), StringSyntax(StringSyntaxAttribute.Uri)] string? uriString, in UriCreationOptions creationOptions, [NotNullWhen(true)] out Uri? result)
235237
{
236238
result = CreateHelper(uriString, false, UriKind.Absolute, in creationOptions);
237-
result?.DebugSetLeftCtor();
238239
return result is not null;
239240
}
240241

@@ -283,7 +284,6 @@ public static bool TryCreate(Uri? baseUri, Uri? relativeUri, [NotNullWhen(true)]
283284
result ??= CreateHelper(newUriString!, dontEscape, UriKind.Absolute);
284285
Debug.Assert(result is null || result.IsAbsoluteUri);
285286

286-
result?.DebugSetLeftCtor();
287287
return result is not null;
288288
}
289289

@@ -625,77 +625,28 @@ public static string EscapeDataString(ReadOnlySpan<char> charsToEscape) =>
625625
public static bool TryEscapeDataString(ReadOnlySpan<char> charsToEscape, Span<char> destination, out int charsWritten) =>
626626
UriHelper.TryEscapeDataString(charsToEscape, destination, out charsWritten);
627627

628-
// Should never be used except by the below method
629-
private Uri(Flags flags, UriParser? uriParser, string uri)
630-
{
631-
_flags = flags;
632-
_syntax = uriParser!;
633-
_string = uri;
634-
635-
if (uriParser is null)
636-
{
637-
// Relative Uris are fully initialized after the call to this constructor
638-
// Absolute Uris will be initialized with a call to InitializeUri on the newly created instance
639-
DebugSetLeftCtor();
640-
}
641-
}
628+
#pragma warning disable CS8618 // _string will be initialized by TryCreateThis later.
629+
/// <summary>Must never be used except by <see cref="CreateHelper(string?, bool, UriKind, in UriCreationOptions)"/>.</summary>
630+
private Uri() { }
631+
#pragma warning restore CS8618
642632

643-
//
644-
// a Uri.TryCreate() method goes through here.
645-
//
633+
/// <summary>Called by TryCreate.</summary>
646634
internal static Uri? CreateHelper(string? uriString, bool dontEscape, UriKind uriKind, in UriCreationOptions creationOptions = default)
647635
{
648636
if (uriString is null)
649637
{
650638
return null;
651639
}
652640

653-
if (uriKind is < UriKind.RelativeOrAbsolute or > UriKind.Relative)
654-
{
655-
throw new ArgumentException(SR.Format(SR.net_uri_InvalidUriKind, uriKind));
656-
}
657-
658-
UriParser? syntax = null;
659-
Flags flags = Flags.Zero;
660-
ParsingError err = ParseScheme(uriString, ref flags, ref syntax);
661-
662-
if (dontEscape)
663-
flags |= Flags.UserEscaped;
664-
665-
if (creationOptions.DangerousDisablePathAndQueryCanonicalization)
666-
flags |= Flags.DisablePathAndQueryCanonicalization;
667-
668-
// We won't use User factory for these errors
669-
if (err != ParsingError.None)
670-
{
671-
// If it looks as a relative Uri, custom factory is ignored
672-
if (uriKind != UriKind.Absolute && err <= ParsingError.LastErrorOkayForRelativeUris)
673-
return new Uri((flags & Flags.UserEscaped), null, uriString);
674-
675-
return null;
676-
}
677-
678-
// Cannot be relative Uri if came here
679-
Debug.Assert(syntax != null);
680-
Uri result = new Uri(flags, syntax, uriString);
681-
682-
// Validate instance using ether built in or a user Parser
641+
Uri result = new();
683642
try
684643
{
685-
UriFormatException? e = result.InitializeUri(err, uriKind);
686-
687-
if (e == null)
688-
{
689-
result.DebugSetLeftCtor();
690-
return result;
691-
}
692-
693-
return null;
644+
UriFormatException? e = result.TryCreateThis(uriString, dontEscape, uriKind, in creationOptions);
645+
return e is null ? result : null;
694646
}
695647
catch (UriFormatException)
696648
{
697-
Debug.Assert(!syntax.IsSimple, "A UriPraser threw on InitializeAndValidate.");
698-
// A precaution since custom Parser should never throw in this case.
649+
Debug.Assert(result.Syntax is { IsSimple: false }, "A custom UriParser threw on InitializeAndValidate.");
699650
return null;
700651
}
701652
}
@@ -938,6 +889,7 @@ internal bool IsBaseOfHelper(Uri uriLink)
938889
private void CreateThisFromUri(Uri otherUri)
939890
{
940891
DebugAssertInCtor();
892+
Debug.Assert((otherUri._flags & Flags.Debug_LeftConstructor) != 0);
941893

942894
_flags = otherUri._flags;
943895

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

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -914,14 +914,38 @@ public static IEnumerable<object[]> ToStringTest_MemberData()
914914
// a) We can test each method without it being impacted by implicit caching of a previous method's results
915915
// b) xunit's implicit formatting of arguments doesn't similarly disturb the results
916916

917-
yield return new object[] { () => new Uri("http://test"), "http://test/" };
918-
yield return new object[] { () => new Uri(" http://test "), "http://test/" };
919-
yield return new object[] { () => new Uri("/test", UriKind.Relative), "/test" };
920-
yield return new object[] { () => new Uri("test", UriKind.Relative), "test" };
921-
yield return new object[] { () => new Uri("http://foo/bar/baz#frag"), "http://foo/bar/baz#frag" };
922-
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" };
923-
yield return new object[] { () => new Uri("http://test/a/b/c/d/../../e/f"), "http://test/a/b/e/f" };
924-
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" };
917+
(Func<Uri>, string)[] testData =
918+
[
919+
(() => new Uri("http://test"), "http://test/"),
920+
(() => new Uri(" http://test "), "http://test/"),
921+
(() => new Uri("/test", UriKind.Relative), "/test"),
922+
(() => new Uri("test", UriKind.Relative), "test"),
923+
(() => new Uri("http://foo/bar/baz#frag"), "http://foo/bar/baz#frag"),
924+
(() => new Uri(new Uri(@"http://www.contoso.com/"), "catalog/shownew.htm?date=today"), "http://www.contoso.com/catalog/shownew.htm?date=today"),
925+
(() => new Uri("http://test/a/b/c/d/../../e/f"), "http://test/a/b/e/f"),
926+
(() => { var uri = new Uri("http://test/a/b/c/d/../../e/f"); uri.ToString(); return uri; }, "http://test/a/b/e/f"),
927+
(() => new Uri("p%41th", UriKind.Relative), "pAth"),
928+
(() => new Uri("pa\uFFFFth", UriKind.Relative), "pa%EF%BF%BFth"),
929+
(() => new Uri("C:\\path", UriKind.Relative), "C:\\path"),
930+
(() => new Uri("C:\\p%41th", UriKind.Relative), "C:\\pAth"),
931+
(() => new Uri("http:\\host/path", UriKind.Relative), "http:\\host/path"),
932+
(() => new Uri("http:\\host/p%41th", UriKind.Relative), "http:\\host/pAth"),
933+
(() => new Uri("//host/path", UriKind.RelativeOrAbsolute), "//host/path"),
934+
(() => new Uri("//host/p%41th", UriKind.RelativeOrAbsolute), "//host/pAth"),
935+
];
936+
937+
foreach ((Func<Uri> factory, string expected) in testData)
938+
{
939+
yield return [factory, expected];
940+
941+
yield return [() =>
942+
{
943+
Uri uri = factory();
944+
Assert.True(Uri.TryCreate(uri.OriginalString, uri.IsAbsoluteUri ? UriKind.Absolute : UriKind.Relative, out Uri? uri2));
945+
Assert.Same(uri.OriginalString, uri2.OriginalString);
946+
return uri2;
947+
}, expected];
948+
}
925949
}
926950

927951
[Theory]

0 commit comments

Comments
 (0)