Spanified Webencoders.Base64UrlEncode (#11047)

This commit is contained in:
Günther Foidl 2019-06-11 03:18:32 +02:00 committed by Brennan
parent 4d1262c39b
commit 90ab2cb965
3 changed files with 80 additions and 4 deletions

View File

@ -233,6 +233,7 @@ namespace Microsoft.AspNetCore.WebUtilities
public static string Base64UrlEncode(byte[] 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 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(byte[] input, int offset, int count) { throw null; }
public static string Base64UrlEncode(System.ReadOnlySpan<byte> input) { throw null; }
public static int GetArraySizeRequiredToDecode(int count) { throw null; } public static int GetArraySizeRequiredToDecode(int count) { throw null; }
public static int GetArraySizeRequiredToEncode(int count) { throw null; } public static int GetArraySizeRequiredToEncode(int count) { throw null; }
} }

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Buffers;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization; using System.Globalization;
using Microsoft.Extensions.WebEncoders.Sources; using Microsoft.Extensions.WebEncoders.Sources;
@ -220,6 +221,9 @@ namespace Microsoft.Extensions.Internal
ValidateParameters(input.Length, nameof(input), offset, count); ValidateParameters(input.Length, nameof(input), offset, count);
#if NETCOREAPP3_0
return Base64UrlEncode(input.AsSpan(offset, count));
#else
// Special-case empty input // Special-case empty input
if (count == 0) if (count == 0)
{ {
@ -229,7 +233,8 @@ namespace Microsoft.Extensions.Internal
var buffer = new char[GetArraySizeRequiredToEncode(count)]; var buffer = new char[GetArraySizeRequiredToEncode(count)];
var numBase64Chars = Base64UrlEncode(input, offset, buffer, outputOffset: 0, count: count); var numBase64Chars = Base64UrlEncode(input, offset, buffer, outputOffset: 0, count: count);
return new String(buffer, startIndex: 0, length: numBase64Chars); return new string(buffer, startIndex: 0, length: numBase64Chars);
#endif
} }
/// <summary> /// <summary>
@ -280,6 +285,9 @@ namespace Microsoft.Extensions.Internal
nameof(count)); nameof(count));
} }
#if NETCOREAPP3_0
return Base64UrlEncode(input.AsSpan(offset, count), output.AsSpan(outputOffset));
#else
// Special-case empty input. // Special-case empty input.
if (count == 0) if (count == 0)
{ {
@ -311,6 +319,7 @@ namespace Microsoft.Extensions.Internal
} }
return numBase64Chars; return numBase64Chars;
#endif
} }
/// <summary> /// <summary>
@ -327,6 +336,73 @@ namespace Microsoft.Extensions.Internal
return checked(numWholeOrPartialInputBlocks * 4); return checked(numWholeOrPartialInputBlocks * 4);
} }
#if NETCOREAPP3_0
/// <summary>
/// Encodes <paramref name="input"/> using base64url encoding.
/// </summary>
/// <param name="input">The binary input to encode.</param>
/// <returns>The base64url-encoded form of <paramref name="input"/>.</returns>
public static string Base64UrlEncode(ReadOnlySpan<byte> input)
{
if (input.IsEmpty)
{
return string.Empty;
}
int bufferSize = GetArraySizeRequiredToEncode(input.Length);
char[] bufferToReturnToPool = null;
Span<char> buffer = bufferSize <= 128
? stackalloc char[bufferSize]
: bufferToReturnToPool = ArrayPool<char>.Shared.Rent(bufferSize);
var numBase64Chars = Base64UrlEncode(input, buffer);
var base64Url = new string(buffer.Slice(0, numBase64Chars));
if (bufferToReturnToPool != null)
{
ArrayPool<char>.Shared.Return(bufferToReturnToPool);
}
return base64Url;
}
private static int Base64UrlEncode(ReadOnlySpan<byte> input, Span<char> 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.
Convert.TryToBase64Chars(input, output, out int charsWritten);
// Fix up '+' -> '-' and '/' -> '_'. Drop padding characters.
for (var i = 0; i < charsWritten; 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;
}
}
return charsWritten;
}
#endif
private static int GetNumBase64PaddingCharsInString(string str) private static int GetNumBase64PaddingCharsInString(string str)
{ {
// Assumption: input contains a well-formed base64 string with no whitespace. // Assumption: input contains a well-formed base64 string with no whitespace.

View File

@ -105,11 +105,10 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
private static string MakeNewConnectionId() 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 // 128 bit buffer / 8 bits per byte = 16 bytes
var buffer = new byte[16]; Span<byte> buffer = stackalloc byte[16];
_keyGenerator.GetBytes(buffer);
// Generate the id with RNGCrypto because we want a cryptographically random id, which GUID is not // Generate the id with RNGCrypto because we want a cryptographically random id, which GUID is not
_keyGenerator.GetBytes(buffer);
return WebEncoders.Base64UrlEncode(buffer); return WebEncoders.Base64UrlEncode(buffer);
} }