Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimize HttpUtility.JavaScriptStringEncode by using SearchValues #102917

Merged
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
<Compile Include="System\Web\Util\Utf16StringValidator.cs" />
<Compile Include="$(CommonPath)System\HexConverter.cs"
Link="Common\System\HexConverter.cs" />
<Compile Include="$(CommonPath)System\Text\ValueStringBuilder.cs"
Link="Common\System\Text\ValueStringBuilder.cs" />
<Compile Include="$(CommonPath)System\Text\ValueStringBuilder.AppendSpanFormattable.cs"
Link="Common\System\Text\ValueStringBuilder.AppendSpanFormattable.cs" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,12 +237,8 @@ public static NameValueCollection ParseQueryString(string query, Encoding encodi
[return: NotNullIfNotNull(nameof(bytes))]
public static byte[]? UrlDecodeToBytes(byte[]? bytes, int offset, int count) => HttpEncoder.UrlDecode(bytes, offset, count);

public static string JavaScriptStringEncode(string? value) => HttpEncoder.JavaScriptStringEncode(value);
public static string JavaScriptStringEncode(string? value) => HttpEncoder.JavaScriptStringEncode(value, false);

public static string JavaScriptStringEncode(string? value, bool addDoubleQuotes)
{
string encoded = HttpEncoder.JavaScriptStringEncode(value);
return addDoubleQuotes ? "\"" + encoded + "\"" : encoded;
}
public static string JavaScriptStringEncode(string? value, bool addDoubleQuotes) => HttpEncoder.JavaScriptStringEncode(value, addDoubleQuotes);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,13 @@ internal static class HttpEncoder
private static readonly SearchValues<byte> s_urlSafeBytes = SearchValues.Create(
"!()*-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz"u8);

private static void AppendCharAsUnicodeJavaScript(StringBuilder builder, char c)
{
builder.Append($"\\u{(int)c:x4}");
}

private static bool CharRequiresJavaScriptEncoding(char c) =>
c < 0x20 // control chars always have to be encoded
|| c == '\"' // chars which must be encoded per JSON spec
|| c == '\\'
|| c == '\'' // HTML-sensitive chars encoded for safety
|| c == '<'
|| c == '>'
|| (c == '&')
|| c == '\u0085' // newline chars (see Unicode 6.2, Table 5-1 [http://www.unicode.org/versions/Unicode6.2.0/ch05.pdf]) have to be encoded
|| c == '\u2028'
|| c == '\u2029';
private static readonly SearchValues<char> s_invalidJavaScriptChars = SearchValues.Create(
// Any Control, < 32 (' ')
"\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\u0008\u0009\u000A\u000B\u000C\u000D\u000E\u000F\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001A\u001B\u001C\u001D\u001E\u001F" +
// Chars which must be encoded per JSON spec / HTML-sensitive chars encoded for safety
"\"&'<>\\" +
// newline chars (see Unicode 6.2, Table 5-1 [http://www.unicode.org/versions/Unicode6.2.0/ch05.pdf]) have to be encoded
"\u0085\u2028\u2029");

[return: NotNullIfNotNull(nameof(value))]
internal static string? HtmlAttributeEncode(string? value)
Expand Down Expand Up @@ -137,79 +128,71 @@ private static int IndexOfHtmlAttributeEncodingChars(string s) =>

private static bool IsNonAsciiByte(byte b) => b >= 0x7F || b < 0x20;

internal static string JavaScriptStringEncode(string? value)
internal static string JavaScriptStringEncode(string? value, bool addDoubleQuotes)
{
if (string.IsNullOrEmpty(value))
int i = value.AsSpan().IndexOfAny(s_invalidJavaScriptChars);
if (i < 0)
{
return string.Empty;
return addDoubleQuotes ? $"\"{value}\"" : value ?? string.Empty;
}

StringBuilder? b = null;
int startIndex = 0;
int count = 0;
for (int i = 0; i < value.Length; i++)
{
char c = value[i];
return EncodeCore(value, i, addDoubleQuotes);

// Append the unhandled characters (that do not require special treament)
// to the string builder when special characters are detected.
if (CharRequiresJavaScriptEncoding(c))
static string EncodeCore(ReadOnlySpan<char> value, int i, bool addDoubleQuotes)
{
var vsb = new ValueStringBuilder(stackalloc char[StackallocThreshold]);
if (addDoubleQuotes)
{
b ??= new StringBuilder(value.Length + 5);

if (count > 0)
{
b.Append(value, startIndex, count);
}

startIndex = i + 1;
count = 0;
vsb.Append('"');
}

ReadOnlySpan<char> chars = value;
do
{
vsb.Append(chars.Slice(0, i));
char c = chars[i];
chars = chars.Slice(i + 1);
switch (c)
{
case '\r':
b.Append("\\r");
vsb.Append("\\r");
break;
case '\t':
b.Append("\\t");
vsb.Append("\\t");
break;
case '\"':
b.Append("\\\"");
vsb.Append("\\\"");
break;
case '\\':
b.Append("\\\\");
vsb.Append("\\\\");
break;
case '\n':
b.Append("\\n");
vsb.Append("\\n");
break;
case '\b':
b.Append("\\b");
vsb.Append("\\b");
break;
case '\f':
b.Append("\\f");
vsb.Append("\\f");
break;
default:
AppendCharAsUnicodeJavaScript(b, c);
vsb.Append("\\u");
vsb.AppendSpanFormattable((int)c, "x4");
break;
}
}
else

i = chars.IndexOfAny(s_invalidJavaScriptChars);
} while (i >= 0);

vsb.Append(chars);

if (addDoubleQuotes)
{
count++;
vsb.Append('"');
}
}

if (b == null)
{
return value;
}

if (count > 0)
{
b.Append(value, startIndex, count);
return vsb.ToString();
}

return b.ToString();
}

[return: NotNullIfNotNull(nameof(bytes))]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@ public static IEnumerable<object[]> JavaScriptStringEncodeData
yield return new object[] { "", "" };
yield return new object[] {"No escaping needed.", "No escaping needed."};
yield return new object[] {"The \t and \n will need to be escaped.", "The \\t and \\n will need to be escaped."};
yield return new object[] {"The \t and \n will need to be escaped.>", "The \\t and \\n will need to be escaped.\\u003e" };
for (char c = char.MinValue; c < TestMaxChar; c++)
{
if (c >= 0 && c <= 7 || c == 11 || c >= 14 && c <= 31 || c == 38 || c == 39 || c == 60 || c == 62 || c == 133 || c == 8232 || c == 8233)
Expand Down
Loading