diff --git a/src/libraries/System.Private.Uri/src/System/PercentEncodingHelper.cs b/src/libraries/System.Private.Uri/src/System/PercentEncodingHelper.cs index 11cbc4f49d4f7e..3261ba739b66e9 100644 --- a/src/libraries/System.Private.Uri/src/System/PercentEncodingHelper.cs +++ b/src/libraries/System.Private.Uri/src/System/PercentEncodingHelper.cs @@ -10,15 +10,10 @@ namespace System { internal static class PercentEncodingHelper { - public static int UnescapePercentEncodedUTF8Sequence(ReadOnlySpan input, ref ValueStringBuilder dest, bool isQuery, bool iriParsing) + public static int UnescapePercentEncodedUTF8Sequence(scoped ReadOnlySpan input, ref ValueStringBuilder dest, bool isQuery, bool iriParsing) { - int length = input.Length; - - // The following assertions rely on the input not mutating mid-operation, as is the case currently since callers are working with strings - // If we start accepting input such as spans, this method must be audited to ensure no buffer overruns/infinite loops could occur - // As an optimization, this method should only be called after the first character is known to be a part of a non-ascii UTF8 sequence - Debug.Assert(length >= 3); + Debug.Assert(input.Length >= 3); Debug.Assert(input[0] == '%'); Debug.Assert(UriHelper.DecodeHexChars(input[1], input[2]) != Uri.c_DummyChar); Debug.Assert(UriHelper.DecodeHexChars(input[1], input[2]) >= 128); @@ -34,7 +29,7 @@ public static int UnescapePercentEncodedUTF8Sequence(ReadOnlySpan input, r int i = totalCharsConsumed + (bytesLeftInBuffer * 3); ReadByteFromInput: - if ((uint)(length - i) <= 2 || input[i] != '%') + if ((uint)(input.Length - i) <= 2 || input[i] != '%') goto NoMoreOrInvalidInput; uint value = input[i + 1]; diff --git a/src/libraries/System.Private.Uri/src/System/Uri.cs b/src/libraries/System.Private.Uri/src/System/Uri.cs index 6e3777358beb99..7d47124a72c662 100644 --- a/src/libraries/System.Private.Uri/src/System/Uri.cs +++ b/src/libraries/System.Private.Uri/src/System/Uri.cs @@ -1024,7 +1024,7 @@ private string GetLocalPath() string str = (IsImplicitFile && _info.Offset.Host == (IsDosPath ? 0 : 2) && _info.Offset.Query == _info.Offset.End) ? _string - : (IsDosPath && (_string[start] == '/' || _string[start] == '\\')) + : (IsDosPath && _string[start] is '/' or '\\') ? _string.Substring(start + 1, _info.Offset.Query - start - 1) : _string.Substring(start, _info.Offset.Query - start); @@ -1042,39 +1042,39 @@ private string GetLocalPath() return str; } - char[] result; - int count = 0; - start = _info.Offset.Path; + var result = new ValueStringBuilder(stackalloc char[StackallocThreshold]); string host = _info.Host; - result = new char[host.Length + 3 + _info.Offset.Fragment - _info.Offset.Path]; + start = _info.Offset.Path; if (IsUncPath) { - result[0] = '\\'; - result[1] = '\\'; - count = 2; - - UriHelper.UnescapeString(host, 0, host.Length, result, ref count, c_DummyChar, c_DummyChar, - c_DummyChar, UnescapeMode.CopyOnly, _syntax, false); + result.Append('\\'); + result.Append('\\'); + result.Append(host); } else { // Dos path - if (_string[start] == '/' || _string[start] == '\\') + if (_string[start] is '/' or '\\') { // Skip leading slash for a DOS path - ++start; + start++; } } + int pathStart = result.Length; // save for optional Compress() call - int pathStart = count; // save for optional Compress() call + ReadOnlySpan path = _string.AsSpan(start, _info.Offset.Query - start); - UnescapeMode mode = (InFact(Flags.PathNotCanonical) && !IsImplicitFile) - ? (UnescapeMode.Unescape | UnescapeMode.UnescapeAll) : UnescapeMode.CopyOnly; - UriHelper.UnescapeString(_string, start, _info.Offset.Query, result, ref count, c_DummyChar, - c_DummyChar, c_DummyChar, mode, _syntax, true); + if (InFact(Flags.PathNotCanonical) && !IsImplicitFile) + { + UriHelper.Unescape(path, ref result); + } + else + { + result.Append(path); + } // Possibly convert c|\ into c:\ if (result[1] == '|') @@ -1084,16 +1084,16 @@ private string GetLocalPath() { // suspecting not compressed path // For a dos path we won't compress the "x:" part if found /../ sequences - Compress(result, IsDosPath ? pathStart + 2 : pathStart, ref count, _syntax); + Compress(ref result, IsDosPath ? pathStart + 2 : pathStart, _syntax); } // We don't know whether all slashes were the back ones // Plus going through Compress will turn them into / anyway // Converting / back into \ - Span slashSpan = result.AsSpan(0, count); + Span slashSpan = result.RawChars.Slice(0, result.Length); slashSpan.Replace('/', '\\'); - return new string(result, 0, count); + return result.ToString(); } else { @@ -1243,15 +1243,7 @@ public string IdnHost // It might be a registry-based host from RFC 2396 Section 3.2.1 else if (hostType == Flags.BasicHostType && InFact(Flags.HostNotCanonical | Flags.E_HostNotCanonical)) { - // Unescape everything - var dest = new ValueStringBuilder(stackalloc char[StackallocThreshold]); - - UriHelper.UnescapeString(host, 0, host.Length, ref dest, - c_DummyChar, c_DummyChar, c_DummyChar, - UnescapeMode.Unescape | UnescapeMode.UnescapeAll, - _syntax, isQuery: false); - - host = dest.ToString(); + host = UnescapeDataString(host); } _info.IdnHost = host; @@ -2865,10 +2857,7 @@ private ReadOnlySpan RecreateParts(scoped ref ValueStringBuilder dest, str break; case UriFormat.Unescaped: - UriHelper.UnescapeString(slice, - ref dest, c_DummyChar, c_DummyChar, c_DummyChar, - UnescapeMode.Unescape | UnescapeMode.UnescapeAll, - _syntax, isQuery: false); + UriHelper.Unescape(slice, ref dest); break; default: //V1ToStringUnescape @@ -2895,20 +2884,6 @@ private ReadOnlySpan RecreateParts(scoped ref ValueStringBuilder dest, str if (host.Length != 0) { - UnescapeMode mode; - if (formatAs != UriFormat.UriEscaped && HostType == Flags.BasicHostType - && (nonCanonical & (ushort)UriComponents.Host) != 0) - { - // only Basic host could be in the escaped form - mode = formatAs == UriFormat.Unescaped - ? (UnescapeMode.Unescape | UnescapeMode.UnescapeAll) : - (InFact(Flags.UserEscaped) ? UnescapeMode.Unescape : UnescapeMode.EscapeUnescape); - } - else - { - mode = UnescapeMode.CopyOnly; - } - var hostBuilder = new ValueStringBuilder(stackalloc char[StackallocThreshold]); // NormalizedHost @@ -2923,10 +2898,27 @@ private ReadOnlySpan RecreateParts(scoped ref ValueStringBuilder dest, str } } - UriHelper.UnescapeString(hostBuilder.Length == 0 ? host : hostBuilder.AsSpan(), - ref dest, '/', '?', '#', - mode, - _syntax, isQuery: false); + ReadOnlySpan hostSlice = hostBuilder.Length == 0 ? host : hostBuilder.AsSpan(); + + if (formatAs != UriFormat.UriEscaped && HostType == Flags.BasicHostType && (nonCanonical & (ushort)UriComponents.Host) != 0) + { + // only Basic host could be in the escaped form + if (formatAs == UriFormat.Unescaped) + { + UriHelper.Unescape(hostSlice, ref dest); + } + else + { + UriHelper.UnescapeString(hostSlice, + ref dest, '/', '?', '#', + InFact(Flags.UserEscaped) ? UnescapeMode.Unescape : UnescapeMode.EscapeUnescape, + _syntax, isQuery: false); + } + } + else + { + dest.Append(hostSlice); + } hostBuilder.Dispose(); @@ -2981,37 +2973,8 @@ private ReadOnlySpan RecreateParts(scoped ref ValueStringBuilder dest, str if (parts != UriComponents.Query) dest.Append('?'); - UnescapeMode mode = UnescapeMode.CopyOnly; - - if ((nonCanonical & (ushort)UriComponents.Query) != 0) - { - if (formatAs == UriFormat.UriEscaped) - { - if (NotAny(Flags.UserEscaped)) - { - UriHelper.EscapeString( - str.AsSpan(offset, _info.Offset.Fragment - offset), - ref dest, checkExistingEscaped: true, UriHelper.UnreservedReservedExceptHash); - - goto AfterQuery; - } - } - else - { - mode = formatAs switch - { - V1ToStringUnescape => (InFact(Flags.UserEscaped) ? UnescapeMode.Unescape : UnescapeMode.EscapeUnescape) | UnescapeMode.V1ToStringFlag, - UriFormat.Unescaped => UnescapeMode.Unescape | UnescapeMode.UnescapeAll, - _ => InFact(Flags.UserEscaped) ? UnescapeMode.Unescape : UnescapeMode.EscapeUnescape - }; - } - } - - UriHelper.UnescapeString(str, offset, _info.Offset.Fragment, - ref dest, '#', c_DummyChar, c_DummyChar, - mode, _syntax, isQuery: true); + FormatQueryOrFragment(str.AsSpan(offset, _info.Offset.Fragment - offset), ref dest, nonCanonical, formatAs, isQuery: true); } - AfterQuery: //Fragment (possibly strip the '#' delimiter) if ((parts & UriComponents.Fragment) != 0 && _info.Offset.Fragment < _info.Offset.End) @@ -3020,39 +2983,49 @@ private ReadOnlySpan RecreateParts(scoped ref ValueStringBuilder dest, str if (parts != UriComponents.Fragment) dest.Append('#'); - UnescapeMode mode = UnescapeMode.CopyOnly; + FormatQueryOrFragment(str.AsSpan(offset, _info.Offset.End - offset), ref dest, nonCanonical, formatAs, isQuery: false); + } - if ((nonCanonical & (ushort)UriComponents.Fragment) != 0) + return dest.AsSpan(); + + void FormatQueryOrFragment(ReadOnlySpan slice, ref ValueStringBuilder dest, ushort nonCanonical, UriFormat formatAs, bool isQuery) + { + if ((nonCanonical & (ushort)(isQuery ? UriComponents.Query : UriComponents.Fragment)) == 0) + { + dest.Append(slice); + } + else { if (formatAs == UriFormat.UriEscaped) { if (NotAny(Flags.UserEscaped)) { - UriHelper.EscapeString( - str.AsSpan(offset, _info.Offset.End - offset), - ref dest, checkExistingEscaped: true, UriHelper.UnreservedReserved); - - goto AfterFragment; + UriHelper.EscapeString(slice, ref dest, checkExistingEscaped: true, isQuery ? UriHelper.UnreservedReservedExceptHash : UriHelper.UnreservedReserved); } + else + { + dest.Append(slice); + } + } + else if (formatAs == UriFormat.Unescaped) + { + UriHelper.Unescape(slice, ref dest); } else { - mode = formatAs switch + UnescapeMode mode = InFact(Flags.UserEscaped) ? UnescapeMode.Unescape : UnescapeMode.EscapeUnescape; + + if (formatAs == V1ToStringUnescape) { - V1ToStringUnescape => (InFact(Flags.UserEscaped) ? UnescapeMode.Unescape : UnescapeMode.EscapeUnescape) | UnescapeMode.V1ToStringFlag, - UriFormat.Unescaped => UnescapeMode.Unescape | UnescapeMode.UnescapeAll, - _ => InFact(Flags.UserEscaped) ? UnescapeMode.Unescape : UnescapeMode.EscapeUnescape - }; + mode |= UnescapeMode.V1ToStringFlag; + } + + UriHelper.UnescapeString(slice, + ref dest, '#', c_DummyChar, c_DummyChar, + mode, _syntax, isQuery); } } - - UriHelper.UnescapeString(str, offset, _info.Offset.End, - ref dest, '#', c_DummyChar, c_DummyChar, - mode, _syntax, isQuery: false); } - - AfterFragment: - return dest.AsSpan(); } // @@ -4449,10 +4422,7 @@ private unsafe void GetCanonicalPath(ref ValueStringBuilder dest, UriFormat form if (InFact(Flags.ShouldBeCompressed) && dest.Length - offset > 0) { // It will also convert back slashes if needed - dest.Length = offset + UriHelper.Compress( - dest.RawChars.Slice(offset, dest.Length - offset), - _syntax.InFact(UriSyntaxFlags.ConvertPathSlashes), - _syntax.InFact(UriSyntaxFlags.CanonicalizeAsFilePath)); + Compress(ref dest, offset, _syntax); if (dest[start] == '\\') dest[start] = '/'; @@ -4477,46 +4447,48 @@ private unsafe void GetCanonicalPath(ref ValueStringBuilder dest, UriFormat form if (formatAs != UriFormat.UriEscaped && InFact(Flags.PathNotCanonical)) { - UnescapeMode mode; - switch (formatAs) - { - case V1ToStringUnescape: + ReadOnlySpan slice = dest.AsSpan(start, dest.Length - start); - mode = (InFact(Flags.UserEscaped) ? UnescapeMode.Unescape : UnescapeMode.EscapeUnescape) - | UnescapeMode.V1ToStringFlag; - if (IsImplicitFile) - mode &= ~UnescapeMode.Unescape; - break; + if (formatAs == UriFormat.Unescaped) + { + if (!IsImplicitFile) + { + // Unescape in-place + dest.Length = start; + UriHelper.Unescape(slice, ref dest); + Debug.Assert(slice.Overlaps(dest.RawChars)); + } + } + else + { + // UriFormat.SafeUnescaped / V1ToStringUnescape + UnescapeMode mode = InFact(Flags.UserEscaped) ? UnescapeMode.Unescape : UnescapeMode.EscapeUnescape; - case UriFormat.Unescaped: - mode = IsImplicitFile ? UnescapeMode.CopyOnly - : UnescapeMode.Unescape | UnescapeMode.UnescapeAll; - break; + if (IsImplicitFile) + { + mode &= ~UnescapeMode.Unescape; + } - default: // UriFormat.SafeUnescaped + if (formatAs == V1ToStringUnescape) + { + mode |= UnescapeMode.V1ToStringFlag; + } - mode = InFact(Flags.UserEscaped) ? UnescapeMode.Unescape : UnescapeMode.EscapeUnescape; - if (IsImplicitFile) - mode &= ~UnescapeMode.Unescape; - break; - } + if (mode != UnescapeMode.None) + { + // We can't do an in-place escape/unescape, create a copy + var copy = new ValueStringBuilder(stackalloc char[StackallocThreshold]); + copy.Append(slice); - if (mode != UnescapeMode.CopyOnly) - { - // We can't do an in-place unescape, create a copy - var copy = new ValueStringBuilder(stackalloc char[StackallocThreshold]); - copy.Append(dest.AsSpan(start, dest.Length - start)); + dest.Length = start; - dest.Length = start; - fixed (char* pCopy = copy) - { - UriHelper.UnescapeString(pCopy, 0, copy.Length, + UriHelper.UnescapeString(copy.AsSpan(), ref dest, '?', '#', c_DummyChar, mode, _syntax, isQuery: false); - } - copy.Dispose(); + copy.Dispose(); + } } } } @@ -4589,6 +4561,16 @@ private static unsafe void UnescapeOnly(char* pch, int start, ref int end, char end -= (int)(pch - pnew); } + private static void Compress(ref ValueStringBuilder dest, int start, UriParser syntax) + { + Debug.Assert(start <= dest.Length); + + dest.Length = start + UriHelper.Compress( + dest.RawChars.Slice(start, dest.Length - start), + syntax.InFact(UriSyntaxFlags.ConvertPathSlashes), + syntax.InFact(UriSyntaxFlags.CanonicalizeAsFilePath)); + } + private static void Compress(char[] dest, int start, ref int destLength, UriParser syntax) { destLength = start + UriHelper.Compress( @@ -4953,11 +4935,7 @@ protected virtual string Unescape(string path) // to the derived class without any permission demand. // Should be deprecated and removed asap. - char[] dest = new char[path.Length]; - int count = 0; - dest = UriHelper.UnescapeString(path, 0, path.Length, dest, ref count, c_DummyChar, c_DummyChar, - c_DummyChar, UnescapeMode.Unescape | UnescapeMode.UnescapeAll, null, false); - return new string(dest, 0, count); + return UnescapeDataString(path); } [Obsolete("Uri.EscapeString has been deprecated. Use GetComponents() or Uri.EscapeDataString to escape a Uri component or a string.")] diff --git a/src/libraries/System.Private.Uri/src/System/UriEnumTypes.cs b/src/libraries/System.Private.Uri/src/System/UriEnumTypes.cs index 1a1c3eb5fef7f9..17d9456ba77328 100644 --- a/src/libraries/System.Private.Uri/src/System/UriEnumTypes.cs +++ b/src/libraries/System.Private.Uri/src/System/UriEnumTypes.cs @@ -84,11 +84,10 @@ internal enum ParsingError [Flags] internal enum UnescapeMode { - CopyOnly = 0x0, // used for V1.0 ToString() compatibility mode only + None = 0x0, Escape = 0x1, // Only used by ImplicitFile, the string is already fully unescaped Unescape = 0x2, // Only used as V1.0 UserEscaped compatibility mode EscapeUnescape = Unescape | Escape, // does both escaping control+reserved and unescaping of safe characters V1ToStringFlag = 0x4, // Only used as V1.0 ToString() compatibility mode, assumes DontEscape level also - UnescapeAll = 0x8, // just unescape everything, leave bad escaped sequences as is } } diff --git a/src/libraries/System.Private.Uri/src/System/UriExt.cs b/src/libraries/System.Private.Uri/src/System/UriExt.cs index 24de5ab699e950..4558cd997cc6aa 100644 --- a/src/libraries/System.Private.Uri/src/System/UriExt.cs +++ b/src/libraries/System.Private.Uri/src/System/UriExt.cs @@ -601,11 +601,7 @@ private static string UnescapeDataString(ReadOnlySpan charsToUnescape, str // We may throw for very large inputs (when growing the ValueStringBuilder). vsb.EnsureCapacity(charsToUnescape.Length - indexOfFirstToUnescape); - UriHelper.UnescapeString( - charsToUnescape.Slice(indexOfFirstToUnescape), ref vsb, - c_DummyChar, c_DummyChar, c_DummyChar, - UnescapeMode.Unescape | UnescapeMode.UnescapeAll, - syntax: null, isQuery: false); + UriHelper.Unescape(charsToUnescape.Slice(indexOfFirstToUnescape), ref vsb); string result = string.Concat(charsToUnescape.Slice(0, indexOfFirstToUnescape), vsb.AsSpan()); vsb.Dispose(); @@ -651,11 +647,7 @@ public static bool TryUnescapeDataString(ReadOnlySpan charsToUnescape, Spa vsb = new ValueStringBuilder(destination.Slice(indexOfFirstToUnescape)); } - UriHelper.UnescapeString( - charsToUnescape.Slice(indexOfFirstToUnescape), ref vsb, - c_DummyChar, c_DummyChar, c_DummyChar, - UnescapeMode.Unescape | UnescapeMode.UnescapeAll, - syntax: null, isQuery: false); + UriHelper.Unescape(charsToUnescape.Slice(indexOfFirstToUnescape), ref vsb); int newLength = indexOfFirstToUnescape + vsb.Length; Debug.Assert(newLength <= charsToUnescape.Length); diff --git a/src/libraries/System.Private.Uri/src/System/UriHelper.cs b/src/libraries/System.Private.Uri/src/System/UriHelper.cs index 938940883f84a4..a1e897873e4f03 100644 --- a/src/libraries/System.Private.Uri/src/System/UriHelper.cs +++ b/src/libraries/System.Private.Uri/src/System/UriHelper.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Runtime.InteropServices; using System.Text; namespace System @@ -317,104 +316,87 @@ private static void EscapeStringToBuilder( } } - internal static unsafe char[] UnescapeString(string input, int start, int end, char[] dest, - ref int destPosition, char rsvd1, char rsvd2, char rsvd3, UnescapeMode unescapeMode, UriParser? syntax, - bool isQuery) + internal static void Unescape(scoped ReadOnlySpan chars, ref ValueStringBuilder dest) { - fixed (char* pStr = input) + for (int i = 0; (uint)i < (uint)chars.Length;) { - return UnescapeString(pStr, start, end, dest, ref destPosition, rsvd1, rsvd2, rsvd3, unescapeMode, - syntax, isQuery); - } - } + if (chars[i] == '%' && (uint)(i + 2) < (uint)chars.Length) + { + char unescaped = DecodeHexChars(chars[i + 1], chars[i + 2]); - internal static unsafe char[] UnescapeString(char* pStr, int start, int end, char[] dest, ref int destPosition, - char rsvd1, char rsvd2, char rsvd3, UnescapeMode unescapeMode, UriParser? syntax, bool isQuery) - { - ValueStringBuilder vsb = new ValueStringBuilder(dest.Length); - vsb.Append(dest.AsSpan(0, destPosition)); - UnescapeString(pStr, start, end, ref vsb, rsvd1, rsvd2, rsvd3, unescapeMode, - syntax, isQuery); + if (unescaped == Uri.c_DummyChar) + { + i++; + continue; + } - if (vsb.Length > dest.Length) - { - dest = vsb.AsSpan().ToArray(); - } - else - { - vsb.AsSpan(destPosition).TryCopyTo(dest.AsSpan(destPosition)); - } - destPosition = vsb.Length; - vsb.Dispose(); - return dest; - } + // Copy previous characters that don't require any transformations. + // Using a loop instead of Append(span) to avoid the call overhead for typically short sections. + foreach (char c in chars.Slice(0, i)) + { + dest.Append(c); + } - // - // This method will assume that any good Escaped Sequence will be unescaped in the output - // - Assumes Dest.Length - detPosition >= end-start - // - UnescapeLevel controls various modes of operation - // - Any "bad" escape sequence will remain as is or '%' will be escaped. - // - destPosition tells the starting index in dest for placing the result. - // On return destPosition tells the last character + 1 position in the "dest" array. - // - The control chars and chars passed in rsdvX parameters may be re-escaped depending on UnescapeLevel - // - It is a RARE case when Unescape actually needs escaping some characters mentioned above. - // For this reason it returns a char[] that is usually the same ref as the input "dest" value. - // - internal static unsafe void UnescapeString(string input, int start, int end, ref ValueStringBuilder dest, - char rsvd1, char rsvd2, char rsvd3, UnescapeMode unescapeMode, UriParser? syntax, bool isQuery) - { - fixed (char* pStr = input) - { - UnescapeString(pStr, start, end, ref dest, rsvd1, rsvd2, rsvd3, unescapeMode, syntax, isQuery); - } - } - internal static unsafe void UnescapeString(scoped ReadOnlySpan input, scoped ref ValueStringBuilder dest, - char rsvd1, char rsvd2, char rsvd3, UnescapeMode unescapeMode, UriParser? syntax, bool isQuery) - { - fixed (char* pStr = &MemoryMarshal.GetReference(input)) - { - UnescapeString(pStr, 0, input.Length, ref dest, rsvd1, rsvd2, rsvd3, unescapeMode, syntax, isQuery); + if (char.IsAscii(unescaped)) + { + dest.Append(unescaped); + i += 3; + } + else + { + int charactersRead = PercentEncodingHelper.UnescapePercentEncodedUTF8Sequence( + chars.Slice(i), + ref dest, + isQuery: false, + iriParsing: false); + + Debug.Assert(charactersRead > 0); + i += charactersRead; + } + + chars = chars.Slice(i); + i = 0; + } + else + { + i++; + } } + + dest.Append(chars); } - internal static unsafe void UnescapeString(char* pStr, int start, int end, ref ValueStringBuilder dest, + + internal static void UnescapeString(scoped ReadOnlySpan chars, ref ValueStringBuilder dest, char rsvd1, char rsvd2, char rsvd3, UnescapeMode unescapeMode, UriParser? syntax, bool isQuery) { - if ((unescapeMode & UnescapeMode.EscapeUnescape) == UnescapeMode.CopyOnly) - { - dest.Append(new ReadOnlySpan(pStr + start, end - start)); - return; - } + Debug.Assert(unescapeMode != UnescapeMode.None); bool escapeReserved = false; bool iriParsing = Uri.IriParsingStatic(syntax) && ((unescapeMode & UnescapeMode.EscapeUnescape) == UnescapeMode.EscapeUnescape); - for (int next = start; next < end;) + while (!chars.IsEmpty) { + int i; char ch = (char)0; - for (; next < end; ++next) + for (i = 0; (uint)i < (uint)chars.Length; i++) { - if ((ch = pStr[next]) == '%') + ch = chars[i]; + + if (ch == '%') { if ((unescapeMode & UnescapeMode.Unescape) == 0) { // re-escape, don't check anything else escapeReserved = true; } - else if (next + 2 < end) + else if ((uint)(i + 2) < (uint)chars.Length) { - ch = DecodeHexChars(pStr[next + 1], pStr[next + 2]); - // Unescape a good sequence if full unescape is requested - if (unescapeMode >= UnescapeMode.UnescapeAll) - { - if (ch == Uri.c_DummyChar) - { - continue; - } - } + ch = DecodeHexChars(chars[i + 1], chars[i + 2]); + // re-escape % from an invalid sequence - else if (ch == Uri.c_DummyChar) + if (ch == Uri.c_DummyChar) { if ((unescapeMode & UnescapeMode.Escape) != 0) escapeReserved = true; @@ -424,36 +406,31 @@ internal static unsafe void UnescapeString(char* pStr, int start, int end, ref V // Do not unescape '%' itself unless full unescape is requested else if (ch == '%') { - next += 2; + i += 2; continue; } // Do not unescape a reserved char unless full unescape is requested else if (ch == rsvd1 || ch == rsvd2 || ch == rsvd3) { - next += 2; + i += 2; continue; } // Do not unescape a dangerous char unless it's V1ToStringFlags mode else if ((unescapeMode & UnescapeMode.V1ToStringFlag) == 0 && IsNotSafeForUnescape(ch)) { - next += 2; + i += 2; continue; } else if (iriParsing && (ch <= '\x9F' ? IsNotSafeForUnescape(ch) : !IriHelper.CheckIriUnicodeRange(ch, isQuery))) { // check if unenscaping gives a char outside iri range // if it does then keep it escaped - next += 2; + i += 2; continue; } // unescape escaped char or escape % break; } - else if (unescapeMode >= UnescapeMode.UnescapeAll) - { - // keep a '%' as part of a bogus sequence - continue; - } else { escapeReserved = true; @@ -461,11 +438,6 @@ internal static unsafe void UnescapeString(char* pStr, int start, int end, ref V // escape (escapeReserved==true) or otherwise unescape the sequence break; } - else if ((unescapeMode & (UnescapeMode.Unescape | UnescapeMode.UnescapeAll)) - == (UnescapeMode.Unescape | UnescapeMode.UnescapeAll)) - { - continue; - } else if ((unescapeMode & UnescapeMode.Escape) != 0) { // Could actually escape some of the characters @@ -485,38 +457,41 @@ internal static unsafe void UnescapeString(char* pStr, int start, int end, ref V } } - //copy off previous characters from input - while (start < next) - dest.Append(pStr[start++]); + // Copy previous characters that don't require any transformations. + // Using a loop instead of Append(span) to avoid the call overhead for typically short sections. + foreach (char c in chars.Slice(0, i)) + { + dest.Append(c); + } - if (next != end) + if (i < chars.Length) { if (escapeReserved) { - PercentEncodeByte((byte)pStr[next], ref dest); + PercentEncodeByte((byte)chars[i], ref dest); escapeReserved = false; - next++; + i++; } else if (ch <= 127) { dest.Append(ch); - next += 3; + i += 3; } else { // Unicode int charactersRead = PercentEncodingHelper.UnescapePercentEncodedUTF8Sequence( - new ReadOnlySpan(pStr + next, end - next), + chars.Slice(i), ref dest, isQuery, iriParsing); Debug.Assert(charactersRead > 0); - next += charactersRead; + i += charactersRead; } - - start = next; } + + chars = chars.Slice(i); } } diff --git a/src/libraries/System.Private.Uri/tests/FunctionalTests/UriParserTest.cs b/src/libraries/System.Private.Uri/tests/FunctionalTests/UriParserTest.cs index 6b6d5be47d12c3..c54e432067e5c8 100644 --- a/src/libraries/System.Private.Uri/tests/FunctionalTests/UriParserTest.cs +++ b/src/libraries/System.Private.Uri/tests/FunctionalTests/UriParserTest.cs @@ -469,6 +469,45 @@ public static void ReRegister() Assert.Throws(() => UriParser.Register(parser, scheme, 2006)); } + [Fact] + public static void NoQuery() + { + UriParser.Register(new GenericUriParser(GenericUriParserOptions.NoQuery), "no-query-scheme", 123); + + var uri = new Uri("no-query-scheme://host/path?query?#?fragment#"); + Assert.Equal("host", uri.Host); + Assert.Equal(123, uri.Port); + Assert.Equal("/path%3Fquery%3F", uri.AbsolutePath); + Assert.Equal(string.Empty, uri.Query); + Assert.Equal("#?fragment#", uri.Fragment); + } + + [Fact] + public static void NoFragment() + { + UriParser.Register(new GenericUriParser(GenericUriParserOptions.NoFragment), "no-fragment-scheme", 321); + + var uri = new Uri("no-fragment-scheme://host/path?query?#?fragment#"); + Assert.Equal("host", uri.Host); + Assert.Equal(321, uri.Port); + Assert.Equal("/path", uri.AbsolutePath); + Assert.Equal("?query?%23?fragment%23", uri.Query); + Assert.Equal(string.Empty, uri.Fragment); + } + + [Fact] + public static void NoQueryOrFragment() + { + UriParser.Register(new GenericUriParser(GenericUriParserOptions.NoQuery | GenericUriParserOptions.NoFragment), "no-queryfragment-scheme", 213); + + var uri = new Uri("no-queryfragment-scheme://host/path?query?#?fragment#"); + Assert.Equal("host", uri.Host); + Assert.Equal(213, uri.Port); + Assert.Equal("/path%3Fquery%3F%23%3Ffragment%23", uri.AbsolutePath); + Assert.Equal(string.Empty, uri.Query); + Assert.Equal(string.Empty, uri.Fragment); + } + #endregion UriParser tests #region GenericUriParser tests