diff --git a/src/libraries/System.Web.HttpUtility/src/System.Web.HttpUtility.csproj b/src/libraries/System.Web.HttpUtility/src/System.Web.HttpUtility.csproj index b7c44506200dd3..5da18b0fae6c82 100644 --- a/src/libraries/System.Web.HttpUtility/src/System.Web.HttpUtility.csproj +++ b/src/libraries/System.Web.HttpUtility/src/System.Web.HttpUtility.csproj @@ -15,6 +15,10 @@ + + diff --git a/src/libraries/System.Web.HttpUtility/src/System/Web/HttpUtility.cs b/src/libraries/System.Web.HttpUtility/src/System/Web/HttpUtility.cs index 58dfcd39a9bdf0..270aedb275c946 100644 --- a/src/libraries/System.Web.HttpUtility/src/System/Web/HttpUtility.cs +++ b/src/libraries/System.Web.HttpUtility/src/System/Web/HttpUtility.cs @@ -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); } } diff --git a/src/libraries/System.Web.HttpUtility/src/System/Web/Util/HttpEncoder.cs b/src/libraries/System.Web.HttpUtility/src/System/Web/Util/HttpEncoder.cs index 3c96060bd20a92..690645e0759147 100644 --- a/src/libraries/System.Web.HttpUtility/src/System/Web/Util/HttpEncoder.cs +++ b/src/libraries/System.Web.HttpUtility/src/System/Web/Util/HttpEncoder.cs @@ -20,22 +20,13 @@ internal static class HttpEncoder private static readonly SearchValues 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 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) @@ -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 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 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))] diff --git a/src/libraries/System.Web.HttpUtility/tests/HttpUtility/HttpUtilityTest.cs b/src/libraries/System.Web.HttpUtility/tests/HttpUtility/HttpUtilityTest.cs index b95c663e932d4b..27942d50051001 100644 --- a/src/libraries/System.Web.HttpUtility/tests/HttpUtility/HttpUtilityTest.cs +++ b/src/libraries/System.Web.HttpUtility/tests/HttpUtility/HttpUtilityTest.cs @@ -310,6 +310,7 @@ public static IEnumerable 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)