Skip to content

Commit fe50d91

Browse files
authored
Implement UriCreationOptions (#59173)
* Add UriCreationOptions Includes DangerousDisablePathAndQueryCanonicalization * Add /// comments
1 parent b9926a5 commit fe50d91

File tree

9 files changed

+441
-6
lines changed

9 files changed

+441
-6
lines changed

src/libraries/System.Private.Uri/src/Resources/Strings.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,4 +198,7 @@
198198
<data name="net_uri_InitializeCalledAlreadyOrTooLate" xml:space="preserve">
199199
<value>UriParser's base InitializeAndValidate may only be called once on a single Uri instance and only from an override of InitializeAndValidate.</value>
200200
</data>
201+
<data name="net_uri_GetComponentsCalledWhenCanonicalizationDisabled" xml:space="preserve">
202+
<value>GetComponents() may not be used for Path/Query on a Uri instance created with UriCreationOptions.DangerousDisablePathAndQueryCanonicalization.</value>
203+
</data>
201204
</root>

src/libraries/System.Private.Uri/src/System.Private.Uri.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
<Compile Include="System\UncNameHelper.cs" />
2525
<Compile Include="System\Uri.cs" />
2626
<Compile Include="System\UriBuilder.cs" />
27+
<Compile Include="System\UriCreationOptions.cs" />
2728
<Compile Include="System\UriEnumTypes.cs" />
2829
<Compile Include="System\UriExt.cs" />
2930
<Compile Include="System\UriFormatException.cs" />

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

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,11 @@ internal enum Flags : ulong
121121
IriCanonical = 0x78000000000,
122122
UnixPath = 0x100000000000,
123123

124+
/// <summary>
125+
/// Disables any validation/normalization past the authority. Fragments will always be empty. GetComponents will throw for Path/Query.
126+
/// </summary>
127+
DisablePathAndQueryCanonicalization = 0x200000000000,
128+
124129
/// <summary>
125130
/// Used to ensure that InitializeAndValidate is only called once per Uri instance and only from an override of InitializeAndValidate
126131
/// </summary>
@@ -267,6 +272,8 @@ internal static bool IriParsingStatic(UriParser? syntax)
267272
return syntax is null || syntax.InFact(UriSyntaxFlags.AllowIriParsing);
268273
}
269274

275+
internal bool DisablePathAndQueryCanonicalization => (_flags & Flags.DisablePathAndQueryCanonicalization) != 0;
276+
270277
internal bool UserDrivenParsing
271278
{
272279
get
@@ -410,6 +417,20 @@ public Uri(string uriString, UriKind uriKind)
410417
DebugSetLeftCtor();
411418
}
412419

420+
/// <summary>
421+
/// Initializes a new instance of the <see cref="Uri"/> class with the specified URI and additional <see cref="UriCreationOptions"/>.
422+
/// </summary>
423+
/// <param name="uriString">A string that identifies the resource to be represented by the <see cref="Uri"/> instance.</param>
424+
/// <param name="creationOptions">Options that control how the <seealso cref="Uri"/> is created and behaves.</param>
425+
public Uri(string uriString, in UriCreationOptions creationOptions)
426+
{
427+
if (uriString is null)
428+
throw new ArgumentNullException(nameof(uriString));
429+
430+
CreateThis(uriString, false, UriKind.Absolute, in creationOptions);
431+
DebugSetLeftCtor();
432+
}
433+
413434
//
414435
// Uri(Uri, string)
415436
//
@@ -1639,6 +1660,9 @@ public override bool Equals([NotNullWhen(true)] object? comparand)
16391660
// canonicalize the comparand, making comparison possible
16401661
if (obj is null)
16411662
{
1663+
if (DisablePathAndQueryCanonicalization)
1664+
return false;
1665+
16421666
if (!(comparand is string s))
16431667
return false;
16441668

@@ -1649,6 +1673,9 @@ public override bool Equals([NotNullWhen(true)] object? comparand)
16491673
return false;
16501674
}
16511675

