Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 99 additions & 10 deletions src/libraries/System.Private.Uri/src/System/UriExt.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Licensed to the .NET Foundation under one or more agreements.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
Expand Down Expand Up @@ -552,28 +552,99 @@ internal unsafe bool InternalIsWellFormedOriginalString()
return true;
}

/// <summary>Converts a string to its unescaped representation.</summary>
/// <param name="stringToUnescape">The string to unescape.</param>
/// <returns>The unescaped representation of <paramref name="stringToUnescape"/>.</returns>
public static string UnescapeDataString(string stringToUnescape)
{
ArgumentNullException.ThrowIfNull(stringToUnescape);

if (stringToUnescape.Length == 0)
return string.Empty;
return UnescapeDataString(stringToUnescape, stringToUnescape);
}

/// <summary>Converts a span to its unescaped representation.</summary>
/// <param name="charsToUnescape">The span to unescape.</param>
/// <returns>The unescaped representation of <paramref name="charsToUnescape"/>.</returns>
public static string UnescapeDataString(ReadOnlySpan<char> charsToUnescape)
{
return UnescapeDataString(charsToUnescape, backingString: null);
}

private static string UnescapeDataString(ReadOnlySpan<char> charsToUnescape, string? backingString = null)
{
Debug.Assert(backingString is null || backingString.Length == charsToUnescape.Length);

int indexOfFirstToUnescape = charsToUnescape.IndexOf('%');
if (indexOfFirstToUnescape < 0)
{
// Nothing to unescape, just return the original value.
return backingString ?? charsToUnescape.ToString();
}

var vsb = new ValueStringBuilder(stackalloc char[StackallocThreshold]);

// This may throw for very large inputs. ¯\_(ツ)_/¯
Comment thread
MihaZupan marked this conversation as resolved.
Resolved
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);

string result = string.Concat(charsToUnescape.Slice(0, indexOfFirstToUnescape), vsb.AsSpan());
vsb.Dispose();
return result;
}

/// <summary>Attempts to convert a span to its unescaped representation.</summary>
/// <param name="charsToUnescape">The span to unescape.</param>
/// <param name="destination">The output span that contains the unescaped result of the operation.</param>
/// <param name="charsWritten">When this method returns, contains the number of chars that were written into <paramref name="destination"/>.</param>
/// <returns><see langword="true"/> if the <paramref name="destination"/> was large enough to hold the entire result; otherwise, <see langword="false"/>.</returns>
public static bool TryUnescapeDataString(ReadOnlySpan<char> charsToUnescape, Span<char> destination, out int charsWritten)
{
int indexOfFirstToUnescape = charsToUnescape.IndexOf('%');
if (indexOfFirstToUnescape < 0)
{
// Nothing to unescape, just copy the original chars.
if (charsToUnescape.TryCopyTo(destination))
{
charsWritten = charsToUnescape.Length;
return true;
}

int position = stringToUnescape.IndexOf('%');
if (position == -1)
return stringToUnescape;
charsWritten = 0;
return false;
}

var vsb = new ValueStringBuilder(stackalloc char[StackallocThreshold]);
Comment thread
MihaZupan marked this conversation as resolved.
Resolved
vsb.EnsureCapacity(stringToUnescape.Length);

vsb.Append(stringToUnescape.AsSpan(0, position));
// This may throw for very large inputs. ¯\_(ツ)_/¯
vsb.EnsureCapacity(charsToUnescape.Length - indexOfFirstToUnescape);

UriHelper.UnescapeString(
stringToUnescape, position, stringToUnescape.Length, ref vsb,
charsToUnescape.Slice(indexOfFirstToUnescape), ref vsb,
c_DummyChar, c_DummyChar, c_DummyChar,
UnescapeMode.Unescape | UnescapeMode.UnescapeAll,
syntax: null, isQuery: false);

return vsb.ToString();
int newLength = indexOfFirstToUnescape + vsb.Length;
Debug.Assert(newLength <= charsToUnescape.Length);

if (destination.Length >= newLength)
{
charsToUnescape.Slice(0, indexOfFirstToUnescape).CopyTo(destination);
vsb.AsSpan().CopyTo(destination.Slice(indexOfFirstToUnescape));

vsb.Dispose();
charsWritten = newLength;
return true;
}

vsb.Dispose();
charsWritten = 0;
return false;
}

// Where stringToEscape is intended to be a completely unescaped URI string.
Expand All @@ -584,9 +655,27 @@ public static string EscapeUriString(string stringToEscape) =>

// Where stringToEscape is intended to be URI data, but not an entire URI.
// This method will escape any character that is not an unreserved character, including percent signs.

/// <summary>Converts a string to its escaped representation.</summary>
/// <param name="stringToEscape">The string to escape.</param>
/// <returns>The escaped representation of <paramref name="stringToEscape"/>.</returns>
public static string EscapeDataString(string stringToEscape) =>
UriHelper.EscapeString(stringToEscape, checkExistingEscaped: false, UriHelper.Unreserved);

/// <summary>Converts a span to its escaped representation.</summary>
/// <param name="charsToEscape">The span to escape.</param>
/// <returns>The escaped representation of <paramref name="charsToEscape"/>.</returns>
public static string EscapeDataString(ReadOnlySpan<char> charsToEscape) =>
UriHelper.EscapeString(charsToEscape, checkExistingEscaped: false, UriHelper.Unreserved, backingString: null);

