diff --git a/src/libraries/System.Private.Uri/src/Resources/Strings.resx b/src/libraries/System.Private.Uri/src/Resources/Strings.resx
index 92a87cd44ccaa1..9975314e3d97c3 100644
--- a/src/libraries/System.Private.Uri/src/Resources/Strings.resx
+++ b/src/libraries/System.Private.Uri/src/Resources/Strings.resx
@@ -198,4 +198,7 @@
UriParser's base InitializeAndValidate may only be called once on a single Uri instance and only from an override of InitializeAndValidate.
+
+ GetComponents() may not be used for Path/Query on a Uri instance created with UriCreationOptions.DangerousDisablePathAndQueryCanonicalization.
+
\ No newline at end of file
diff --git a/src/libraries/System.Private.Uri/src/System.Private.Uri.csproj b/src/libraries/System.Private.Uri/src/System.Private.Uri.csproj
index 4b63c5b3cb3061..ce5a97d87051bf 100644
--- a/src/libraries/System.Private.Uri/src/System.Private.Uri.csproj
+++ b/src/libraries/System.Private.Uri/src/System.Private.Uri.csproj
@@ -24,6 +24,7 @@
+
diff --git a/src/libraries/System.Private.Uri/src/System/Uri.cs b/src/libraries/System.Private.Uri/src/System/Uri.cs
index ad6efdba8540d1..7d1b4d72201a38 100644
--- a/src/libraries/System.Private.Uri/src/System/Uri.cs
+++ b/src/libraries/System.Private.Uri/src/System/Uri.cs
@@ -121,6 +121,11 @@ internal enum Flags : ulong
IriCanonical = 0x78000000000,
UnixPath = 0x100000000000,
+ ///
+ /// Disables any validation/normalization past the authority. Fragments will always be empty. GetComponents will throw for Path/Query.
+ ///
+ DisablePathAndQueryCanonicalization = 0x200000000000,
+
///
/// Used to ensure that InitializeAndValidate is only called once per Uri instance and only from an override of InitializeAndValidate
///
@@ -267,6 +272,8 @@ internal static bool IriParsingStatic(UriParser? syntax)
return syntax is null || syntax.InFact(UriSyntaxFlags.AllowIriParsing);
}
+ internal bool DisablePathAndQueryCanonicalization => (_flags & Flags.DisablePathAndQueryCanonicalization) != 0;
+
internal bool UserDrivenParsing
{
get
@@ -410,6 +417,20 @@ public Uri(string uriString, UriKind uriKind)
DebugSetLeftCtor();
}
+ ///
+ /// Initializes a new instance of the class with the specified URI and additional .
+ ///
+ /// A string that identifies the resource to be represented by the instance.
+ /// Options that control how the is created and behaves.
+ public Uri(string uriString, in UriCreationOptions creationOptions)
+ {
+ if (uriString is null)
+ throw new ArgumentNullException(nameof(uriString));
+
+ CreateThis(uriString, false, UriKind.Absolute, in creationOptions);
+ DebugSetLeftCtor();
+ }
+
//
// Uri(Uri, string)
//
@@ -1639,6 +1660,9 @@ public override bool Equals([NotNullWhen(true)] object? comparand)
// canonicalize the comparand, making comparison possible
if (obj is null)
{
+ if (DisablePathAndQueryCanonicalization)
+ return false;
+
if (!(comparand is string s))
return false;
@@ -1649,6 +1673,9 @@ public override bool Equals([NotNullWhen(true)] object? comparand)
return false;
}
+ if (DisablePathAndQueryCanonicalization != obj.DisablePathAndQueryCanonicalization)
+ return false;
+
if (ReferenceEquals(OriginalString, obj.OriginalString))
{
return true;
@@ -2553,7 +2580,7 @@ private unsafe void GetHostViaCustomSyntax()
//
internal string GetParts(UriComponents uriParts, UriFormat formatAs)
{
- return GetComponents(uriParts, formatAs);
+ return InternalGetComponents(uriParts, formatAs);
}
private string GetEscapedParts(UriComponents uriParts)
@@ -3158,9 +3185,6 @@ private unsafe void ParseRemaining()
idx = _info.Offset.Path;
origIdx = _info.Offset.Path;
- //Some uris do not have a query
- // When '?' is passed as delimiter, then it's special case
- // so both '?' and '#' will work as delimiters
if (buildIriStringFromPath)
{
DebugAssertInCtor();
@@ -3180,6 +3204,45 @@ private unsafe void ParseRemaining()
_info.Offset.Path = (ushort)_string.Length;
idx = _info.Offset.Path;
+ }
+
+ // If the user explicitly disabled canonicalization, only figure out the offsets
+ if (DisablePathAndQueryCanonicalization)
+ {
+ if (buildIriStringFromPath)
+ {
+ DebugAssertInCtor();
+ _string += _originalUnicodeString.Substring(origIdx);
+ }
+
+ string str = _string;
+
+ if (IsImplicitFile || (syntaxFlags & UriSyntaxFlags.MayHaveQuery) == 0)
+ {
+ idx = str.Length;
+ }
+ else
+ {
+ idx = str.IndexOf('?');
+ if (idx == -1)
+ {
+ idx = str.Length;
+ }
+ }
+
+ _info.Offset.Query = (ushort)idx;
+ _info.Offset.Fragment = (ushort)str.Length; // There is no fragment in UseRawTarget mode
+ _info.Offset.End = (ushort)str.Length;
+
+ goto Done;
+ }
+
+ //Some uris do not have a query
+ // When '?' is passed as delimiter, then it's special case
+ // so both '?' and '#' will work as delimiters
+ if (buildIriStringFromPath)
+ {
+ DebugAssertInCtor();
int offset = origIdx;
if (IsImplicitFile || ((syntaxFlags & (UriSyntaxFlags.MayHaveQuery | UriSyntaxFlags.MayHaveFragment)) == 0))
diff --git a/src/libraries/System.Private.Uri/src/System/UriCreationOptions.cs b/src/libraries/System.Private.Uri/src/System/UriCreationOptions.cs
new file mode 100644
index 00000000000000..2d0ee84d08b218
--- /dev/null
+++ b/src/libraries/System.Private.Uri/src/System/UriCreationOptions.cs
@@ -0,0 +1,28 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace System
+{
+ ///
+ /// Options that control how a is created and behaves.
+ ///
+ public struct UriCreationOptions
+ {
+ private bool _disablePathAndQueryCanonicalization;
+
+ ///
+ /// Disables validation and normalization of the Path and Query.
+ /// No transformations of the URI past the Authority will take place.
+ /// instances created with this option do not support s.
+ /// may not be used for or .
+ /// Be aware that disabling canonicalization also means that reserved characters will not be escaped,
+ /// which may corrupt the HTTP request and makes the application subject to request smuggling.
+ /// Only set this option if you have ensured that the URI string is already sanitized.
+ ///
+ public bool DangerousDisablePathAndQueryCanonicalization
+ {
+ readonly get => _disablePathAndQueryCanonicalization;
+ set => _disablePathAndQueryCanonicalization = value;
+ }
+ }
+}
diff --git a/src/libraries/System.Private.Uri/src/System/UriExt.cs b/src/libraries/System.Private.Uri/src/System/UriExt.cs
index a644dfa566699a..070e11a7f987f3 100644
--- a/src/libraries/System.Private.Uri/src/System/UriExt.cs
+++ b/src/libraries/System.Private.Uri/src/System/UriExt.cs
@@ -13,7 +13,7 @@ public partial class Uri
//
// All public ctors go through here
//
- private void CreateThis(string? uri, bool dontEscape, UriKind uriKind)
+ private void CreateThis(string? uri, bool dontEscape, UriKind uriKind, in UriCreationOptions creationOptions = default)
{
DebugAssertInCtor();
@@ -31,6 +31,9 @@ private void CreateThis(string? uri, bool dontEscape, UriKind uriKind)
if (dontEscape)
_flags |= Flags.UserEscaped;
+ if (creationOptions.DangerousDisablePathAndQueryCanonicalization)
+ _flags |= Flags.DisablePathAndQueryCanonicalization;
+
ParsingError err = ParseScheme(_string, ref _flags, ref _syntax!);
InitializeUri(err, uriKind, out UriFormatException? e);
@@ -259,6 +262,26 @@ public static bool TryCreate([NotNullWhen(true)] string? uriString, UriKind uriK
return e is null && result != null;
}
+ ///
+ /// Creates a new using the specified instance and .
+ ///
+ /// The string representation of the .
+ /// Options that control how the is created and behaves.
+ /// The constructed .
+ /// if the was successfully created; otherwise, .
+ public static bool TryCreate([NotNullWhen(true)] string? uriString, in UriCreationOptions creationOptions, [NotNullWhen(true)] out Uri? result)
+ {
+ if (uriString is null)
+ {
+ result = null;
+ return false;
+ }
+ UriFormatException? e = null;
+ result = CreateHelper(uriString, false, UriKind.Absolute, ref e, in creationOptions);
+ result?.DebugSetLeftCtor();
+ return e is null && result != null;
+ }
+
public static bool TryCreate(Uri? baseUri, string? relativeUri, [NotNullWhen(true)] out Uri? result)
{
if (TryCreate(relativeUri, UriKind.RelativeOrAbsolute, out Uri? relativeLink))
@@ -309,6 +332,16 @@ public static bool TryCreate(Uri? baseUri, Uri? relativeUri, [NotNullWhen(true)]
}
public string GetComponents(UriComponents components, UriFormat format)
+ {
+ if (DisablePathAndQueryCanonicalization && (components & (UriComponents.Path | UriComponents.Query)) != 0)
+ {
+ throw new InvalidOperationException(SR.net_uri_GetComponentsCalledWhenCanonicalizationDisabled);
+ }
+
+ return InternalGetComponents(components, format);
+ }
+
+ private string InternalGetComponents(UriComponents components, UriFormat format)
{
if (((components & UriComponents.SerializationInfoString) != 0) && components != UriComponents.SerializationInfoString)
throw new ArgumentOutOfRangeException(nameof(components), components, SR.net_uri_NotJustSerialization);
@@ -590,7 +623,7 @@ private Uri(Flags flags, UriParser? uriParser, string uri)
//
// a Uri.TryCreate() method goes through here.
//
- internal static Uri? CreateHelper(string uriString, bool dontEscape, UriKind uriKind, ref UriFormatException? e)
+ internal static Uri? CreateHelper(string uriString, bool dontEscape, UriKind uriKind, ref UriFormatException? e, in UriCreationOptions creationOptions = default)
{
// if (!Enum.IsDefined(typeof(UriKind), uriKind)) -- We currently believe that Enum.IsDefined() is too slow
// to be used here.
@@ -606,6 +639,9 @@ private Uri(Flags flags, UriParser? uriParser, string uri)
if (dontEscape)
flags |= Flags.UserEscaped;
+ if (creationOptions.DangerousDisablePathAndQueryCanonicalization)
+ flags |= Flags.DisablePathAndQueryCanonicalization;
+
// We won't use User factory for these errors
if (err != ParsingError.None)
{
diff --git a/src/libraries/System.Private.Uri/src/System/UriScheme.cs b/src/libraries/System.Private.Uri/src/System/UriScheme.cs
index d9d96ab59ea3cb..7c39a6c44a336e 100644
--- a/src/libraries/System.Private.Uri/src/System/UriScheme.cs
+++ b/src/libraries/System.Private.Uri/src/System/UriScheme.cs
@@ -146,6 +146,9 @@ protected virtual string GetComponents(Uri uri, UriComponents components, UriFor
if (!uri.IsAbsoluteUri)
throw new InvalidOperationException(SR.net_uri_NotAbsolute);
+ if (uri.DisablePathAndQueryCanonicalization && (components & (UriComponents.Path | UriComponents.Query)) != 0)
+ throw new InvalidOperationException(SR.net_uri_GetComponentsCalledWhenCanonicalizationDisabled);
+
return uri.GetComponentsHelper(components, format);
}
diff --git a/src/libraries/System.Private.Uri/tests/FunctionalTests/System.Private.Uri.Functional.Tests.csproj b/src/libraries/System.Private.Uri/tests/FunctionalTests/System.Private.Uri.Functional.Tests.csproj
index 8f07bb90af1ffe..21bb5200471aa1 100644
--- a/src/libraries/System.Private.Uri/tests/FunctionalTests/System.Private.Uri.Functional.Tests.csproj
+++ b/src/libraries/System.Private.Uri/tests/FunctionalTests/System.Private.Uri.Functional.Tests.csproj
@@ -17,6 +17,7 @@
+
diff --git a/src/libraries/System.Private.Uri/tests/FunctionalTests/UriCreationOptionsTest.cs b/src/libraries/System.Private.Uri/tests/FunctionalTests/UriCreationOptionsTest.cs
new file mode 100644
index 00000000000000..3f7438738f0bf9
--- /dev/null
+++ b/src/libraries/System.Private.Uri/tests/FunctionalTests/UriCreationOptionsTest.cs
@@ -0,0 +1,294 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Linq;
+using Xunit;
+
+namespace System.PrivateUri.Tests
+{
+ public class UriCreationOptionsTest
+ {
+ [Fact]
+ public void UriCreationOptions_HasReasonableDefaults()
+ {
+ UriCreationOptions options = default;
+
+ Assert.False(options.DangerousDisablePathAndQueryCanonicalization);
+ }
+
+ [Fact]
+ public void UriCreationOptions_StoresCorrectValues()
+ {
+ var options = new UriCreationOptions { DangerousDisablePathAndQueryCanonicalization = true };
+ Assert.True(options.DangerousDisablePathAndQueryCanonicalization);
+
+ options = new UriCreationOptions { DangerousDisablePathAndQueryCanonicalization = false };
+ Assert.False(options.DangerousDisablePathAndQueryCanonicalization);
+ }
+
+ public static IEnumerable