1676+
if (DisablePathAndQueryCanonicalization != obj.DisablePathAndQueryCanonicalization)
1677+
return false;
1678+
16521679
if (ReferenceEquals(OriginalString, obj.OriginalString))
16531680
{
16541681
return true;
@@ -2553,7 +2580,7 @@ private unsafe void GetHostViaCustomSyntax()
25532580
//
25542581
internal string GetParts(UriComponents uriParts, UriFormat formatAs)
25552582
{
2556-
return GetComponents(uriParts, formatAs);
2583+
return InternalGetComponents(uriParts, formatAs);
25572584
}
25582585

25592586
private string GetEscapedParts(UriComponents uriParts)
@@ -3158,9 +3185,6 @@ private unsafe void ParseRemaining()
31583185
idx = _info.Offset.Path;
31593186
origIdx = _info.Offset.Path;
31603187

3161-
//Some uris do not have a query
3162-
// When '?' is passed as delimiter, then it's special case
3163-
// so both '?' and '#' will work as delimiters
31643188
if (buildIriStringFromPath)
31653189
{
31663190
DebugAssertInCtor();
@@ -3180,6 +3204,45 @@ private unsafe void ParseRemaining()
31803204

31813205
_info.Offset.Path = (ushort)_string.Length;
31823206
idx = _info.Offset.Path;
3207+
}
3208+
3209+
// If the user explicitly disabled canonicalization, only figure out the offsets
3210+
if (DisablePathAndQueryCanonicalization)
3211+
{
3212+
if (buildIriStringFromPath)
3213+
{
3214+
DebugAssertInCtor();
3215+
_string += _originalUnicodeString.Substring(origIdx);
3216+
}
3217+
3218+
string str = _string;
3219+
3220+
if (IsImplicitFile || (syntaxFlags & UriSyntaxFlags.MayHaveQuery) == 0)
3221+
{
3222+
idx = str.Length;
3223+
}
3224+
else
3225+
{
3226+
idx = str.IndexOf('?');
3227+
if (idx == -1)
3228+
{
3229+
idx = str.Length;
3230+
}
3231+
}
3232+
3233+
_info.Offset.Query = (ushort)idx;
3234+
_info.Offset.Fragment = (ushort)str.Length; // There is no fragment in UseRawTarget mode
3235+
_info.Offset.End = (ushort)str.Length;
3236+
3237+
goto Done;
3238+
}
3239+
3240+
//Some uris do not have a query
3241+
// When '?' is passed as delimiter, then it's special case
3242+
// so both '?' and '#' will work as delimiters
3243+
if (buildIriStringFromPath)
3244+
{
3245+
DebugAssertInCtor();
31833246

31843247
int offset = origIdx;
31853248
if (IsImplicitFile || ((syntaxFlags & (UriSyntaxFlags.MayHaveQuery | UriSyntaxFlags.MayHaveFragment)) == 0))
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace System
5+
{
6+
/// <summary>
7+
/// Options that control how a <seealso cref="Uri"/> is created and behaves.
8+
/// </summary>
9+
public struct UriCreationOptions
10+
{
11+
private bool _disablePathAndQueryCanonicalization;
12+
13+
/// <summary>
14+
/// Disables validation and normalization of the Path and Query.
15+
/// No transformations of the URI past the Authority will take place.
16+
/// <see cref="Uri"/> instances created with this option do not support <see cref="Uri.Fragment"/>s.
17+
/// <see cref="Uri.GetComponents(UriComponents, UriFormat)"/> may not be used for <see cref="UriComponents.Path"/> or <see cref="UriComponents.Query"/>.
18+
/// Be aware that disabling canonicalization also means that reserved characters will not be escaped,
19+
/// which may corrupt the HTTP request and makes the application subject to request smuggling.
20+
/// Only set this option if you have ensured that the URI string is already sanitized.
21+
/// </summary>
22+
public bool DangerousDisablePathAndQueryCanonicalization
23+
{
24+
readonly get => _disablePathAndQueryCanonicalization;
25+
set => _disablePathAndQueryCanonicalization = value;
26+
}
27+
}
28+
}

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

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public partial class Uri
1313
//
1414
// All public ctors go through here
1515
//
16-
private void CreateThis(string? uri, bool dontEscape, UriKind uriKind)
16+
private void CreateThis(string? uri, bool dontEscape, UriKind uriKind, in UriCreationOptions creationOptions = default)
1717
{
1818
DebugAssertInCtor();
1919

@@ -31,6 +31,9 @@ private void CreateThis(string? uri, bool dontEscape, UriKind uriKind)
3131
if (dontEscape)
3232
_flags |= Flags.UserEscaped;
3333

34+
if (creationOptions.DangerousDisablePathAndQueryCanonicalization)
35+
_flags |= Flags.DisablePathAndQueryCanonicalization;
36+
3437
ParsingError err = ParseScheme(_string, ref _flags, ref _syntax!);
3538

3639
InitializeUri(err, uriKind, out UriFormatException? e);
@@ -259,6 +262,26 @@ public static bool TryCreate([NotNullWhen(true)] string? uriString, UriKind uriK
259262
return e is null && result != null;
260263
}
261264

265+
/// <summary>
266+
/// Creates a new <see cref="Uri"/> using the specified <see cref="string"/> instance and <see cref="UriCreationOptions"/>.
267+
/// </summary>
268+
/// <param name="uriString">The string representation of the <see cref="Uri"/>.</param>
269+
/// <param name="creationOptions">Options that control how the <seealso cref="Uri"/> is created and behaves.</param>
270+
/// <param name="result">The constructed <see cref="Uri"/>.</param>
271+
/// <returns><see langword="true"/> if the <see cref="Uri"/> was successfully created; otherwise, <see langword="false"/>.</returns>
272+
public static bool TryCreate([NotNullWhen(true)] string? uriString, in UriCreationOptions creationOptions, [NotNullWhen(true)] out Uri? result)
273+
{
274+
if (uriString is null)
275+
{
276+
result = null;
277+
return false;
278+
}
279+
UriFormatException? e = null;
280+
result = CreateHelper(uriString, false, UriKind.Absolute, ref e, in creationOptions);
281+
result?.DebugSetLeftCtor();
282+
return e is null && result != null;
283+
}
284+
262285
public static bool TryCreate(Uri? baseUri, string? relativeUri, [NotNullWhen(true)] out Uri? result)
263286
{
264287
if (TryCreate(relativeUri, UriKind.RelativeOrAbsolute, out Uri? relativeLink))
@@ -309,6 +332,16 @@ public static bool TryCreate(Uri? baseUri, Uri? relativeUri, [NotNullWhen(true)]
309332
}
310333

311334
public string GetComponents(UriComponents components, UriFormat format)
335+
{
336+
if (DisablePathAndQueryCanonicalization && (components & (UriComponents.Path | UriComponents.Query)) != 0)
337+
{
338+
throw new InvalidOperationException(SR.net_uri_GetComponentsCalledWhenCanonicalizationDisabled);
339+
}
340+
341+
return InternalGetComponents(components, format);
342+
}
343+
344+
private string InternalGetComponents(UriComponents components, UriFormat format)
312345
{
313346
if (((components & UriComponents.SerializationInfoString) != 0) && components != UriComponents.SerializationInfoString)
314347
throw new ArgumentOutOfRangeException(nameof(components), components, SR.net_uri_NotJustSerialization);
@@ -590,7 +623,7 @@ private Uri(Flags flags, UriParser? uriParser, string uri)
590623
//
591624
// a Uri.TryCreate() method goes through here.
592625
//
593-
internal static Uri? CreateHelper(string uriString, bool dontEscape, UriKind uriKind, ref UriFormatException? e)
626+
internal static Uri? CreateHelper(string uriString, bool dontEscape, UriKind uriKind, ref UriFormatException? e, in UriCreationOptions creationOptions = default)
594627
{
595628
// if (!Enum.IsDefined(typeof(UriKind), uriKind)) -- We currently believe that Enum.IsDefined() is too slow
596629
// to be used here.
@@ -606,6 +639,9 @@ private Uri(Flags flags, UriParser? uriParser, string uri)
606639
if (dontEscape)
607640
flags |= Flags.UserEscaped;
608641

642+
if (creationOptions.DangerousDisablePathAndQueryCanonicalization)
643+
flags |= Flags.DisablePathAndQueryCanonicalization;
644+
609645
// We won't use User factory for these errors
610646
if (err != ParsingError.None)
611647
{

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,9 @@ protected virtual string GetComponents(Uri uri, UriComponents components, UriFor
146146
if (!uri.IsAbsoluteUri)
147147
throw new InvalidOperationException(SR.net_uri_NotAbsolute);
148148

149+
if (uri.DisablePathAndQueryCanonicalization && (components & (UriComponents.Path | UriComponents.Query)) != 0)
150+
throw new InvalidOperationException(SR.net_uri_GetComponentsCalledWhenCanonicalizationDisabled);
151+
149152
return uri.GetComponentsHelper(components, format);
150153
}
151154

src/libraries/System.Private.Uri/tests/FunctionalTests/System.Private.Uri.Functional.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
<Compile Include="UriBuilderParameterTest.cs" />
1818
<Compile Include="UriBuilderRefreshTest.cs" />
1919
<Compile Include="UriBuilderTests.cs" />
20+
<Compile Include="UriCreationOptionsTest.cs" />
2021
<Compile Include="UriEscapingTest.cs" />
2122
<Compile Include="UriGetComponentsTest.cs" />
2223
<Compile Include="UriIpHostTest.cs" />

0 commit comments

Comments
 (0)