/// <summary>Attempts to convert a span to its escaped representation.</summary>
/// <param name="charsToEscape">The span to escape.</param>
/// <param name="destination">The output span that contains the escaped result of the operation.</param>
/// <param name="charsWritten">When this method returns, contains the number of chars that were written into <paramref name="destination"/>.</param>
/// <returns><see langword="true"/> if the <paramref name="destination"/> was large enough to hold the entire result; otherwise, <see langword="false"/>.</returns>
public static bool TryEscapeDataString(ReadOnlySpan<char> charsToEscape, Span<char> destination, out int charsWritten) =>
UriHelper.TryEscapeDataString(charsToEscape, destination, out charsWritten);

//
// Cleans up the specified component according to Iri rules
// a) Chars allowed by iri in a component are unescaped if found escaped
Expand Down
73 changes: 63 additions & 10 deletions src/libraries/System.Private.Uri/src/System/UriHelper.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Licensed to the .NET Foundation under one or more agreements.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Buffers;
Expand Down Expand Up @@ -118,26 +118,79 @@ internal static unsafe bool TestForSubPath(char* selfPtr, int selfLength, char*
return true;
}

internal static string EscapeString(string stringToEscape, bool checkExistingEscaped, SearchValues<char> noEscape)
public static bool TryEscapeDataString(ReadOnlySpan<char> charsToEscape, Span<char> destination, out int charsWritten)
{
if (destination.Length < charsToEscape.Length)
{
charsWritten = 0;
return false;
}

int indexOfFirstToEscape = charsToEscape.IndexOfAnyExcept(Unreserved);
if (indexOfFirstToEscape < 0)
{
// Nothing to escape, just copy the original chars.
charsToEscape.CopyTo(destination);
charsWritten = charsToEscape.Length;
return true;
}

var vsb = new ValueStringBuilder(stackalloc char[Uri.StackallocThreshold]);

// This may throw for very large inputs. ¯\_(ツ)_/¯
vsb.EnsureCapacity(charsToEscape.Length - indexOfFirstToEscape);
Comment thread
MihaZupan marked this conversation as resolved.
Resolved

EscapeStringToBuilder(charsToEscape.Slice(indexOfFirstToEscape), ref vsb, Unreserved, checkExistingEscaped: false);

int newLength = checked(indexOfFirstToEscape + vsb.Length);
Debug.Assert(newLength > charsToEscape.Length);

if (destination.Length >= newLength)
{
charsToEscape.Slice(0, indexOfFirstToEscape).CopyTo(destination);
vsb.AsSpan().CopyTo(destination.Slice(indexOfFirstToEscape));

vsb.Dispose();
charsWritten = newLength;
return true;
}

vsb.Dispose();
charsWritten = 0;
return false;
}

public static string EscapeString(string stringToEscape, bool checkExistingEscaped, SearchValues<char> noEscape)
{
ArgumentNullException.ThrowIfNull(stringToEscape);

return EscapeString(stringToEscape, checkExistingEscaped, noEscape, stringToEscape);
}

public static string EscapeString(ReadOnlySpan<char> charsToEscape, bool checkExistingEscaped, SearchValues<char> noEscape, string? backingString)
{
Debug.Assert(!noEscape.Contains('%'), "Need to treat % specially; it should be part of any escaped set");
Debug.Assert(backingString is null || backingString.Length == charsToEscape.Length);

int indexOfFirstToEscape = stringToEscape.AsSpan().IndexOfAnyExcept(noEscape);
int indexOfFirstToEscape = charsToEscape.IndexOfAnyExcept(noEscape);
if (indexOfFirstToEscape < 0)
{
// Nothing to escape, just return the original string.
return stringToEscape;
// Nothing to escape, just return the original value.
return backingString ?? charsToEscape.ToString();
}

// Otherwise, create a ValueStringBuilder to store the escaped data into,
// append to it all of the noEscape chars we already iterated through,
// escape the rest, and return the result as a string.
// escape the rest, and concat the result with the characters we skipped above.
var vsb = new ValueStringBuilder(stackalloc char[Uri.StackallocThreshold]);
vsb.Append(stringToEscape.AsSpan(0, indexOfFirstToEscape));
EscapeStringToBuilder(stringToEscape.AsSpan(indexOfFirstToEscape), ref vsb, noEscape, checkExistingEscaped);
return vsb.ToString();

// This may throw for very large inputs. ¯\_(ツ)_/¯
vsb.EnsureCapacity(charsToEscape.Length - indexOfFirstToEscape);

EscapeStringToBuilder(charsToEscape.Slice(indexOfFirstToEscape), ref vsb, noEscape, checkExistingEscaped);

string result = string.Concat(charsToEscape.Slice(0, indexOfFirstToEscape), vsb.AsSpan());
vsb.Dispose();
return result;
}

internal static unsafe void EscapeString(scoped ReadOnlySpan<char> stringToEscape, ref ValueStringBuilder dest,
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
<Compile Include="IriRelativeFileResolutionTest.cs" />
<Compile Include="IriTest.cs" />
<Compile Include="UncTest.cs" />
<Compile Include="PercentEncodingHelperTests.cs" />
<Compile Include="UriBuilderParameterTest.cs" />
<Compile Include="UriBuilderRefreshTest.cs" />
<Compile Include="UriBuilderTests.cs" />
Expand Down
Loading