diff --git a/Common.sln b/Common.sln index 9e34e08031a..7219dcc2edc 100644 --- a/Common.sln +++ b/Common.sln @@ -71,6 +71,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Internal.AspNetCore.Analyze EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Internal.AspNetCore.Analyzers.Tests", "test\Internal.AspNetCore.Analyzers.Tests\Internal.AspNetCore.Analyzers.Tests.csproj", "{1A579BD1-A4C4-4B1B-B092-D1670DF7F239}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Extensions.Internal.Benchmarks", "benchmarks\Microsoft.Extensions.Internal.Benchmarks\Microsoft.Extensions.Internal.Benchmarks.csproj", "{7EAB9B74-5E5E-4D93-BA1E-865906C50676}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -137,6 +139,10 @@ Global {1A579BD1-A4C4-4B1B-B092-D1670DF7F239}.Debug|Any CPU.Build.0 = Debug|Any CPU {1A579BD1-A4C4-4B1B-B092-D1670DF7F239}.Release|Any CPU.ActiveCfg = Release|Any CPU {1A579BD1-A4C4-4B1B-B092-D1670DF7F239}.Release|Any CPU.Build.0 = Release|Any CPU + {7EAB9B74-5E5E-4D93-BA1E-865906C50676}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7EAB9B74-5E5E-4D93-BA1E-865906C50676}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7EAB9B74-5E5E-4D93-BA1E-865906C50676}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7EAB9B74-5E5E-4D93-BA1E-865906C50676}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -157,6 +163,7 @@ Global {B439E0C8-F892-4AC5-BBF7-63BCDAACA7A9} = {6878D8F1-6DCE-4677-AA1A-4D14BA6D2D60} {FACBCBCB-D043-4AE8-A22D-A683040999DD} = {FEAA3936-5906-4383-B750-F07FE1B156C5} {1A579BD1-A4C4-4B1B-B092-D1670DF7F239} = {6878D8F1-6DCE-4677-AA1A-4D14BA6D2D60} + {7EAB9B74-5E5E-4D93-BA1E-865906C50676} = {A9A93AF9-2113-4321-AD20-51F60FF8B2BD} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {371030CF-B541-4BA9-9F54-3C7563415CF1} diff --git a/benchmarks/Microsoft.Extensions.Internal.Benchmarks/Microsoft.Extensions.Internal.Benchmarks.csproj b/benchmarks/Microsoft.Extensions.Internal.Benchmarks/Microsoft.Extensions.Internal.Benchmarks.csproj new file mode 100644 index 00000000000..e98bdd99e03 --- /dev/null +++ b/benchmarks/Microsoft.Extensions.Internal.Benchmarks/Microsoft.Extensions.Internal.Benchmarks.csproj @@ -0,0 +1,29 @@ + + + + netcoreapp2.1 + Exe + true + true + false + latest + + + + + Shared\%(FileName)%(Extension) + + + + + + + + + + + + + + + diff --git a/benchmarks/Microsoft.Extensions.Internal.Benchmarks/Properties/AssemblyInfo.cs b/benchmarks/Microsoft.Extensions.Internal.Benchmarks/Properties/AssemblyInfo.cs new file mode 100644 index 00000000000..409fcf814af --- /dev/null +++ b/benchmarks/Microsoft.Extensions.Internal.Benchmarks/Properties/AssemblyInfo.cs @@ -0,0 +1,4 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +[assembly: BenchmarkDotNet.Attributes.AspNetCoreBenchmark] diff --git a/benchmarks/Microsoft.Extensions.Internal.Benchmarks/WebEncodersBenchmarks.cs b/benchmarks/Microsoft.Extensions.Internal.Benchmarks/WebEncodersBenchmarks.cs new file mode 100644 index 00000000000..51151ab8439 --- /dev/null +++ b/benchmarks/Microsoft.Extensions.Internal.Benchmarks/WebEncodersBenchmarks.cs @@ -0,0 +1,80 @@ +using System; +using System.Linq; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Running; + +namespace Microsoft.Extensions.Internal.Benchmarks +{ + public class WebEncodersBenchmarks + { + private const int ByteArraySize = 500; + private readonly byte[] _data; + private readonly string _dataEncoded; + private readonly byte[] _dataWithOffset; + private readonly string _dataWithOffsetEncoded; + private readonly byte[] _guid; + private readonly string _guidEncoded; + + public WebEncodersBenchmarks() + { + var random = new Random(); + _data = new byte[ByteArraySize]; + random.NextBytes(_data); + _dataEncoded = WebEncoders.Base64UrlEncode(_data); + + _dataWithOffset = new byte[3].Concat(_data).Concat(new byte[2]).ToArray(); + _dataWithOffsetEncoded = "xx" + _dataEncoded + "yyy"; + + _guid = Guid.NewGuid().ToByteArray(); + _guidEncoded = WebEncoders.Base64UrlEncode(_guid); + } + + [Benchmark] + public byte[] Base64UrlDecode_Data() + { + return WebEncoders.Base64UrlDecode(_dataEncoded); + } + + [Benchmark] + public byte[] Base64UrlDecode_DataWithOffset() + { + return WebEncoders.Base64UrlDecode(_dataWithOffsetEncoded, 2, _dataEncoded.Length); + } + + [Benchmark] + public byte[] Base64UrlDecode_Guid() + { + return WebEncoders.Base64UrlDecode(_guidEncoded); + } + + [Benchmark] + public string Base64UrlEncode_Data() + { + return WebEncoders.Base64UrlEncode(_data); + } + + [Benchmark] + public string Base64UrlEncode_DataWithOffset() + { + return WebEncoders.Base64UrlEncode(_dataWithOffset, 3, _data.Length); + } + + [Benchmark] + public string Base64UrlEncode_Guid() + { + return WebEncoders.Base64UrlEncode(_guid); + } + + [Benchmark] + public int GetArraySizeRequiredToDecode() + { + return WebEncoders.GetArraySizeRequiredToDecode(ByteArraySize); + } + + [Benchmark] + public int GetArraySizeRequiredToEncode() + { + return WebEncoders.GetArraySizeRequiredToEncode(ByteArraySize); + } + } +} diff --git a/shared/Microsoft.Extensions.WebEncoders.Sources/Properties/EncoderResources.cs b/shared/Microsoft.Extensions.WebEncoders.Sources/Properties/EncoderResources.cs index 3474ae82c5b..ca56e35599d 100644 --- a/shared/Microsoft.Extensions.WebEncoders.Sources/Properties/EncoderResources.cs +++ b/shared/Microsoft.Extensions.WebEncoders.Sources/Properties/EncoderResources.cs @@ -19,6 +19,16 @@ internal static class EncoderResources /// internal static readonly string WebEncoders_MalformedInput = "Malformed input: {0} is an invalid input length."; + /// + /// Invalid input, that doesn't conform a base64 string. + /// + internal static readonly string WebEncoders_InvalidInput = "The input is not a valid Base-64 string as it contains a non-base 64 character, more than two padding characters, or an illegal character among the padding characters."; + + /// + /// Destination buffer is too small. + /// + internal static readonly string WebEncoders_DestinationTooSmall = "The destination buffer is too small."; + /// /// Invalid {0}, {1} or {2} length. /// diff --git a/shared/Microsoft.Extensions.WebEncoders.Sources/WebEncoders.cs b/shared/Microsoft.Extensions.WebEncoders.Sources/WebEncoders.cs index 17068ae67a5..e1ae08426d8 100644 --- a/shared/Microsoft.Extensions.WebEncoders.Sources/WebEncoders.cs +++ b/shared/Microsoft.Extensions.WebEncoders.Sources/WebEncoders.cs @@ -2,8 +2,10 @@ // 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 System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using Microsoft.Extensions.WebEncoders.Sources; #if WebEncoders_In_WebUtilities @@ -22,6 +24,10 @@ namespace Microsoft.Extensions.Internal #endif static class WebEncoders { +#if !NETCOREAPP2_2 && !NETCOREAPP2_1 + private const int MaxStackallocBytes = 256; +#endif + private const int MaxEncodedLength = (int.MaxValue / 4) * 3; // encode inflates the data by 4/3 private static readonly byte[] EmptyBytes = new byte[0]; /// @@ -37,10 +43,10 @@ public static byte[] Base64UrlDecode(string input) { if (input == null) { - throw new ArgumentNullException(nameof(input)); + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.input); } - return Base64UrlDecode(input, offset: 0, count: input.Length); + return Base64UrlDecode(input.AsSpan()); } /// @@ -56,23 +62,93 @@ public static byte[] Base64UrlDecode(string input) /// public static byte[] Base64UrlDecode(string input, int offset, int count) { - if (input == null) + if (input == null + || (uint)offset > (uint)input.Length + || (uint)count > (uint)(input.Length - offset)) { - throw new ArgumentNullException(nameof(input)); + ThrowInvalidArguments(input, offset, count); } - ValidateParameters(input.Length, nameof(input), offset, count); + return Base64UrlDecode(input.AsSpan(offset, count)); + } + /// + /// Decodes a base64url-encoded span of chars. + /// + /// The base64url-encoded input to decode. + /// The base64url-decoded form of the input. + /// + /// The input must not contain any whitespace or padding characters. + /// Throws if the input is malformed. + /// + public static byte[] Base64UrlDecode(ReadOnlySpan base64Url) + { // Special-case empty input - if (count == 0) + if (base64Url.IsEmpty) { return EmptyBytes; } - // Create array large enough for the Base64 characters, not just shorter Base64-URL-encoded form. - var buffer = new char[GetArraySizeRequiredToDecode(count)]; + var base64Len = GetBufferSizeRequiredToUrlDecode(base64Url.Length, out int dataLength); + var data = new byte[dataLength]; + var status = Base64UrlDecodeCore(base64Url, data, out int consumed, out int written); + Debug.Assert(base64Url.Length == consumed); + Debug.Assert(data.Length == written); + + return data; + } + + /// + /// Decodes a base64url-encoded span of chars into a span of bytes. + /// + /// A span containing the base64url-encoded input to decode. + /// The base64url-decoded form of . + /// The number of the bytes written to . + /// + /// The input must not contain any whitespace or padding characters. + /// Throws if the input is malformed. + /// + public static int Base64UrlDecode(ReadOnlySpan base64Url, Span data) + { + // Special-case empty input + if (base64Url.IsEmpty) + { + return 0; + } + + var status = Base64UrlDecodeCore(base64Url, data, out int consumed, out int written); + Debug.Assert(base64Url.Length == consumed); + Debug.Assert(data.Length >= written); - return Base64UrlDecode(input, offset, buffer, bufferOffset: 0, count: count); + return written; + } + + /// + /// Decode the span of UTF-8 base64url-encoded text into binary data. + /// + /// The input span which contains UTF-8 base64url-encoded text that needs to be decoded. + /// The output span which contains the result of the operation, i.e. the decoded binary data. + /// The number of input bytes consumed during the operation. This can be used to slice the input for subsequent calls, if necessary. + /// The number of bytes written into the output span. This can be used to slice the output for subsequent calls, if necessary. + /// True (default) when the input span contains the entire data to decode. + /// Set to false only if it is known that the input span contains partial data with more data to follow. + /// It returns the OperationStatus enum values: + /// - Done - on successful processing of the entire input span + /// - DestinationTooSmall - if there is not enough space in the output span to fit the decoded input + /// - NeedMoreData - only if isFinalBlock is false and the input is not a multiple of 4, otherwise the partial input would be considered as InvalidData + /// - InvalidData - if the input contains bytes outside of the expected base 64 range, or if it contains invalid/more than two padding characters, + /// or if the input is incomplete (i.e. not a multiple of 4) and isFinalBlock is true. + public static OperationStatus Base64UrlDecode(ReadOnlySpan base64Url, Span data, out int bytesConsumed, out int bytesWritten, bool isFinalBlock = true) + { + // Special-case empty input + if (base64Url.IsEmpty) + { + bytesConsumed = 0; + bytesWritten = 0; + return OperationStatus.Done; + } + + return Base64UrlDecodeCore(base64Url, data, out bytesConsumed, out bytesWritten, isFinalBlock); } /// @@ -96,144 +172,191 @@ public static byte[] Base64UrlDecode(string input, int offset, int count) /// public static byte[] Base64UrlDecode(string input, int offset, char[] buffer, int bufferOffset, int count) { - if (input == null) - { - throw new ArgumentNullException(nameof(input)); - } - if (buffer == null) + if (input == null + || (uint)offset > (uint)input.Length + || (uint)count > (uint)(input.Length - offset) + || buffer == null + || (uint)bufferOffset > (uint)buffer.Length) { - throw new ArgumentNullException(nameof(buffer)); - } - - ValidateParameters(input.Length, nameof(input), offset, count); - if (bufferOffset < 0) - { - throw new ArgumentOutOfRangeException(nameof(bufferOffset)); + ThrowInvalidArguments(input, offset, count, buffer, bufferOffset, validateBuffer: true); } + // Special-case empty input if (count == 0) { return EmptyBytes; } - // Assumption: input is base64url encoded without padding and contains no whitespace. + var base64Len = GetBufferSizeRequiredToUrlDecode(count, out int dataLength); - var paddingCharsToAdd = GetNumBase64PaddingCharsToAddForDecode(count); - var arraySizeRequired = checked(count + paddingCharsToAdd); - Debug.Assert(arraySizeRequired % 4 == 0, "Invariant: Array length must be a multiple of 4."); - - if (buffer.Length - bufferOffset < arraySizeRequired) + if ((uint)buffer.Length < (uint)(bufferOffset + base64Len)) { - throw new ArgumentException( - string.Format( - CultureInfo.CurrentCulture, - EncoderResources.WebEncoders_InvalidCountOffsetOrLength, - nameof(count), - nameof(bufferOffset), - nameof(input)), - nameof(count)); + ThrowHelper.ThrowInvalidCountOffsetOrLengthException(ExceptionArgument.count, ExceptionArgument.bufferOffset, ExceptionArgument.input); } - // Copy input into buffer, fixing up '-' -> '+' and '_' -> '/'. - var i = bufferOffset; - for (var j = offset; i - bufferOffset < count; i++, j++) + var data = new byte[dataLength]; + var status = Base64UrlDecodeCore(input.AsSpan(offset, count), data, out int consumed, out int written); + Debug.Assert(count == consumed); + Debug.Assert(dataLength == written); + + return data; + } + + /// + /// Encodes using base64url-encoding. + /// + /// The binary input to encode. + /// The base64url-encoded form of . + public static string Base64UrlEncode(byte[] input) + { + if (input == null) { - var ch = input[j]; - if (ch == '-') - { - buffer[i] = '+'; - } - else if (ch == '_') - { - buffer[i] = '/'; - } - else - { - buffer[i] = ch; - } + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.input); } - // Add the padding characters back. - for (; paddingCharsToAdd > 0; i++, paddingCharsToAdd--) + return Base64UrlEncode(input.AsSpan()); + } + + /// + /// Encodes using base64url-encoding. + /// + /// The binary input to encode. + /// The offset into at which to begin encoding. + /// The number of bytes from to encode. + /// The base64url-encoded form of . + public static string Base64UrlEncode(byte[] input, int offset, int count) + { + if (input == null + || (uint)offset > (uint)input.Length + || (uint)count > (uint)(input.Length - offset)) { - buffer[i] = '='; + ThrowInvalidArguments(input, offset, count); } - // Decode. - // If the caller provided invalid base64 chars, they'll be caught here. - return Convert.FromBase64CharArray(buffer, bufferOffset, arraySizeRequired); + return Base64UrlEncode(input.AsSpan(offset, count)); } /// - /// Gets the minimum char[] size required for decoding of characters - /// with the method. + /// Encodes using base64url-encoding. /// - /// The number of characters to decode. - /// - /// The minimum char[] size required for decoding of characters. - /// - public static int GetArraySizeRequiredToDecode(int count) + /// The binary input to encode. + /// The base64url-encoded form of . + public static unsafe string Base64UrlEncode(ReadOnlySpan data) { - if (count < 0) + // Special-case empty input + if (data.IsEmpty) { - throw new ArgumentOutOfRangeException(nameof(count)); + return string.Empty; } - if (count == 0) + var base64Len = GetBufferSizeRequiredToBase64Encode(data.Length, out int numPaddingChars); + var base64UrlLen = base64Len - numPaddingChars; + +#if NETCOREAPP2_2 || NETCOREAPP2_1 + fixed (byte* ptr = &MemoryMarshal.GetReference(data)) { - return 0; + return string.Create(base64UrlLen, (Ptr: (IntPtr)ptr, data.Length), (base64Url, state) => + { + var bytes = new ReadOnlySpan(state.Ptr.ToPointer(), state.Length); + var status = Base64UrlEncodeCore(bytes, base64Url, out int consumed, out int written); + Debug.Assert(bytes.Length == consumed); + Debug.Assert(base64Url.Length == written); + }); } - var numPaddingCharsToAdd = GetNumBase64PaddingCharsToAddForDecode(count); +#elif NETCOREAPP2_0 + char[] arrayToReturnToPool = null; + try + { + var base64Url = base64UrlLen <= MaxStackallocBytes / sizeof(char) + ? stackalloc char[base64UrlLen] + : arrayToReturnToPool = ArrayPool.Shared.Rent(base64UrlLen); - return checked(count + numPaddingCharsToAdd); - } + var status = Base64UrlEncodeCore(data, base64Url, out int consumed, out int written); + Debug.Assert(base64UrlLen == written); - /// - /// Encodes using base64url encoding. - /// - /// The binary input to encode. - /// The base64url-encoded form of . - public static string Base64UrlEncode(byte[] input) - { - if (input == null) + fixed (char* ptr = &MemoryMarshal.GetReference(base64Url)) + { + return new string(ptr, 0, written); + } + } + finally { - throw new ArgumentNullException(nameof(input)); + if (arrayToReturnToPool != null) + { + ArrayPool.Shared.Return(arrayToReturnToPool); + } } +#else + var base64Url = base64UrlLen <= MaxStackallocBytes / sizeof(char) + ? stackalloc char[base64UrlLen] + : new char[base64UrlLen]; - return Base64UrlEncode(input, offset: 0, count: input.Length); + var status = Base64UrlEncodeCore(data, base64Url, out int consumed, out int written); + Debug.Assert(base64UrlLen == written); + + fixed (char* ptr = &MemoryMarshal.GetReference(base64Url)) + { + return new string(ptr, 0, written); + } +#endif } /// - /// Encodes using base64url encoding. + /// Encodes using base64url-encoding into . /// - /// The binary input to encode. - /// The offset into at which to begin encoding. - /// The number of bytes from to encode. - /// The base64url-encoded form of . - public static string Base64UrlEncode(byte[] input, int offset, int count) + /// The binary input to encode. + /// The base64url-encoded form of . + /// The number of chars written to . + public static int Base64UrlEncode(ReadOnlySpan data, Span base64Url) { - if (input == null) + // Use base64url encoding with no padding characters. See RFC 4648, Sec. 5. + + // Special-case empty input + if (data.IsEmpty) { - throw new ArgumentNullException(nameof(input)); + return 0; } - ValidateParameters(input.Length, nameof(input), offset, count); + var status = Base64UrlEncodeCore(data, base64Url, out int consumed, out int written); + Debug.Assert(data.Length == consumed); + Debug.Assert(base64Url.Length >= written); + + return written; + } + /// + /// Encode the span of binary data into UTF-8 base64url-encoded representation. + /// + /// The input span which contains binary data that needs to be encoded. + /// + /// The output span which contains the result of the operation, i.e. the UTF-8 base64url-encoded text. + /// The span must be large enough to hold the full base64-encoded form of , included padding characters. + /// + /// The number of input bytes consumed during the operation. This can be used to slice the input for subsequent calls, if necessary. + /// The number of bytes written into the output span. This can be used to slice the output for subsequent calls, if necessary. + /// True (default) when the input span contains the entire data to decode. + /// Set to false only if it is known that the input span contains partial data with more data to follow. + /// It returns the OperationStatus enum values: + /// - Done - on successful processing of the entire input span + /// - DestinationTooSmall - if there is not enough space in the output span to fit the encoded input + /// - NeedMoreData - only if isFinalBlock is false, otherwise the output is padded if the input is not a multiple of 3 + /// It does not return InvalidData since that is not possible for base 64 encoding. + public static OperationStatus Base64UrlEncode(ReadOnlySpan data, Span base64Url, out int bytesConsumed, out int bytesWritten, bool isFinalBlock = true) + { // Special-case empty input - if (count == 0) + if (data.IsEmpty) { - return string.Empty; + bytesConsumed = 0; + bytesWritten = 0; + return OperationStatus.Done; } - 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 Base64UrlEncodeCore(data, base64Url, out bytesConsumed, out bytesWritten, isFinalBlock); } /// - /// Encodes using base64url encoding. + /// Encodes using base64url-encoding. /// /// The binary input to encode. /// The offset into at which to begin encoding. @@ -252,32 +375,19 @@ public static string Base64UrlEncode(byte[] input, int offset, int count) /// public static int Base64UrlEncode(byte[] input, int offset, char[] output, int outputOffset, int count) { - if (input == null) - { - throw new ArgumentNullException(nameof(input)); - } - if (output == null) + if (input == null + || (uint)offset > (uint)input.Length + || (uint)count > (uint)(input.Length - offset) + || output == null + || (uint)outputOffset > (uint)output.Length) { - throw new ArgumentNullException(nameof(output)); + ThrowInvalidArguments(input, offset, count, output, outputOffset, ExceptionArgument.output, validateBuffer: true); } - ValidateParameters(input.Length, nameof(input), offset, count); - if (outputOffset < 0) + var base64Len = GetArraySizeRequiredToEncode(count); + if ((uint)output.Length < (uint)(outputOffset + base64Len)) { - throw new ArgumentOutOfRangeException(nameof(outputOffset)); - } - - var arraySizeRequired = GetArraySizeRequiredToEncode(count); - if (output.Length - outputOffset < arraySizeRequired) - { - throw new ArgumentException( - string.Format( - CultureInfo.CurrentCulture, - EncoderResources.WebEncoders_InvalidCountOffsetOrLength, - nameof(count), - nameof(outputOffset), - nameof(output)), - nameof(count)); + ThrowHelper.ThrowInvalidCountOffsetOrLengthException(ExceptionArgument.count, ExceptionArgument.outputOffset, ExceptionArgument.output); } // Special-case empty input. @@ -286,103 +396,727 @@ public static int Base64UrlEncode(byte[] input, int offset, char[] output, int o return 0; } - // Use base64url encoding with no padding characters. See RFC 4648, Sec. 5. + var status = Base64UrlEncodeCore(input.AsSpan(offset, count), output.AsSpan(outputOffset), out int consumed, out int written); + Debug.Assert(count == consumed); + Debug.Assert(base64Len >= written); - // Start with default Base64 encoding. - var numBase64Chars = Convert.ToBase64CharArray(input, offset, count, output, outputOffset); + return written; + } - // Fix up '+' -> '-' and '/' -> '_'. Drop padding characters. - for (var i = outputOffset; i - outputOffset < numBase64Chars; i++) + /// + /// Gets the minimum buffer size required for decoding of characters. + /// + /// The number of characters to decode. + /// + /// The minimum buffer size required for decoding of characters. + /// + /// + /// The returned buffer size is large enough to hold characters as well + /// as base64 padding characters. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetArraySizeRequiredToDecode(int count) + { + if (count < 0) { - 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; - } + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count); } - return numBase64Chars; + return count == 0 ? 0 : GetBufferSizeRequiredToUrlDecode(count, out int dataLength); } /// - /// Get the minimum output char[] size required for encoding - /// s with the method. + /// Gets the minimum output buffer size required for encoding bytes. /// /// The number of characters to encode. /// - /// The minimum output char[] size required for encoding s. + /// The minimum output buffer size required for encoding s. /// + /// + /// The returned buffer size is large enough to hold bytes as well + /// as base64 padding characters. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int GetArraySizeRequiredToEncode(int count) { - var numWholeOrPartialInputBlocks = checked(count + 2) / 3; - return checked(numWholeOrPartialInputBlocks * 4); + return count == 0 ? 0 : GetBufferSizeRequiredToBase64Encode(count); + } + + private static OperationStatus Base64UrlDecodeCore(ReadOnlySpan base64Url, Span data, out int consumed, out int written, bool isFinalBlock = true) + { + var status = UrlEncoder.Decode(base64Url, data, out consumed, out written, isFinalBlock); + + if (status != OperationStatus.Done && status != OperationStatus.NeedMoreData) + { + ThrowHelper.ThrowOperationNotDone(status); + } + + return status; } - private static int GetNumBase64PaddingCharsInString(string str) + private static OperationStatus Base64UrlEncodeCore(ReadOnlySpan data, Span base64Url, out int consumed, out int written, bool isFinalBlock = true) { - // Assumption: input contains a well-formed base64 string with no whitespace. + var status = UrlEncoder.Encode(data, base64Url, out consumed, out written, isFinalBlock); + + if (status != OperationStatus.Done && status != OperationStatus.NeedMoreData) + { + ThrowHelper.ThrowOperationNotDone(status); + } + + return status; + } - // base64 guaranteed have 0 - 2 padding characters. - if (str[str.Length - 1] == '=') + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetBufferSizeRequiredToUrlDecode(int urlEncodedLen, out int dataLength, bool isFinalBlock = true) + { + if (isFinalBlock) { - if (str[str.Length - 2] == '=') + // Shortcut for Guid and other 16 byte data + if (urlEncodedLen == 22) { - return 2; + dataLength = 16; + return 24; } - return 1; + + var numPaddingChars = GetNumBase64PaddingCharsToAddForDecode(urlEncodedLen); + var base64Len = urlEncodedLen + numPaddingChars; + Debug.Assert(base64Len % 4 == 0, "Invariant: Array length must be a multiple of 4."); + dataLength = (base64Len >> 2) * 3 - numPaddingChars; + + return base64Len; + } + else + { + dataLength = (urlEncodedLen >> 2) * 3; + return urlEncodedLen; } - return 0; } - private static int GetNumBase64PaddingCharsToAddForDecode(int inputLength) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetNumBase64PaddingCharsToAddForDecode(int urlEncodedLen) { - switch (inputLength % 4) - { - case 0: - return 0; - case 2: - return 2; - case 3: - return 1; - default: - throw new FormatException( - string.Format( - CultureInfo.CurrentCulture, - EncoderResources.WebEncoders_MalformedInput, - inputLength)); + // Calculation is: + // switch (inputLength % 4) + // 0 -> 0 + // 2 -> 2 + // 3 -> 1 + // default -> format exception + + var result = (4 - urlEncodedLen) & 3; + + if (result == 3) + { + ThrowHelper.ThrowMalformedInputException(urlEncodedLen); } + + return result; } - private static void ValidateParameters(int bufferLength, string inputName, int offset, int count) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetBufferSizeRequiredToBase64Encode(int count) { - if (offset < 0) + if ((uint)count > MaxEncodedLength) { - throw new ArgumentOutOfRangeException(nameof(offset)); + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count); } - if (count < 0) + + var numWholeOrPartialInputBlocks = (count + 2) / 3; + return numWholeOrPartialInputBlocks * 4; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetBufferSizeRequiredToBase64Encode(int dataLength, out int numPaddingChars) + { + // Shortcut for Guid and other 16 byte data + if (dataLength == 16) { - throw new ArgumentOutOfRangeException(nameof(count)); + numPaddingChars = 2; + return 24; } - if (bufferLength - offset < count) + + numPaddingChars = GetNumBase64PaddingCharsAddedByEncode(dataLength); + return GetBufferSizeRequiredToBase64Encode(dataLength); + } + + private static int GetNumBase64PaddingCharsAddedByEncode(int dataLength) + { + // Calculation is: + // switch (dataLength % 3) + // 0 -> 0 + // 1 -> 2 + // 2 -> 1 + + return dataLength % 3 == 0 ? 0 : 3 - dataLength % 3; + } + + private static void ThrowInvalidArguments(object input, int offset, int count, char[] buffer = null, int bufferOffset = 0, ExceptionArgument bufferName = ExceptionArgument.buffer, bool validateBuffer = false) + { + throw GetInvalidArgumentsException(); + + Exception GetInvalidArgumentsException() { - throw new ArgumentException( - string.Format( - CultureInfo.CurrentCulture, - EncoderResources.WebEncoders_InvalidCountOffsetOrLength, - nameof(count), - nameof(offset), - inputName), - nameof(count)); + if (input == null) + { + return ThrowHelper.GetArgumentNullException(ExceptionArgument.input); + } + + if (validateBuffer && buffer == null) + { + return ThrowHelper.GetArgumentNullException(bufferName); + } + + if (offset < 0) + { + return ThrowHelper.GetArgumentOutOfRangeException(ExceptionArgument.offset); + } + + if (count < 0) + { + return ThrowHelper.GetArgumentOutOfRangeException(ExceptionArgument.count); + } + + if (bufferOffset < 0) + { + return ThrowHelper.GetArgumentOutOfRangeException(ExceptionArgument.bufferOffset); + } + + return ThrowHelper.GetInvalidCountOffsetOrLengthException(ExceptionArgument.count, ExceptionArgument.offset, ExceptionArgument.input); } } + + internal static class UrlEncoder + { + public static OperationStatus Decode(ReadOnlySpan urlEncoded, Span data, out int consumed, out int written, bool isFinalBlock = true) + { + ref var source = ref MemoryMarshal.GetReference(urlEncoded); + ref var destBytes = ref MemoryMarshal.GetReference(data); + + var base64Len = GetBufferSizeRequiredToUrlDecode(urlEncoded.Length, out int dataLength, isFinalBlock); + var srcLength = base64Len & ~0x3; // only decode input up to closest multiple of 4. + var destLength = data.Length; + + var sourceIndex = 0; + var destIndex = 0; + + if (urlEncoded.Length == 0) + { + goto DoneExit; + } + + ref var decodingMap = ref s_decodingMap[0]; + + // Last bytes could have padding characters, so process them separately and treat them as valid only if isFinalBlock is true. + // If isFinalBlock is false, padding characters are considered invalid. + var skipLastChunk = isFinalBlock ? 4 : 0; + + var maxSrcLength = 0; + if (destLength >= dataLength) + { + maxSrcLength = srcLength - skipLastChunk; + } + else + { + // This should never overflow since destLength here is less than int.MaxValue / 4 * 3. + // Therefore, (destLength / 3) * 4 will always be less than int.MaxValue. + maxSrcLength = (destLength / 3) * 4; + } + + while (sourceIndex < maxSrcLength) + { + var result = DecodeFour(ref Unsafe.Add(ref source, sourceIndex), ref decodingMap); + + if (result < 0) goto InvalidExit; + + WriteThreeLowOrderBytes(ref destBytes, destIndex, result); + destIndex += 3; + sourceIndex += 4; + } + + if (maxSrcLength != srcLength - skipLastChunk) + { + goto DestinationSmallExit; + } + + // If input is less than 4 bytes, srcLength == sourceIndex == 0 + // If input is not a multiple of 4, sourceIndex == srcLength != 0 + if (sourceIndex == srcLength) + { + if (isFinalBlock) + { + goto InvalidExit; + } + + goto NeedMoreExit; + } + + // If isFinalBlock is false, we will never reach this point. + + // Handle last four bytes. There are 0, 1, 2 padding chars. + var numPaddingChars = base64Len - urlEncoded.Length; + ref var lastFourStart = ref Unsafe.Add(ref source, srcLength - 4); + + if (numPaddingChars == 0) + { + var result = DecodeFour(ref lastFourStart, ref decodingMap); + + if (result < 0) goto InvalidExit; + if (destIndex > destLength - 3) goto DestinationSmallExit; + + WriteThreeLowOrderBytes(ref destBytes, destIndex, result); + destIndex += 3; + sourceIndex += 4; + } + else if (numPaddingChars == 1) + { + var result = DecodeThree(ref lastFourStart, ref decodingMap); + + if (result < 0) + { + goto InvalidExit; + } + + if (destIndex > destLength - 2) + { + goto DestinationSmallExit; + } + + WriteTwoLowOrderBytes(ref destBytes, destIndex, result); + destIndex += 2; + sourceIndex += 3; + } + else + { + var result = DecodeTwo(ref lastFourStart, ref decodingMap); + + if (result < 0) + { + goto InvalidExit; + } + + if (destIndex > destLength - 1) + { + goto DestinationSmallExit; + } + + WriteOneLowOrderByte(ref destBytes, destIndex, result); + destIndex += 1; + sourceIndex += 2; + } + + if (srcLength != base64Len) + { + goto InvalidExit; + } + + DoneExit: + consumed = sourceIndex; + written = destIndex; + return OperationStatus.Done; + + DestinationSmallExit: + if (srcLength != urlEncoded.Length && isFinalBlock) + { + goto InvalidExit; // if input is not a multiple of 4, and there is no more data, return invalid data instead + } + consumed = sourceIndex; + written = destIndex; + return OperationStatus.DestinationTooSmall; + + NeedMoreExit: + consumed = sourceIndex; + written = destIndex; + return OperationStatus.NeedMoreData; + + InvalidExit: + consumed = sourceIndex; + written = destIndex; + return OperationStatus.InvalidData; + } + + public static OperationStatus Encode(ReadOnlySpan data, Span urlEncoded, out int consumed, out int written, bool isFinalBlock = true) + { + ref var srcBytes = ref MemoryMarshal.GetReference(data); + ref var destination = ref MemoryMarshal.GetReference(urlEncoded); + + var srcLength = data.Length; + var destLength = urlEncoded.Length; + + var maxSrcLength = -2; + if (srcLength <= MaxEncodedLength && destLength >= GetBufferSizeRequiredToBase64Encode(srcLength, out int numPaddingChars) - numPaddingChars) + { + maxSrcLength += srcLength; + } + else + { + maxSrcLength += (destLength >> 2) * 3; + } + + var sourceIndex = 0; + var destIndex = 0; + + ref byte encodingMap = ref s_encodingMap[0]; + + while (sourceIndex < maxSrcLength) + { + EncodeThreeBytes(ref Unsafe.Add(ref srcBytes, sourceIndex), ref Unsafe.Add(ref destination, destIndex), ref encodingMap); + destIndex += 4; + sourceIndex += 3; + } + + if (maxSrcLength != srcLength - 2) + { + goto DestinationSmallExit; + } + + if (!isFinalBlock) + { + goto NeedMoreDataExit; + } + + if (sourceIndex == srcLength - 1) + { + EncodeOneByte(ref Unsafe.Add(ref srcBytes, sourceIndex), ref Unsafe.Add(ref destination, destIndex), ref encodingMap); + destIndex += 2; + sourceIndex += 1; + } + else if (sourceIndex == srcLength - 2) + { + EncodeTwoBytes(ref Unsafe.Add(ref srcBytes, sourceIndex), ref Unsafe.Add(ref destination, destIndex), ref encodingMap); + destIndex += 3; + sourceIndex += 2; + } + + consumed = sourceIndex; + written = destIndex; + return OperationStatus.Done; + + NeedMoreDataExit: + consumed = sourceIndex; + written = destIndex; + return OperationStatus.NeedMoreData; + + DestinationSmallExit: + consumed = sourceIndex; + written = destIndex; + return OperationStatus.DestinationTooSmall; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int DecodeFour(ref T encoded, ref sbyte decodingMap) + { + int i0, i1, i2, i3; + + if (typeof(T) == typeof(byte)) + { + ref var tmp = ref Unsafe.As(ref encoded); + i0 = Unsafe.Add(ref tmp, 0); + i1 = Unsafe.Add(ref tmp, 1); + i2 = Unsafe.Add(ref tmp, 2); + i3 = Unsafe.Add(ref tmp, 3); + } + else if (typeof(T) == typeof(char)) + { + ref var tmp = ref Unsafe.As(ref encoded); + i0 = Unsafe.Add(ref tmp, 0); + i1 = Unsafe.Add(ref tmp, 1); + i2 = Unsafe.Add(ref tmp, 2); + i3 = Unsafe.Add(ref tmp, 3); + } + else + { + throw new NotSupportedException(); // just in case new types are introduced in the future + } + + i0 = Unsafe.Add(ref decodingMap, i0); + i1 = Unsafe.Add(ref decodingMap, i1); + i2 = Unsafe.Add(ref decodingMap, i2); + i3 = Unsafe.Add(ref decodingMap, i3); + + return i0 << 18 + | i1 << 12 + | i2 << 6 + | i3; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int DecodeThree(ref T encoded, ref sbyte decodingMap) + { + int i0, i1, i2; + + if (typeof(T) == typeof(byte)) + { + ref var tmp = ref Unsafe.As(ref encoded); + i0 = Unsafe.Add(ref tmp, 0); + i1 = Unsafe.Add(ref tmp, 1); + i2 = Unsafe.Add(ref tmp, 2); + } + else if (typeof(T) == typeof(char)) + { + ref var tmp = ref Unsafe.As(ref encoded); + i0 = Unsafe.Add(ref tmp, 0); + i1 = Unsafe.Add(ref tmp, 1); + i2 = Unsafe.Add(ref tmp, 2); + } + else + { + throw new NotSupportedException(); // just in case new types are introduced in the future + } + + i0 = Unsafe.Add(ref decodingMap, i0); + i1 = Unsafe.Add(ref decodingMap, i1); + i2 = Unsafe.Add(ref decodingMap, i2); + + return i0 << 18 + | i1 << 12 + | i2 << 6; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int DecodeTwo(ref T encoded, ref sbyte decodingMap) + { + int i0, i1; + + if (typeof(T) == typeof(byte)) + { + ref var tmp = ref Unsafe.As(ref encoded); + i0 = Unsafe.Add(ref tmp, 0); + i1 = Unsafe.Add(ref tmp, 1); + } + else if (typeof(T) == typeof(char)) + { + ref var tmp = ref Unsafe.As(ref encoded); + i0 = Unsafe.Add(ref tmp, 0); + i1 = Unsafe.Add(ref tmp, 1); + } + else + { + throw new NotSupportedException(); // just in case new types are introduced in the future + } + + i0 = Unsafe.Add(ref decodingMap, i0); + i1 = Unsafe.Add(ref decodingMap, i1); + + return i0 << 18 + | i1 << 12; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteThreeLowOrderBytes(ref byte destination, int destIndex, int value) + { + Unsafe.Add(ref destination, destIndex + 0) = (byte)(value >> 16); + Unsafe.Add(ref destination, destIndex + 1) = (byte)(value >> 8); + Unsafe.Add(ref destination, destIndex + 2) = (byte)value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteTwoLowOrderBytes(ref byte destination, int destIndex, int value) + { + Unsafe.Add(ref destination, destIndex + 0) = (byte)(value >> 16); + Unsafe.Add(ref destination, destIndex + 1) = (byte)(value >> 8); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteOneLowOrderByte(ref byte destination, int destIndex, int value) + { + Unsafe.Add(ref destination, destIndex) = (byte)(value >> 16); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void EncodeThreeBytes(ref byte threeBytes, ref T encoded, ref byte encodingMap) + { + var i = (threeBytes << 16) | (Unsafe.Add(ref threeBytes, 1) << 8) | Unsafe.Add(ref threeBytes, 2); + + var i0 = Unsafe.Add(ref encodingMap, i >> 18); + var i1 = Unsafe.Add(ref encodingMap, (i >> 12) & 0x3F); + var i2 = Unsafe.Add(ref encodingMap, (i >> 6) & 0x3F); + var i3 = Unsafe.Add(ref encodingMap, i & 0x3F); + + if (typeof(T) == typeof(byte)) + { + i = i0 | (i1 << 8) | (i2 << 16) | (i3 << 24); + Unsafe.WriteUnaligned(ref Unsafe.As(ref encoded), i); + } + else if (typeof(T) == typeof(char)) + { + ref var enc = ref Unsafe.As(ref encoded); + Unsafe.Add(ref enc, 0) = (char)i0; + Unsafe.Add(ref enc, 1) = (char)i1; + Unsafe.Add(ref enc, 2) = (char)i2; + Unsafe.Add(ref enc, 3) = (char)i3; + } + else + { + throw new NotSupportedException(); // just in case new types are introduced in the future + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void EncodeTwoBytes(ref byte twoBytes, ref T encoded, ref byte encodingMap) + { + var i = (twoBytes << 16) | (Unsafe.Add(ref twoBytes, 1) << 8); + + var i0 = Unsafe.Add(ref encodingMap, i >> 18); + var i1 = Unsafe.Add(ref encodingMap, (i >> 12) & 0x3F); + var i2 = Unsafe.Add(ref encodingMap, (i >> 6) & 0x3F); + + if (typeof(T) == typeof(byte)) + { + ref var enc = ref Unsafe.As(ref encoded); + Unsafe.Add(ref enc, 0) = (byte)i0; + Unsafe.Add(ref enc, 1) = (byte)i1; + Unsafe.Add(ref enc, 2) = (byte)i2; + } + else if (typeof(T) == typeof(char)) + { + ref var enc = ref Unsafe.As(ref encoded); + Unsafe.Add(ref enc, 0) = (char)i0; + Unsafe.Add(ref enc, 1) = (char)i1; + Unsafe.Add(ref enc, 2) = (char)i2; + } + else + { + throw new NotSupportedException(); // just in case new types are introduced in the future + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void EncodeOneByte(ref byte oneByte, ref T encoded, ref byte encodingMap) + { + var i = (oneByte << 16); + + var i0 = Unsafe.Add(ref encodingMap, i >> 18); + var i1 = Unsafe.Add(ref encodingMap, (i >> 12) & 0x3F); + + if (typeof(T) == typeof(byte)) + { + ref var enc = ref Unsafe.As(ref encoded); + Unsafe.Add(ref enc, 0) = (byte)i0; + Unsafe.Add(ref enc, 1) = (byte)i1; + } + else if (typeof(T) == typeof(char)) + { + ref var enc = ref Unsafe.As(ref encoded); + Unsafe.Add(ref enc, 0) = (char)i0; + Unsafe.Add(ref enc, 1) = (char)i1; + } + else + { + throw new NotSupportedException(); // just in case new types are introduced in the future + } + } + + private static readonly sbyte[] s_decodingMap = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63, + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 + }; + + private static readonly byte[] s_encodingMap = { + 65/*A*/, 66/*B*/, 67/*C*/, 68/*D*/, 69/*E*/, 70/*F*/, 71/*G*/, 72/*H*/, + 73/*I*/, 74/*J*/, 75/*K*/, 76/*L*/, 77/*M*/, 78/*N*/, 79/*O*/, 80/*P*/, + 81/*Q*/, 82/*R*/, 83/*S*/, 84/*T*/, 85/*U*/, 86/*V*/, 87/*W*/, 88/*X*/, + 89/*Y*/, 90/*Z*/, 97/*a*/, 98/*b*/, 99/*c*/, 100/*d*/, 101/*e*/, 102/*f*/, + 103/*g*/, 104/*h*/, 105/*i*/, 106/*j*/, 107/*k*/, 108/*l*/, 109/*m*/, 110/*n*/, + 111/*o*/, 112/*p*/, 113/*q*/, 114/*r*/, 115/*s*/, 116/*t*/, 117/*u*/, 118/*v*/, + 119/*w*/, 120/*x*/, 121/*y*/, 122/*z*/, 48/*0*/, 49/*1*/, 50/*2*/, 51/*3*/, + 52/*4*/, 53/*5*/, 54/*6*/, 55/*7*/, 56/*8*/, 57/*9*/, 45/*-*/, 95/*_*/ + }; + } + + private static class ThrowHelper + { + public static void ThrowArgumentNullException(ExceptionArgument argument) + { + throw GetArgumentNullException(argument); + } + + public static void ThrowArgumentOutOfRangeException(ExceptionArgument argument) + { + throw GetArgumentOutOfRangeException(argument); + } + + public static void ThrowInvalidCountOffsetOrLengthException(ExceptionArgument arg1, ExceptionArgument arg2, ExceptionArgument arg3) + { + throw GetInvalidCountOffsetOrLengthException(arg1, arg2, arg3); + } + + public static void ThrowMalformedInputException(int inputLength) + { + throw GetMalformdedInputException(inputLength); + } + + public static void ThrowOperationNotDone(OperationStatus status) + { + throw GetOperationNotDoneException(status); + } + + public static ArgumentNullException GetArgumentNullException(ExceptionArgument argument) + { + return new ArgumentNullException(GetArgumentName(argument)); + } + + public static ArgumentOutOfRangeException GetArgumentOutOfRangeException(ExceptionArgument argument) + { + return new ArgumentOutOfRangeException(GetArgumentName(argument)); + } + + public static ArgumentException GetInvalidCountOffsetOrLengthException(ExceptionArgument arg1, ExceptionArgument arg2, ExceptionArgument arg3) + { + return new ArgumentException(EncoderResources.FormatWebEncoders_InvalidCountOffsetOrLength( + GetArgumentName(arg1), + GetArgumentName(arg2), + GetArgumentName(arg3))); + } + + private static Exception GetOperationNotDoneException(OperationStatus status) + { + switch (status) + { + case OperationStatus.DestinationTooSmall: + return new InvalidOperationException(EncoderResources.WebEncoders_DestinationTooSmall); + case OperationStatus.InvalidData: + return new FormatException(EncoderResources.WebEncoders_InvalidInput); + default: // This case won't happen. + throw new NotSupportedException(); // Just in case new states are introduced + } + } + + private static string GetArgumentName(ExceptionArgument argument) + { + Debug.Assert(Enum.IsDefined(typeof(ExceptionArgument), argument), + "The enum value is not defined, please check the ExceptionArgument Enum."); + + return argument.ToString(); + } + + private static FormatException GetMalformdedInputException(int inputLength) + { + return new FormatException(EncoderResources.FormatWebEncoders_MalformedInput(inputLength)); + } + } + + private enum ExceptionArgument + { + input, + buffer, + output, + count, + offset, + bufferOffset, + outputOffset + } } } diff --git a/test/Microsoft.Extensions.Internal.Test/Microsoft.Extensions.Internal.Test.csproj b/test/Microsoft.Extensions.Internal.Test/Microsoft.Extensions.Internal.Test.csproj index 376775ec074..d098b7bb105 100755 --- a/test/Microsoft.Extensions.Internal.Test/Microsoft.Extensions.Internal.Test.csproj +++ b/test/Microsoft.Extensions.Internal.Test/Microsoft.Extensions.Internal.Test.csproj @@ -4,6 +4,7 @@ $(StandardTestTfms) portable true + latest diff --git a/test/Microsoft.Extensions.Internal.Test/WebEncodersTests.cs b/test/Microsoft.Extensions.Internal.Test/WebEncodersTests.cs index 5c71403fd65..f5b687ef405 100644 --- a/test/Microsoft.Extensions.Internal.Test/WebEncodersTests.cs +++ b/test/Microsoft.Extensions.Internal.Test/WebEncodersTests.cs @@ -3,12 +3,68 @@ using System; using System.Linq; +using System.Buffers; using Xunit; +using System.Text; namespace Microsoft.Extensions.Internal { public class WebEncodersTests { + // Taken from https://github.com/aspnet/HttpAbstractions/pull/926 + [Fact] + public void DataOfVariousLength_RoundTripCorrectly() + { + for (var length = 0; length < 256; length++) + { + var data = new byte[length]; + for (var i = 0; i < length; i++) + { + data[i] = (byte)(5 + length + (i * 23)); + } + + string text = WebEncoders.Base64UrlEncode(data); + byte[] result = WebEncoders.Base64UrlDecode(text); + + for (var i = 0; i < length; i++) + { + Assert.Equal(data[i], result[i]); + } + } + } + + [Fact] + public void DataOfVariousLengthAsSpan_RoundTripCorrectly() + { + for (var length = 0; length < 256; length++) + { + var data = new byte[length]; + for (var i = 0; i < length; i++) + { + data[i] = (byte)(5 + length + (i * 23)); + } + + var num = WebEncoders.GetArraySizeRequiredToEncode(data.Length); + var utf8Buffer = new byte[num].AsSpan(); + var status = WebEncoders.Base64UrlEncode(data, utf8Buffer, out int bytesConsumed, out int bytesWritten); + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(data.Length, bytesConsumed); + + utf8Buffer = utf8Buffer.Slice(0, bytesWritten); + num = WebEncoders.GetArraySizeRequiredToDecode(utf8Buffer.Length); + var byteBuffer = new byte[num].AsSpan(); + status = WebEncoders.Base64UrlDecode(utf8Buffer, byteBuffer, out bytesConsumed, out bytesWritten); + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(utf8Buffer.Length, bytesConsumed); + var result = byteBuffer.Slice(0, bytesWritten).ToArray(); + + for (var i = 0; i < length; i++) + { + Assert.Equal(data[i], result[i]); + } + } + } + [Theory] [InlineData("", 1, 0)] [InlineData("", 0, 1)] @@ -36,6 +92,17 @@ public void Base64UrlDecode_MalformedInput(string input) }); } + [Fact] + public void Base64UrlDecodeAsSpan_InputIsEmptyReturns0() + { + var input = string.Empty.AsSpan(); + var output = new byte[100].AsSpan(); + + var result = WebEncoders.Base64UrlDecode(input, output); + + Assert.Equal(0, result); + } + [Theory] [InlineData("", "")] [InlineData("123456qwerty++//X+/x", "123456qwerty--__X-_x")] @@ -109,5 +176,127 @@ public void Base64UrlEncode_BadOffsets(int inputLength, int offset, int count) var retVal = WebEncoders.Base64UrlEncode(input, offset, count); }); } + + [Theory] + [InlineData(0, 0)] + [InlineData(2, 4)] + [InlineData(3, 4)] + [InlineData(4, 4)] + [InlineData(6, 8)] + [InlineData(7, 8)] + public void GetArraySizeRequiredToDecode(int inputLength, int expectedLength) + { + var result = WebEncoders.GetArraySizeRequiredToDecode(inputLength); + + Assert.Equal(expectedLength, result); + } + + [Fact] + public void GetArraySizeRequiredToDecode_NegativeInputLength_Throws() + { + var exception = Assert.Throws(() => WebEncoders.GetArraySizeRequiredToDecode(-1)); + Assert.Equal("count", exception.ParamName); + } + + [Theory] + [InlineData(1)] + [InlineData(5)] + public void GetArraySizeRequiredToDecode_MalformedInputLength(int inputLength) + { + Assert.Throws(() => + { + var retVal = WebEncoders.GetArraySizeRequiredToDecode(inputLength); + }); + } + + [Theory] + [InlineData(0, 0)] + [InlineData(2, 4)] + [InlineData(3, 4)] + [InlineData(4, 8)] + [InlineData(6, 8)] + [InlineData(7, 12)] + [InlineData(16, 24)] + public void GetArraySizeRequiredToEncode(int inputLength, int expectedLength) + { + var result = WebEncoders.GetArraySizeRequiredToEncode(inputLength); + + Assert.Equal(expectedLength, result); + } + + [Fact] + public void GetArraySizeRequiredToEncode_NegativeInputLength_Throws() + { + var exception = Assert.Throws(() => WebEncoders.GetArraySizeRequiredToEncode(-1)); + Assert.Equal("count", exception.ParamName); + } + + [Fact] + public void GetArraySizeRequiredToEncode_InputLengthTooBig_Throws() + { + var exception = Assert.Throws(() => WebEncoders.GetArraySizeRequiredToEncode((int.MaxValue / 4) * 3 + 1)); + Assert.Equal("count", exception.ParamName); + } + + [Fact] + public void Base64UrlDecode_BufferChain() + { + // Arrange + var data = new byte[20]; + var rnd = new Random(0); + rnd.NextBytes(data); + var base64UrlString = WebEncoders.Base64UrlEncode(data); + var base64Url = new byte[base64UrlString.Length]; + Encoding.ASCII.GetBytes(base64UrlString, 0, base64UrlString.Length, base64Url, 0); + + var size = WebEncoders.GetArraySizeRequiredToDecode(base64Url.Length); + var bytes = new byte[size]; + + // Act + var status = WebEncoders.Base64UrlDecode(base64Url.AsSpan(0, base64Url.Length / 2), bytes.AsSpan(), out int consumed, out int written1, isFinalBlock: false); + Assert.Equal(OperationStatus.NeedMoreData, status); + status = WebEncoders.Base64UrlDecode(base64Url.AsSpan(consumed), bytes.AsSpan(written1), out consumed, out int written2, isFinalBlock: true); + Assert.Equal(OperationStatus.Done, status); + + // Assert + var expected = data; + var actual = bytes.AsSpan(0, written1 + written2); + Assert.Equal(expected.Length, actual.Length); +#if !NET461 + Assert.True(expected.AsSpan().SequenceEqual(actual)); +#else + Assert.Equal(string.Join(",", expected), string.Join(",", actual.ToArray())); +#endif + } + + [Fact] + public void Base64UrlEncode_BufferChain() + { + // Arrange + var data = new byte[200]; + var rnd = new Random(0); + rnd.NextBytes(data); + + var size = WebEncoders.GetArraySizeRequiredToEncode(data.Length); + var base64Url = new byte[size]; + + // Act + var status = WebEncoders.Base64UrlEncode(data.AsSpan(0, data.Length / 2), base64Url.AsSpan(), out int consumed, out int written1, isFinalBlock: false); + Assert.Equal(OperationStatus.NeedMoreData, status); + status = WebEncoders.Base64UrlEncode(data.AsSpan(consumed), base64Url.AsSpan(written1), out consumed, out int written2, isFinalBlock: true); + Assert.Equal(OperationStatus.Done, status); + + // Assert + var expected = WebEncoders.Base64UrlEncode(data); + Assert.Equal(expected.Length, written1 + written2); + var chars = new char[expected.Length]; + Encoding.ASCII.GetChars(base64Url, 0, written1 + written2, chars, 0); +#if NETCOREAPP2_1 + var actual = new String(chars); +#else + var actual = new String(chars.ToArray()); +#endif + Assert.Equal(expected, actual); + } } }