Skip to content

Commit e730d83

Browse files
Optimize HttpUtility.UrlEncodeToBytes for (string, Encoding) overload. (#102805)
* Optimize HttpUtility.UrlEncodeToBytes for (string, Encoding) overload. * feedback * use SearchValues * Remove IsUrlSafeChar * Split UrlEncode to potentially inlinable function * Address feedback * remove fallback
1 parent b23dea6 commit e730d83

File tree

3 files changed

+64
-85
lines changed

3 files changed

+64
-85
lines changed

src/libraries/System.Web.HttpUtility/src/System/Web/HttpUtility.cs

+3-12
Original file line numberDiff line numberDiff line change
@@ -213,22 +213,13 @@ public static NameValueCollection ParseQueryString(string query, Encoding encodi
213213
}
214214

215215
[return: NotNullIfNotNull(nameof(bytes))]
216-
public static byte[]? UrlDecodeToBytes(byte[]? bytes) => bytes == null ? null : HttpEncoder.UrlDecode(bytes.AsSpan(0, bytes.Length));
216+
public static byte[]? UrlDecodeToBytes(byte[]? bytes) => bytes == null ? null : HttpEncoder.UrlDecode(bytes);
217217

218218
[return: NotNullIfNotNull(nameof(str))]
219-
public static byte[]? UrlEncodeToBytes(string? str, Encoding e)
220-
{
221-
if (str == null)
222-
{
223-
return null;
224-
}
225-
226-
byte[] bytes = e.GetBytes(str);
227-
return HttpEncoder.UrlEncode(bytes, 0, bytes.Length, alwaysCreateNewReturnValue: false);
228-
}
219+
public static byte[]? UrlEncodeToBytes(string? str, Encoding e) => str == null ? null : HttpEncoder.UrlEncode(str, e);
229220

230221
[return: NotNullIfNotNull(nameof(bytes))]
231-
public static byte[]? UrlEncodeToBytes(byte[]? bytes, int offset, int count) => HttpEncoder.UrlEncode(bytes, offset, count, alwaysCreateNewReturnValue: true);
222+
public static byte[]? UrlEncodeToBytes(byte[]? bytes, int offset, int count) => HttpEncoder.UrlEncode(bytes, offset, count);
232223

233224
[Obsolete("This method produces non-standards-compliant output and has interoperability issues. The preferred alternative is UrlEncode(String).")]
234225
[return: NotNullIfNotNull(nameof(str))]

src/libraries/System.Web.HttpUtility/src/System/Web/Util/HttpEncoder.cs

+61-50
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Buffers;
45
using System.Diagnostics;
56
using System.Diagnostics.CodeAnalysis;
67
using System.Globalization;
@@ -13,6 +14,12 @@ namespace System.Web.Util
1314
internal static class HttpEncoder
1415
{
1516
private const int MaxStackAllocUrlLength = 256;
17+
private const int StackallocThreshold = 512;
18+
19+
// Set of safe chars, from RFC 1738.4 minus '+'
20+
private static readonly SearchValues<byte> s_urlSafeBytes = SearchValues.Create(
21+
"!()*-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz"u8);
22+
1623
private static void AppendCharAsUnicodeJavaScript(StringBuilder builder, char c)
1724
{
1825
builder.Append($"\\u{(int)c:x4}");
@@ -218,8 +225,6 @@ internal static string JavaScriptStringEncode(string? value)
218225

219226
internal static byte[] UrlDecode(ReadOnlySpan<byte> bytes)
220227
{
221-
const int StackallocThreshold = 512;
222-
223228
int decodedBytesCount = 0;
224229
int count = bytes.Length;
225230
Span<byte> decodedBytes = count <= StackallocThreshold ? stackalloc byte[StackallocThreshold] : new byte[count];
@@ -401,71 +406,40 @@ internal static string UrlDecode(ReadOnlySpan<char> value, Encoding encoding)
401406
}
402407

403408
[return: NotNullIfNotNull(nameof(bytes))]
404-
internal static byte[]? UrlEncode(byte[]? bytes, int offset, int count, bool alwaysCreateNewReturnValue)
405-
{
406-
byte[]? encoded = UrlEncode(bytes, offset, count);
407-
408-
return (alwaysCreateNewReturnValue && (encoded != null) && (encoded == bytes))
409-
? (byte[])encoded.Clone()
410-
: encoded;
411-
}
412-
413-
[return: NotNullIfNotNull(nameof(bytes))]
414-
private static byte[]? UrlEncode(byte[]? bytes, int offset, int count)
409+
internal static byte[]? UrlEncode(byte[]? bytes, int offset, int count)
415410
{
416411
if (!ValidateUrlEncodingParameters(bytes, offset, count))
417412
{
418413
return null;
419414
}
420415

421-
int cSpaces = 0;
422-
int cUnsafe = 0;
423-
424-
// count them first
425-
for (int i = 0; i < count; i++)
426-
{
427-
char ch = (char)bytes[offset + i];
428-
429-
if (ch == ' ')
430-
{
431-
cSpaces++;
432-
}
433-
else if (!HttpEncoderUtility.IsUrlSafeChar(ch))
434-
{
435-
cUnsafe++;
436-
}
437-
}
416+
return UrlEncode(bytes.AsSpan(offset, count));
417+
}
438418

419+
private static byte[] UrlEncode(ReadOnlySpan<byte> bytes)
420+
{
439421
// nothing to expand?
440-
if (cSpaces == 0 && cUnsafe == 0)
422+
if (!NeedsEncoding(bytes, out int cUnsafe))
441423
{
442-
// DevDiv 912606: respect "offset" and "count"
443-
if (0 == offset && bytes.Length == count)
444-
{
445-
return bytes;
446-
}
447-
else
448-
{
449-
byte[] subarray = new byte[count];
450-
Buffer.BlockCopy(bytes, offset, subarray, 0, count);
451-
return subarray;
452-
}
424+
return bytes.ToArray();
453425
}
454426

427+
return UrlEncode(bytes, cUnsafe);
428+
}
429+
430+
private static byte[] UrlEncode(ReadOnlySpan<byte> bytes, int cUnsafe)
431+
{
455432
// expand not 'safe' characters into %XX, spaces to +s
456-
byte[] expandedBytes = new byte[count + cUnsafe * 2];
433+
byte[] expandedBytes = new byte[bytes.Length + cUnsafe * 2];
457434
int pos = 0;
458435

459-
for (int i = 0; i < count; i++)
436+
foreach (byte b in bytes)
460437
{
461-
byte b = bytes[offset + i];
462-
char ch = (char)b;
463-
464-
if (HttpEncoderUtility.IsUrlSafeChar(ch))
438+
if (s_urlSafeBytes.Contains(b))
465439
{
466440
expandedBytes[pos++] = b;
467441
}
468-
else if (ch == ' ')
442+
else if (b == ' ')
469443
{
470444
expandedBytes[pos++] = (byte)'+';
471445
}
@@ -480,6 +454,43 @@ internal static string UrlDecode(ReadOnlySpan<char> value, Encoding encoding)
480454
return expandedBytes;
481455
}
482456

457+
private static bool NeedsEncoding(ReadOnlySpan<byte> bytes, out int cUnsafe)
458+
{
459+
cUnsafe = 0;
460+
461+
int i = bytes.IndexOfAnyExcept(s_urlSafeBytes);
462+
if (i < 0)
463+
{
464+
return false;
465+
}
466+
467+
foreach (byte b in bytes.Slice(i))
468+
{
469+
if (!s_urlSafeBytes.Contains(b) && b != ' ')
470+
{
471+
cUnsafe++;
472+
}
473+
}
474+
475+
return true;
476+
}
477+
478+
internal static byte[] UrlEncode(string str, Encoding e)
479+
{
480+
if (e.GetMaxByteCount(str.Length) <= StackallocThreshold)
481+
{
482+
Span<byte> byteSpan = stackalloc byte[StackallocThreshold];
483+
int encodedBytes = e.GetBytes(str, byteSpan);
484+
485+
return UrlEncode(byteSpan.Slice(0, encodedBytes));
486+
}
487+
488+
byte[] bytes = e.GetBytes(str);
489+
return NeedsEncoding(bytes, out int cUnsafe)
490+
? UrlEncode(bytes, cUnsafe)
491+
: bytes;
492+
}
493+
483494
// Helper to encode the non-ASCII url characters only
484495
private static string UrlEncodeNonAscii(string str, Encoding e)
485496
{
@@ -550,7 +561,7 @@ private static byte[] UrlEncodeNonAscii(byte[] bytes, int offset, int count)
550561

551562
if ((ch & 0xff80) == 0)
552563
{ // 7 bit?
553-
if (HttpEncoderUtility.IsUrlSafeChar(ch))
564+
if (s_urlSafeBytes.Contains((byte)ch))
554565
{
555566
sb.Append(ch);
556567
}

src/libraries/System.Web.HttpUtility/src/System/Web/Util/HttpEncoderUtility.cs

-23
Original file line numberDiff line numberDiff line change
@@ -8,29 +8,6 @@ namespace System.Web.Util
88
{
99
internal static class HttpEncoderUtility
1010
{
11-
// Set of safe chars, from RFC 1738.4 minus '+'
12-
public static bool IsUrlSafeChar(char ch)
13-
{
14-
if (char.IsAsciiLetterOrDigit(ch))
15-
{
16-
return true;
17-
}
18-
19-
switch (ch)
20-
{
21-
case '-':
22-
case '_':
23-
case '.':
24-
case '!':
25-
case '*':
26-
case '(':
27-
case ')':
28-
return true;
29-
}
30-
31-
return false;
32-
}
33-
3411
// Helper to encode spaces only
3512
[return: NotNullIfNotNull(nameof(str))]
3613
internal static string? UrlEncodeSpaces(string? str) => str != null && str.Contains(' ') ? str.Replace(" ", "%20") : str;

0 commit comments

Comments
 (0)