From 249b565e3ab69361e5bb29bf1b966a57162d1c17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther=20Foidl?= Date: Mon, 10 Jun 2019 19:24:51 +0200 Subject: [PATCH 1/5] Spanified WebEncoders.Base64UrlEncode with string return --- src/Shared/WebEncoders/WebEncoders.cs | 89 +++++++++++++++++---------- 1 file changed, 58 insertions(+), 31 deletions(-) diff --git a/src/Shared/WebEncoders/WebEncoders.cs b/src/Shared/WebEncoders/WebEncoders.cs index 17068ae67a55..1dc8fe21c758 100644 --- a/src/Shared/WebEncoders/WebEncoders.cs +++ b/src/Shared/WebEncoders/WebEncoders.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Buffers; using System.Diagnostics; using System.Globalization; using Microsoft.Extensions.WebEncoders.Sources; @@ -189,6 +190,36 @@ public static int GetArraySizeRequiredToDecode(int count) return checked(count + numPaddingCharsToAdd); } + /// + /// Encodes using base64url encoding. + /// + /// The binary input to encode. + /// The base64url-encoded form of . + public static string Base64UrlEncode(ReadOnlySpan input) + { + if (input.IsEmpty) + { + return string.Empty; + } + + int bufferSize = GetArraySizeRequiredToEncode(input.Length); + + char[] bufferToReturnToPool = null; + Span buffer = bufferSize <= 128 + ? stackalloc char[bufferSize] + : bufferToReturnToPool = ArrayPool.Shared.Rent(bufferSize); + + var numBase64Chars = Base64UrlEncode(input, buffer); + var base64Url = new string(buffer.Slice(0, numBase64Chars)); + + if (bufferToReturnToPool != null) + { + ArrayPool.Shared.Return(bufferToReturnToPool); + } + + return base64Url; + } + /// /// Encodes using base64url encoding. /// @@ -220,16 +251,7 @@ public static string Base64UrlEncode(byte[] input, int offset, int count) ValidateParameters(input.Length, nameof(input), offset, count); - // Special-case empty input - if (count == 0) - { - return string.Empty; - } - - var buffer = new char[GetArraySizeRequiredToEncode(count)]; - var numBase64Chars = Base64UrlEncode(input, offset, buffer, outputOffset: 0, count: count); - - return new String(buffer, startIndex: 0, length: numBase64Chars); + return Base64UrlEncode(input.AsSpan(offset, count)); } /// @@ -280,19 +302,38 @@ public static int Base64UrlEncode(byte[] input, int offset, char[] output, int o nameof(count)); } - // Special-case empty input. - if (count == 0) + return Base64UrlEncode(input.AsSpan(offset, count), output.AsSpan(outputOffset)); + } + + /// + /// Get the minimum output char[] size required for encoding + /// s with the method. + /// + /// The number of characters to encode. + /// + /// The minimum output char[] size required for encoding s. + /// + public static int GetArraySizeRequiredToEncode(int count) + { + var numWholeOrPartialInputBlocks = checked(count + 2) / 3; + return checked(numWholeOrPartialInputBlocks * 4); + } + + private static int Base64UrlEncode(ReadOnlySpan input, Span output) + { + Debug.Assert(output.Length >= GetArraySizeRequiredToEncode(input.Length)); + + if (input.IsEmpty) { return 0; } // Use base64url encoding with no padding characters. See RFC 4648, Sec. 5. - // Start with default Base64 encoding. - var numBase64Chars = Convert.ToBase64CharArray(input, offset, count, output, outputOffset); + Convert.TryToBase64Chars(input, output, out int charsWritten); // Fix up '+' -> '-' and '/' -> '_'. Drop padding characters. - for (var i = outputOffset; i - outputOffset < numBase64Chars; i++) + for (var i = 0; i < charsWritten; i++) { var ch = output[i]; if (ch == '+') @@ -306,25 +347,11 @@ public static int Base64UrlEncode(byte[] input, int offset, char[] output, int o else if (ch == '=') { // We've reached a padding character; truncate the remainder. - return i - outputOffset; + return i; } } - return numBase64Chars; - } - - /// - /// Get the minimum output char[] size required for encoding - /// s with the method. - /// - /// The number of characters to encode. - /// - /// The minimum output char[] size required for encoding s. - /// - public static int GetArraySizeRequiredToEncode(int count) - { - var numWholeOrPartialInputBlocks = checked(count + 2) / 3; - return checked(numWholeOrPartialInputBlocks * 4); + return charsWritten; } private static int GetNumBase64PaddingCharsInString(string str) From 5001569dfcacf84aba710186b07b6b488beaf04e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther=20Foidl?= Date: Mon, 10 Jun 2019 19:04:06 +0200 Subject: [PATCH 2/5] HttpConnectionManager to use span based WebEncoders --- .../Http.Connections/src/Internal/HttpConnectionManager.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionManager.cs b/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionManager.cs index 283bee8d9880..206f352acc1e 100644 --- a/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionManager.cs +++ b/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionManager.cs @@ -105,11 +105,10 @@ public void RemoveConnection(string id) private static string MakeNewConnectionId() { - // TODO: Use Span when WebEncoders implements Span methods https://github.com/aspnet/Home/issues/2966 // 128 bit buffer / 8 bits per byte = 16 bytes - var buffer = new byte[16]; - _keyGenerator.GetBytes(buffer); + Span buffer = stackalloc byte[16]; // Generate the id with RNGCrypto because we want a cryptographically random id, which GUID is not + _keyGenerator.GetBytes(buffer); return WebEncoders.Base64UrlEncode(buffer); } From c6781484c8739e0ec383b0714bac515db81ad19a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther=20Foidl?= Date: Mon, 10 Jun 2019 21:28:58 +0200 Subject: [PATCH 3/5] Update ref-Assembly --- .../ref/Microsoft.AspNetCore.WebUtilities.netcoreapp3.0.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Http/WebUtilities/ref/Microsoft.AspNetCore.WebUtilities.netcoreapp3.0.cs b/src/Http/WebUtilities/ref/Microsoft.AspNetCore.WebUtilities.netcoreapp3.0.cs index afdad71011f3..f5ce56cab3c9 100644 --- a/src/Http/WebUtilities/ref/Microsoft.AspNetCore.WebUtilities.netcoreapp3.0.cs +++ b/src/Http/WebUtilities/ref/Microsoft.AspNetCore.WebUtilities.netcoreapp3.0.cs @@ -230,6 +230,7 @@ public static partial class WebEncoders public static byte[] Base64UrlDecode(string input) { throw null; } public static byte[] Base64UrlDecode(string input, int offset, char[] buffer, int bufferOffset, int count) { throw null; } public static byte[] Base64UrlDecode(string input, int offset, int count) { throw null; } + public static string Base64UrlEncode(System.ReadOnlySpan input) { throw null; } public static string Base64UrlEncode(byte[] input) { throw null; } public static int Base64UrlEncode(byte[] input, int offset, char[] output, int outputOffset, int count) { throw null; } public static string Base64UrlEncode(byte[] input, int offset, int count) { throw null; } From 64db801439f1438848e33f7f811f6f09ce24e3d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther=20Foidl?= Date: Mon, 10 Jun 2019 21:41:25 +0200 Subject: [PATCH 4/5] Multi-targeting for netcoreapp3.0 and net461 to fix build-failures --- src/Shared/WebEncoders/WebEncoders.cs | 109 +++++++++++++++++++------- 1 file changed, 79 insertions(+), 30 deletions(-) diff --git a/src/Shared/WebEncoders/WebEncoders.cs b/src/Shared/WebEncoders/WebEncoders.cs index 1dc8fe21c758..979ab5d2d7f5 100644 --- a/src/Shared/WebEncoders/WebEncoders.cs +++ b/src/Shared/WebEncoders/WebEncoders.cs @@ -190,36 +190,6 @@ public static int GetArraySizeRequiredToDecode(int count) return checked(count + numPaddingCharsToAdd); } - /// - /// Encodes using base64url encoding. - /// - /// The binary input to encode. - /// The base64url-encoded form of . - public static string Base64UrlEncode(ReadOnlySpan input) - { - if (input.IsEmpty) - { - return string.Empty; - } - - int bufferSize = GetArraySizeRequiredToEncode(input.Length); - - char[] bufferToReturnToPool = null; - Span buffer = bufferSize <= 128 - ? stackalloc char[bufferSize] - : bufferToReturnToPool = ArrayPool.Shared.Rent(bufferSize); - - var numBase64Chars = Base64UrlEncode(input, buffer); - var base64Url = new string(buffer.Slice(0, numBase64Chars)); - - if (bufferToReturnToPool != null) - { - ArrayPool.Shared.Return(bufferToReturnToPool); - } - - return base64Url; - } - /// /// Encodes using base64url encoding. /// @@ -251,7 +221,20 @@ public static string Base64UrlEncode(byte[] input, int offset, int count) ValidateParameters(input.Length, nameof(input), offset, count); +#if NETCOREAPP3_0 return Base64UrlEncode(input.AsSpan(offset, count)); +#else + // Special-case empty input + if (count == 0) + { + return string.Empty; + } + + var buffer = new char[GetArraySizeRequiredToEncode(count)]; + var numBase64Chars = Base64UrlEncode(input, offset, buffer, outputOffset: 0, count: count); + + return new string(buffer, startIndex: 0, length: numBase64Chars); +#endif } /// @@ -302,7 +285,41 @@ public static int Base64UrlEncode(byte[] input, int offset, char[] output, int o nameof(count)); } +#if NETCOREAPP3_0 return Base64UrlEncode(input.AsSpan(offset, count), output.AsSpan(outputOffset)); +#else + // Special-case empty input. + if (count == 0) + { + return 0; + } + + // Use base64url encoding with no padding characters. See RFC 4648, Sec. 5. + + // Start with default Base64 encoding. + var numBase64Chars = Convert.ToBase64CharArray(input, offset, count, output, outputOffset); + + // Fix up '+' -> '-' and '/' -> '_'. Drop padding characters. + for (var i = outputOffset; i - outputOffset < numBase64Chars; i++) + { + var ch = output[i]; + if (ch == '+') + { + output[i] = '-'; + } + else if (ch == '/') + { + output[i] = '_'; + } + else if (ch == '=') + { + // We've reached a padding character; truncate the remainder. + return i - outputOffset; + } + } + + return numBase64Chars; +#endif } /// @@ -319,6 +336,37 @@ public static int GetArraySizeRequiredToEncode(int count) return checked(numWholeOrPartialInputBlocks * 4); } +#if NETCOREAPP3_0 + /// + /// Encodes using base64url encoding. + /// + /// The binary input to encode. + /// The base64url-encoded form of . + public static string Base64UrlEncode(ReadOnlySpan input) + { + if (input.IsEmpty) + { + return string.Empty; + } + + int bufferSize = GetArraySizeRequiredToEncode(input.Length); + + char[] bufferToReturnToPool = null; + Span buffer = bufferSize <= 128 + ? stackalloc char[bufferSize] + : bufferToReturnToPool = ArrayPool.Shared.Rent(bufferSize); + + var numBase64Chars = Base64UrlEncode(input, buffer); + var base64Url = new string(buffer.Slice(0, numBase64Chars)); + + if (bufferToReturnToPool != null) + { + ArrayPool.Shared.Return(bufferToReturnToPool); + } + + return base64Url; + } + private static int Base64UrlEncode(ReadOnlySpan input, Span output) { Debug.Assert(output.Length >= GetArraySizeRequiredToEncode(input.Length)); @@ -353,6 +401,7 @@ private static int Base64UrlEncode(ReadOnlySpan input, Span output) return charsWritten; } +#endif private static int GetNumBase64PaddingCharsInString(string str) { From 92f1076afe6d9867859cf064a1482c7e350fb870 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther=20Foidl?= Date: Mon, 10 Jun 2019 23:34:48 +0200 Subject: [PATCH 5/5] Update ref-Assembly --- .../ref/Microsoft.AspNetCore.WebUtilities.netcoreapp3.0.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Http/WebUtilities/ref/Microsoft.AspNetCore.WebUtilities.netcoreapp3.0.cs b/src/Http/WebUtilities/ref/Microsoft.AspNetCore.WebUtilities.netcoreapp3.0.cs index f5ce56cab3c9..3c502d7d4b4f 100644 --- a/src/Http/WebUtilities/ref/Microsoft.AspNetCore.WebUtilities.netcoreapp3.0.cs +++ b/src/Http/WebUtilities/ref/Microsoft.AspNetCore.WebUtilities.netcoreapp3.0.cs @@ -230,10 +230,10 @@ public static partial class WebEncoders public static byte[] Base64UrlDecode(string input) { throw null; } public static byte[] Base64UrlDecode(string input, int offset, char[] buffer, int bufferOffset, int count) { throw null; } public static byte[] Base64UrlDecode(string input, int offset, int count) { throw null; } - public static string Base64UrlEncode(System.ReadOnlySpan input) { throw null; } public static string Base64UrlEncode(byte[] input) { throw null; } public static int Base64UrlEncode(byte[] input, int offset, char[] output, int outputOffset, int count) { throw null; } public static string Base64UrlEncode(byte[] input, int offset, int count) { throw null; } + public static string Base64UrlEncode(System.ReadOnlySpan input) { throw null; } public static int GetArraySizeRequiredToDecode(int count) { throw null; } public static int GetArraySizeRequiredToEncode(int count) { throw null; } }