From 168bfe8003b2474eb7c2951b8878b983f25ee178 Mon Sep 17 00:00:00 2001 From: Brett White Date: Tue, 23 Jul 2024 16:06:52 -0700 Subject: [PATCH] Use new Base64Url API --- .../Base64UrlEncoderTests.cs | 86 +++++ .../identitymodel.benchmarks.yml | 5 + .../JsonWebToken.cs | 19 +- .../Base64UrlEncoder.cs | 221 ++++-------- .../Base64UrlEncoding.cs | 255 +++----------- .../LogMessages.cs | 4 - .../Microsoft.IdentityModel.Tokens.csproj | 1 + .../Base64UrlEncodingTests.cs | 322 +----------------- 8 files changed, 231 insertions(+), 682 deletions(-) create mode 100644 benchmark/Microsoft.IdentityModel.Benchmarks/Base64UrlEncoderTests.cs diff --git a/benchmark/Microsoft.IdentityModel.Benchmarks/Base64UrlEncoderTests.cs b/benchmark/Microsoft.IdentityModel.Benchmarks/Base64UrlEncoderTests.cs new file mode 100644 index 0000000000..30a56e4a69 --- /dev/null +++ b/benchmark/Microsoft.IdentityModel.Benchmarks/Base64UrlEncoderTests.cs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Buffers.Text; +using BenchmarkDotNet.Attributes; +using Microsoft.IdentityModel.Tokens; + +namespace Microsoft.IdentityModel.Benchmarks +{ + // dotnet run -c release -f net9.0 --filter Microsoft.IdentityModel.Benchmarks.Base64UrlEncoderTests* + + [MemoryDiagnoser] + public class Base64UrlEncoderTests + { + private const string base64UrlEncodedString = "V1aOrL0rTJZYRxPHmMZdTWR0-ilwg9V9iEoKSn3CYl5vmBNqsM0x4VtvRbK8nCmnCIc2__QE92D4vQDR8AQ2j_BljJUkNY51VrbZ1wBar_7X2NktF_AQLkqDmwuagjhONR9_MIVysq36EAqxoHAwHHJx87XrrOkPDD8kiQ2uZEgPgK-4o02hhjsETU7KWiOKg4nKlLUU2YwuW2ZQxVubPfEv5SrW8BDgvNwPseyXfKrznhNAQHgwUX6sh1lTBm-cQdujkNsG62DeJSA2o9A_IhpKOuyQpaNda6U8jbBh3FGZhmFAm6yxNag3b6jAVlxphRNDvlm6UprgoFbvzcuH8W5ZH60LjNxsSKLH8W3gHIc7jhDA0vH2T8Nf2HEqFmqcsGr6aNm86ilWg1tchS_DlFPWqu8Wm3EEHTSJcd7BxMTvr9syRLICmhVsfHwdgMy1WfKklnyGJ_RT3kvbfCPQ2sSRMiOqCkdwCUECu-CcxS4CiIanlWnIpllmBov6vawcR6o6gmcFuqxhw2rp3815glnF7jNkmr7hsd0DPQ7qRUOHlGkF8_Sgretbgpb61y8a8DlVLlb7nBBQbTFif-lBAH4gfWWeNF9A3RFPQ8e8UKghJ7u_4ua9W_Lk_xpDkyGDXrkAzTYLxOGujRaWexOpwWSOKsXgIqXa94px0HAUIAVwP2Gy_gWcVz47ayedXh1Tcqb3K1hDlzZt4XK6O9eu-lAgy6gBltSrkntumDB-XEkxRabh8FNMln_LeEh_TgwWX4iVBR1-VD-VJw1e_aypVWj_E178TjCeb6Lc9pKD_r2VAieZpVp0c15g3vxznBWPD5mviHnK_NbSiccodSfpzGJbUsBuvKvhK4EFSw4_YlWJFlEXj3XYtiqO60crVynlEEqegLncI6RrjWe8WEfXEm_yeiglH5I-asU5sl0pBdLRdeg1xo1SZfR-CtgJ0dliwGkPDE6HcyGqhddMbIze_5I8ZazQ31PQaShhXtdH3K_cWXe4WhpR-_qYTrwib89ux2zZxePCkb_RXyvd09hv1J1kkmTf9f7q1xXfiBw49Iun90tJaOMru6PeL3Ayixj4d2C-rnwS43jcRJJ_SBiRgpBQo3Gg893UkxY2l2prQa-zU9GdbwlfDF9Htijxm75SuoxOldhTFDcpw6QqKjt1116gfkmgg16hXjvNhV8sCqxmHdKoIM6EOKVy5MAIJcg_-wbAVhbJQ205udIPb49GY1yDePieu2eQa6TU8Pn66YK5Kl4K6kCmOY6NpDdhDk6BwyJ6Z9wz2nF8OwF2mDKpMdP2nkFnq8iq2z9o7s7HwIP8pbr99kvMlw"; + // Add padding + private static readonly string base64EncodedString = base64UrlEncodedString.Replace('-', '+').Replace('_', '/') + "=="; + // Add "padding" without adding special characters + private static readonly string base64NoSpecialCharsEncodedString = base64UrlEncodedString.Replace('-', 'A').Replace('_', 'B') + "ab"; + // Add padding as only special characters (Base64-encoded but could be decoded with Base64Url API) + private static readonly string base64NoSpecialCharsExceptPaddingEncodedString = base64UrlEncodedString.Replace('-', 'A').Replace('_', 'B') + "=="; + private static readonly string decodedString = Base64UrlEncoder.Decode(base64UrlEncodedString); + private static readonly byte[] decodedBytes = Base64UrlEncoder.DecodeBytes(base64UrlEncodedString); + + [Benchmark] + public void Decode_String_Base64Url() => Base64UrlEncoder.Decode(base64UrlEncodedString); + + [Benchmark] + public void Decode_Span_Base64Url() => Base64UrlEncoder.Decode(base64UrlEncodedString.AsSpan()); + + [Benchmark] + public void DecodeBytes_Base64Url() => Base64UrlEncoder.DecodeBytes(base64UrlEncodedString); + + [Benchmark] + public void Decode_Span_Output_Base64Url() => Base64UrlEncoder.Decode(base64UrlEncodedString.AsSpan(), new byte[Base64.GetMaxDecodedFromUtf8Length(base64UrlEncodedString.Length + 2)]); + + [Benchmark] + public void Decode_String_Base64() => Base64UrlEncoder.Decode(base64EncodedString); + + [Benchmark] + public void Decode_Span_Base64() => Base64UrlEncoder.Decode(base64EncodedString.AsSpan()); + + [Benchmark] + public void DecodeBytes_Base64() => Base64UrlEncoder.DecodeBytes(base64EncodedString); + + [Benchmark] + public void Decode_Span_Output_Base64() => Base64UrlEncoder.Decode(base64EncodedString.AsSpan(), new byte[Base64.GetMaxDecodedFromUtf8Length(base64EncodedString.Length + 2)]); + + [Benchmark] + public void Decode_String_Base64NoSpecialChars() => Base64UrlEncoder.Decode(base64NoSpecialCharsEncodedString); + + [Benchmark] + public void Decode_Span_Base64NoSpecialChars() => Base64UrlEncoder.Decode(base64NoSpecialCharsEncodedString.AsSpan()); + + [Benchmark] + public void DecodeBytes_Base64NoSpecialChars() => Base64UrlEncoder.DecodeBytes(base64NoSpecialCharsEncodedString); + + [Benchmark] + public void Decode_Span_Output_Base64NoSpecialChars() => Base64UrlEncoder.Decode(base64NoSpecialCharsEncodedString.AsSpan(), new byte[Base64.GetMaxDecodedFromUtf8Length(base64NoSpecialCharsEncodedString.Length + 2)]); + + [Benchmark] + public void Decode_String_Base64NoSpecialCharsExceptPadding() => Base64UrlEncoder.Decode(base64NoSpecialCharsExceptPaddingEncodedString); + + [Benchmark] + public void Decode_Span_Base64NoSpecialCharsExceptPadding() => Base64UrlEncoder.Decode(base64NoSpecialCharsExceptPaddingEncodedString.AsSpan()); + + [Benchmark] + public void DecodeBytes_Base64NoSpecialCharsExceptPadding() => Base64UrlEncoder.DecodeBytes(base64NoSpecialCharsExceptPaddingEncodedString); + + [Benchmark] + public void Decode_Span_Output_Base64NoSpecialCharsExceptPadding() => Base64UrlEncoder.Decode(base64NoSpecialCharsExceptPaddingEncodedString.AsSpan(), new byte[Base64.GetMaxDecodedFromUtf8Length(base64NoSpecialCharsExceptPaddingEncodedString.Length + 2)]); + + [Benchmark] + public void Encode_String_Base64Url() => Base64UrlEncoder.Encode(decodedString); + + [Benchmark] + public void Encode_Bytes_Base64Url() => Base64UrlEncoder.Encode(decodedBytes); + + [Benchmark] + public void Encode_Span_Base64Url() => Base64UrlEncoder.Encode(decodedBytes, new char[Base64.GetMaxEncodedToUtf8Length(decodedBytes.Length)]); + + [Benchmark] + public void Encode_Bytes_Offset_Length_Base64Url() => Base64UrlEncoder.Encode(decodedBytes, decodedBytes.Length / 2, decodedBytes.Length / 2 - 10); + } +} diff --git a/benchmark/Microsoft.IdentityModel.Benchmarks/identitymodel.benchmarks.yml b/benchmark/Microsoft.IdentityModel.Benchmarks/identitymodel.benchmarks.yml index 8997e8a3eb..ee33073e46 100644 --- a/benchmark/Microsoft.IdentityModel.Benchmarks/identitymodel.benchmarks.yml +++ b/benchmark/Microsoft.IdentityModel.Benchmarks/identitymodel.benchmarks.yml @@ -57,3 +57,8 @@ scenarios: variables: filterArg: "*ValidateTokenAsyncTests*" + Base64UrlEncoderTests: + application: + job: benchmarks + variables: + filterArg: "*Base64UrlEncoderTests*" diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs index 48af759d86..7e39adeae1 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs @@ -3,6 +3,7 @@ using System; using System.Buffers; +using System.Buffers.Text; using System.Collections.Generic; using System.Security.Claims; using System.Text; @@ -567,17 +568,25 @@ internal void ReadToken(ReadOnlyMemory encodedTokenMemory) internal JsonClaimSet CreateClaimSet(ReadOnlySpan strSpan, int startIndex, int length, bool createHeaderClaimSet) { - int outputSize = Base64UrlEncoding.ValidateAndGetOutputSize(strSpan, startIndex, length); + ReadOnlySpan slice = Base64UrlEncoding.Validate(strSpan, startIndex, length); + int outputsize = Base64Url.GetMaxDecodedLength(length); + + byte[] rented = null; + + const int MaxStackallocThreshold = 256; + Span destination = outputsize <= MaxStackallocThreshold + ? stackalloc byte[outputsize] + : (rented = ArrayPool.Shared.Rent(outputsize)); - byte[] output = ArrayPool.Shared.Rent(outputSize); try { - Base64UrlEncoder.Decode(strSpan.Slice(startIndex, length), output); - return createHeaderClaimSet ? CreateHeaderClaimSet(output.AsSpan()) : CreatePayloadClaimSet(output.AsSpan()); + Base64UrlEncoder.Decode(slice, destination); + return createHeaderClaimSet ? CreateHeaderClaimSet(destination) : CreatePayloadClaimSet(destination); } finally { - ArrayPool.Shared.Return(output, true); + if (rented is not null) + ArrayPool.Shared.Return(rented, true); } } diff --git a/src/Microsoft.IdentityModel.Tokens/Base64UrlEncoder.cs b/src/Microsoft.IdentityModel.Tokens/Base64UrlEncoder.cs index 7fcd84cb1a..0a1a1961ff 100644 --- a/src/Microsoft.IdentityModel.Tokens/Base64UrlEncoder.cs +++ b/src/Microsoft.IdentityModel.Tokens/Base64UrlEncoder.cs @@ -3,6 +3,7 @@ using System; using System.Buffers; +using System.Buffers.Text; using System.Text; using Microsoft.IdentityModel.Logging; @@ -17,11 +18,9 @@ namespace Microsoft.IdentityModel.Tokens /// public static class Base64UrlEncoder { - private const char base64PadCharacter = '='; - private const char base64Character62 = '+'; - private const char base64Character63 = '/'; - private const char base64UrlCharacter62 = '-'; - private const char base64UrlCharacter63 = '_'; + private const char Base64PadCharacter = '='; + private const char Base64Character62 = '+'; + private const char Base64Character63 = '/'; /// /// Performs base64url encoding, which differs from regular base64 encoding as follows: @@ -99,10 +98,7 @@ public static string Encode(byte[] inArray, int offset, int length) LogHelper.MarkAsNonPII(inArray.Length)))); #pragma warning restore CA2208 // Instantiate argument exceptions correctly - char[] destination = new char[(inArray.Length + 2) / 3 * 4]; - int j = Encode(inArray.AsSpan().Slice(offset, length), destination.AsSpan()); - - return new string(destination, 0, j); + return Base64Url.EncodeToString(inArray.AsSpan().Slice(offset, length)); } /// @@ -111,60 +107,7 @@ public static string Encode(byte[] inArray, int offset, int length) /// A read-only span of bytes to encode. /// The span of characters to write the encoded output. /// The number of characters written to the output span. - public static int Encode(ReadOnlySpan inArray, Span output) - { - int lengthmod3 = inArray.Length % 3; - int limit = (inArray.Length - lengthmod3); - ReadOnlySpan table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"u8; - - int i, j = 0; - - // takes 3 bytes from inArray and insert 4 bytes into output - for (i = 0; i < limit; i += 3) - { - byte d0 = inArray[i]; - byte d1 = inArray[i + 1]; - byte d2 = inArray[i + 2]; - - output[j + 0] = (char)table[d0 >> 2]; - output[j + 1] = (char)table[((d0 & 0x03) << 4) | (d1 >> 4)]; - output[j + 2] = (char)table[((d1 & 0x0f) << 2) | (d2 >> 6)]; - output[j + 3] = (char)table[d2 & 0x3f]; - j += 4; - } - - //Where we left off before - i = limit; - - switch (lengthmod3) - { - case 2: - { - byte d0 = inArray[i]; - byte d1 = inArray[i + 1]; - - output[j + 0] = (char)table[d0 >> 2]; - output[j + 1] = (char)table[((d0 & 0x03) << 4) | (d1 >> 4)]; - output[j + 2] = (char)table[(d1 & 0x0f) << 2]; - j += 3; - } - break; - - case 1: - { - byte d0 = inArray[i]; - - output[j + 0] = (char)table[d0 >> 2]; - output[j + 1] = (char)table[(d0 & 0x03) << 4]; - j += 2; - } - break; - - //default or case 0: no further operations are needed. - } - - return j; - } + public static int Encode(ReadOnlySpan inArray, Span output) => Base64Url.EncodeToChars(inArray, output); /// /// Converts the specified base64url encoded string to UTF-8 bytes. @@ -177,47 +120,68 @@ public static byte[] DecodeBytes(string str) return Decode(str.AsSpan()); } +#if NETCOREAPP + [SkipLocalsInit] +#endif internal static byte[] Decode(ReadOnlySpan strSpan) { - int mod = strSpan.Length % 4; - if (mod == 1) - throw LogHelper.LogExceptionMessage(new FormatException(LogHelper.FormatInvariant(LogMessages.IDX10400, strSpan.ToString()))); + int upperBound = Base64Url.GetMaxDecodedLength(strSpan.Length); + byte[] rented = null; - bool needReplace = strSpan.IndexOfAny(base64UrlCharacter62, base64UrlCharacter63) >= 0; - int decodedLength = strSpan.Length + (4 - mod) % 4; + const int MaxStackallocThreshold = 256; + Span destination = upperBound <= MaxStackallocThreshold + ? stackalloc byte[upperBound] + : (rented = ArrayPool.Shared.Rent(upperBound)); -#if NET6_0_OR_GREATER + try + { + int bytesWritten = Decode(strSpan, destination); + return destination.Slice(0, bytesWritten).ToArray(); + } + finally + { + if (rented is not null) + ArrayPool.Shared.Return(rented, true); + } + } - Span output = new byte[decodedLength]; + private static bool IsOnlyValidBase64Chars(ReadOnlySpan strSpan) + { + foreach (char c in strSpan) + if (!char.IsDigit(c) && !char.IsLetter(c) && c != Base64Character62 && c != Base64Character63 && c != Base64PadCharacter) + return false; - int length = Decode(strSpan, output, needReplace, decodedLength); + return true; + } - return output.Slice(0, length).ToArray(); +#if NETCOREAPP + [SkipLocalsInit] +#endif + internal static int Decode(ReadOnlySpan strSpan, Span output) + { + OperationStatus status = Base64Url.DecodeFromChars(strSpan, output, out _, out int bytesWritten); + if (status == OperationStatus.Done) + return bytesWritten; + + if (status == OperationStatus.InvalidData && +#if NET9_0_OR_GREATER + !Base64.IsValid(strSpan)) #else - return UnsafeDecode(strSpan, needReplace, decodedLength); + !IsOnlyValidBase64Chars(strSpan)) #endif - } + throw LogHelper.LogExceptionMessage(new FormatException(LogHelper.FormatInvariant(LogMessages.IDX10400, strSpan.ToString()))); - internal static void Decode(ReadOnlySpan strSpan, Span output) - { int mod = strSpan.Length % 4; if (mod == 1) throw LogHelper.LogExceptionMessage(new FormatException(LogHelper.FormatInvariant(LogMessages.IDX10400, strSpan.ToString()))); - - bool needReplace = strSpan.IndexOfAny(base64UrlCharacter62, base64UrlCharacter63) >= 0; int decodedLength = strSpan.Length + (4 - mod) % 4; -#if NET6_0_OR_GREATER - Decode(strSpan, output, needReplace, decodedLength); -#else - Decode(strSpan, output, needReplace, decodedLength); -#endif + return Decode(strSpan, output, decodedLength); } -#if NET6_0_OR_GREATER - +#if NETCOREAPP [SkipLocalsInit] - private static int Decode(ReadOnlySpan strSpan, Span output, bool needReplace, int decodedLength) + private static int Decode(ReadOnlySpan strSpan, Span output, int decodedLength) { // If the incoming chars don't contain any of the base64url characters that need to be replaced, // and if the incoming chars are of the exact right length, then we'll be able to just pass the @@ -230,14 +194,14 @@ private static int Decode(ReadOnlySpan strSpan, Span output, bool ne scoped Span charsSpan = default; scoped ReadOnlySpan source = strSpan; - if (needReplace || decodedLength != source.Length) + if (decodedLength != source.Length) { charsSpan = decodedLength <= StackAllocThreshold ? stackalloc char[StackAllocThreshold] : arrayPoolChars = ArrayPool.Shared.Rent(decodedLength); charsSpan = charsSpan.Slice(0, decodedLength); - source = HandlePaddingAndReplace(source, charsSpan, needReplace); + source = HandlePadding(source, charsSpan); } byte[] arrayPoolBytes = null; @@ -250,7 +214,7 @@ private static int Decode(ReadOnlySpan strSpan, Span output, bool ne try { - OperationStatus status = System.Buffers.Text.Base64.DecodeFromUtf8InPlace(utf8Span, out int bytesWritten); + OperationStatus status = Base64.DecodeFromUtf8InPlace(utf8Span, out int bytesWritten); if (status != OperationStatus.Done) throw LogHelper.LogExceptionMessage(new FormatException(LogHelper.FormatInvariant(LogMessages.IDX10400, strSpan.ToString()))); @@ -274,86 +238,47 @@ private static int Decode(ReadOnlySpan strSpan, Span output, bool ne } } - private static ReadOnlySpan HandlePaddingAndReplace(ReadOnlySpan source, Span charsSpan, bool needReplace) + private static ReadOnlySpan HandlePadding(ReadOnlySpan source, Span charsSpan) { source.CopyTo(charsSpan); if (source.Length < charsSpan.Length) { - charsSpan[source.Length] = base64PadCharacter; + charsSpan[source.Length] = Base64PadCharacter; if (source.Length + 1 < charsSpan.Length) { - charsSpan[source.Length + 1] = base64PadCharacter; - } - } - - if (needReplace) - { - Span remaining = charsSpan; - int pos; - while ((pos = remaining.IndexOfAny(base64UrlCharacter62, base64UrlCharacter63)) >= 0) - { - remaining[pos] = (remaining[pos] == base64UrlCharacter62) ? base64Character62 : base64Character63; - remaining = remaining.Slice(pos + 1); + charsSpan[source.Length + 1] = Base64PadCharacter; } } return charsSpan; } - #else - - private static unsafe byte[] UnsafeDecode(ReadOnlySpan strSpan, bool needReplace, int decodedLength) + private static unsafe byte[] UnsafeDecode(ReadOnlySpan strSpan, int decodedLength) { - if (needReplace) + if (decodedLength == strSpan.Length) { - string decodedString = new(char.MinValue, decodedLength); - fixed (char* dest = decodedString) - { - int i = 0; - for (; i < strSpan.Length; i++) - { - if (strSpan[i] == base64UrlCharacter62) - dest[i] = base64Character62; - else if (strSpan[i] == base64UrlCharacter63) - dest[i] = base64Character63; - else - dest[i] = strSpan[i]; - } - - for (; i < decodedLength; i++) - dest[i] = base64PadCharacter; - } - - return Convert.FromBase64String(decodedString); + return Convert.FromBase64CharArray(strSpan.ToArray(), 0, strSpan.Length); } - else + + string decodedString = new(char.MinValue, decodedLength); + fixed (char* src = strSpan) + fixed (char* dest = decodedString) { - if (decodedLength == strSpan.Length) - { - return Convert.FromBase64CharArray(strSpan.ToArray(), 0, strSpan.Length); - } - else - { - string decodedString = new(char.MinValue, decodedLength); - fixed (char* src = strSpan) - fixed (char* dest = decodedString) - { - Buffer.MemoryCopy(src, dest, strSpan.Length * 2, strSpan.Length * 2); - - dest[strSpan.Length] = base64PadCharacter; - if (strSpan.Length + 2 == decodedLength) - dest[strSpan.Length + 1] = base64PadCharacter; - } - - return Convert.FromBase64String(decodedString); - } + Buffer.MemoryCopy(src, dest, strSpan.Length * 2, strSpan.Length * 2); + + dest[strSpan.Length] = Base64PadCharacter; + if (strSpan.Length + 2 == decodedLength) + dest[strSpan.Length + 1] = Base64PadCharacter; } + + return Convert.FromBase64String(decodedString); } - private static void Decode(ReadOnlySpan strSpan, Span output, bool needReplace, int decodedLength) + private static int Decode(ReadOnlySpan strSpan, Span output, int decodedLength) { - byte[] result = UnsafeDecode(strSpan, needReplace, decodedLength); + byte[] result = UnsafeDecode(strSpan, decodedLength); result.CopyTo(output); + return result.Length; } #endif diff --git a/src/Microsoft.IdentityModel.Tokens/Base64UrlEncoding.cs b/src/Microsoft.IdentityModel.Tokens/Base64UrlEncoding.cs index 558aaf54bf..7618fe6426 100644 --- a/src/Microsoft.IdentityModel.Tokens/Base64UrlEncoding.cs +++ b/src/Microsoft.IdentityModel.Tokens/Base64UrlEncoding.cs @@ -3,48 +3,19 @@ using System; using System.Buffers; +using System.Buffers.Text; using Microsoft.IdentityModel.Logging; namespace Microsoft.IdentityModel.Tokens { - /// - /// Base64 encode/decode implementation for as per https://tools.ietf.org/html/rfc4648#section-5. - /// Uses ArrayPool[T] to minimize memory usage. - /// internal static class Base64UrlEncoding { - private const uint IntA = 'A'; - private const uint IntZ = 'Z'; - private const uint Inta = 'a'; - private const uint Intz = 'z'; - private const uint Int0 = '0'; - private const uint Int9 = '9'; - private const uint IntEq = '='; - private const uint IntPlus = '+'; - private const uint IntMinus = '-'; - private const uint IntSlash = '/'; - private const uint IntUnderscore = '_'; - - private static readonly char[] Base64Table = - { - 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', - 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', - 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', - 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', - '8', '9', '-', '_', - }; - /// /// Decodes a base64url encoded string into a byte array. /// /// The base64url encoded string to decode. /// The decoded bytes. - public static byte[] Decode(string inputString) - { - _ = inputString ?? throw LogHelper.LogArgumentNullException(nameof(inputString)); - - return Decode(inputString, 0, inputString.Length); - } + public static byte[] Decode(string inputString) => Decode(inputString, 0, inputString.Length); /// /// Decodes a base64url encoded substring of a string into a byte array. @@ -57,11 +28,13 @@ public static byte[] Decode(string input, int offset, int length) { _ = input ?? throw LogHelper.LogArgumentNullException(nameof(input)); - ReadOnlySpan inputSpan = input.AsSpan(); - int outputsize = ValidateAndGetOutputSize(inputSpan, offset, length); + ReadOnlySpan slice = Validate(input.AsSpan(), offset, length); + int outputsize = Base64Url.GetMaxDecodedLength(length); byte[] output = new byte[outputsize]; - Decode(inputSpan, offset, length, output); - return output; + OperationStatus status = Base64Url.DecodeFromChars(slice, output, out _, out int bytesWritten); + if (status != OperationStatus.Done) + throw LogHelper.LogExceptionMessage(new FormatException(LogHelper.FormatInvariant(LogMessages.IDX10400, slice.ToString()))); + return output.AsSpan().Slice(0, bytesWritten).ToArray(); } /// @@ -78,23 +51,26 @@ public static byte[] Decode(string input, int offset, int length) /// /// The buffer for the decode operation uses a shared memory pool to avoid allocations. /// The length of the rented array of bytes may be larger than the decoded bytes; therefore, the action needs to know the actual length to use. - /// The result of is passed to the action. + /// The result of is passed to the action. /// public static T Decode(string input, int offset, int length, TX argx, Func action) { _ = action ?? throw new ArgumentNullException(nameof(action)); - ReadOnlySpan inputSpan = input.AsSpan(); - int outputsize = ValidateAndGetOutputSize(inputSpan, offset, length); + ReadOnlySpan slice = Validate(input.AsSpan(), offset, length); + int outputsize = Base64Url.GetMaxDecodedLength(length); byte[] output = ArrayPool.Shared.Rent(outputsize); + try { - Decode(inputSpan, offset, length, output); + OperationStatus status = Base64Url.DecodeFromChars(slice, output, out _, out int bytesWritten); + if (status != OperationStatus.Done) + throw LogHelper.LogExceptionMessage(new FormatException(LogHelper.FormatInvariant(LogMessages.IDX10400, slice.ToString()))); return action(output, outputsize, argx); } finally { - ArrayPool.Shared.Return(output); + ArrayPool.Shared.Return(output, true); } } @@ -110,24 +86,27 @@ public static T Decode(string input, int offset, int length, TX argx, Fun /// /// The buffer for the decode operation uses a shared memory pool to avoid allocations. /// The length of the rented array of bytes may be larger than the decoded bytes; therefore, the action needs to know the actual length to use. - /// The result of is passed to the action. + /// The result of is passed to the action. /// public static T Decode(string input, int offset, int length, Func action) { _ = input ?? throw LogHelper.LogArgumentNullException(nameof(input)); _ = action ?? throw new ArgumentNullException(nameof(action)); - ReadOnlySpan inputSpan = input.AsSpan(); - int outputsize = ValidateAndGetOutputSize(inputSpan, offset, length); - byte[] output = ArrayPool.Shared.Rent(outputsize); + ReadOnlySpan slice = Validate(input.AsSpan(), offset, length); + int outputsize = Base64Url.GetMaxDecodedLength(length); + byte[] destination = ArrayPool.Shared.Rent(outputsize); + try { - Decode(inputSpan, offset, length, output); - return action(output, outputsize); + OperationStatus status = Base64Url.DecodeFromChars(slice, destination, out _, out int bytesWritten); + if (status != OperationStatus.Done) + throw LogHelper.LogExceptionMessage(new FormatException(LogHelper.FormatInvariant(LogMessages.IDX10400, slice.ToString()))); + return action(destination, bytesWritten); } finally { - ArrayPool.Shared.Return(output); + ArrayPool.Shared.Return(destination, true); } } @@ -149,7 +128,7 @@ public static T Decode(string input, int offset, int length, Func /// The buffer for the decode operation uses a shared memory pool to avoid allocations. /// The length of the rented array of bytes may be larger than the decoded bytes; therefore, the action needs to know the actual length to use. - /// The result of is passed to the action. + /// The result of is passed to the action. /// public static T Decode( string input, @@ -162,106 +141,20 @@ public static T Decode( { _ = action ?? throw LogHelper.LogArgumentNullException(nameof(action)); - ReadOnlySpan inputSpan = input.AsSpan(); - int outputsize = ValidateAndGetOutputSize(inputSpan, offset, length); - byte[] output = ArrayPool.Shared.Rent(outputsize); + ReadOnlySpan slice = Validate(input.AsSpan(), offset, length); + int outputsize = Base64Url.GetMaxDecodedLength(length); + byte[] destination = ArrayPool.Shared.Rent(outputsize); + try { - Decode(inputSpan, offset, length, output); - return action(output, outputsize, argx, argy, argz); + OperationStatus status = Base64Url.DecodeFromChars(slice, destination, out _, out int bytesWritten); + if (status != OperationStatus.Done) + throw LogHelper.LogExceptionMessage(new FormatException(LogHelper.FormatInvariant(LogMessages.IDX10400, slice.ToString()))); + return action(destination, bytesWritten, argx, argy, argz); } finally { - ArrayPool.Shared.Return(output); - } - } - - /// - /// Decodes a Base64Url encoded substring of a string into a byte array. - /// - /// The string represented as a span to decode. - /// The index of the character in to start decoding from. - /// The number of characters beginning from to decode. - /// The byte array to place the decoded results into. - /// - /// Changes from Base64UrlEncoder implementation: - /// 1. Padding is optional. - /// 2. '+' and '-' are treated the same. - /// 3. '/' and '_' are treated the same. - /// - internal static void Decode(ReadOnlySpan input, int offset, int length, byte[] output) - { - int outputpos = 0; - uint curblock = 0x000000FFu; - for (int i = offset; i < (offset + length); i++) - { - uint cur = input[i]; - if (cur >= IntA && cur <= IntZ) - { - cur -= IntA; - } - else if (cur >= Inta && cur <= Intz) - { - cur = (cur - Inta) + 26u; - } - else if (cur >= Int0 && cur <= Int9) - { - cur = (cur - Int0) + 52u; - } - else if (cur == IntPlus || cur == IntMinus) - { - cur = 62u; - } - else if (cur == IntSlash || cur == IntUnderscore) - { - cur = 63u; - } - else if (cur == IntEq) - { - continue; - } - else - { - throw LogHelper.LogExceptionMessage(new ArgumentOutOfRangeException( - LogHelper.FormatInvariant( - LogMessages.IDX10820, - LogHelper.MarkAsNonPII(cur), - input.ToString()))); - } - - curblock = (curblock << 6) | cur; - - // check if 4 characters have been read, based on number of shifts. - if ((0xFF000000u & curblock) == 0xFF000000u) - { - output[outputpos++] = (byte)(curblock >> 16); - output[outputpos++] = (byte)(curblock >> 8); - output[outputpos++] = (byte)curblock; - curblock = 0x000000FFu; - } - } - - // Handle spill over characters. This accounts for case where padding character is not present. - if (curblock != 0x000000FFu) - { - if ((0x03FC0000u & curblock) == 0x03FC0000u) - { - // shifted 3 times, 1 padding character, 2 output characters - curblock <<= 6; - output[outputpos++] = (byte)(curblock >> 16); - output[outputpos++] = (byte)(curblock >> 8); - } - else if ((0x000FF000u & curblock) == 0x000FF000u) - { - // shifted 2 times, 2 padding character, 1 output character - curblock <<= 12; - output[outputpos++] = (byte)(curblock >> 16); - } - else - { - throw LogHelper.LogExceptionMessage(new ArgumentException( - LogHelper.FormatInvariant(LogMessages.IDX10821, input.ToString()))); - } + ArrayPool.Shared.Return(destination, true); } } @@ -270,11 +163,7 @@ internal static void Decode(ReadOnlySpan input, int offset, int length, by /// /// The bytes to encode. /// base64url encoded string. - public static string Encode(byte[] bytes) - { - _ = bytes ?? throw LogHelper.LogArgumentNullException(nameof(bytes)); - return Encode(bytes, 0, bytes.Length); - } + public static string Encode(byte[] bytes) => Encode(bytes, 0, bytes.Length); /// /// Encodes a subset of a byte array into a Base64Url encoded string. @@ -320,15 +209,7 @@ public static string Encode(byte[] input, int offset, int length) LogHelper.MarkAsNonPII(input.Length)))); #pragma warning restore CA2208 // Instantiate argument exceptions correctly - int outputsize = length % 3; - if (outputsize > 0) - outputsize++; - - outputsize += (length / 3) * 4; - - char[] output = new char[outputsize]; - WriteEncodedOutput(input, offset, length, output); - return new string(output); + return Base64Url.EncodeToString(input); } /// @@ -337,15 +218,12 @@ public static string Encode(byte[] input, int offset, int length) /// The string represented by a span to validate. /// The index of the character in to start the decode operation. /// The number of characters in to decode, starting from . - /// The size of the decoded bytes array. - internal static int ValidateAndGetOutputSize(ReadOnlySpan strSpan, int offset, int length) + /// The slice of the span. + internal static ReadOnlySpan Validate(ReadOnlySpan strSpan, int offset, int length) { if (strSpan.IsEmpty) throw LogHelper.LogArgumentNullException(nameof(strSpan)); - if (length == 0) - return 0; - if (offset < 0) throw LogHelper.LogExceptionMessage(new ArgumentException( LogHelper.FormatInvariant( @@ -374,58 +252,7 @@ internal static int ValidateAndGetOutputSize(ReadOnlySpan strSpan, int off if (length % 4 == 1) throw LogHelper.LogExceptionMessage(new FormatException(LogHelper.FormatInvariant(LogMessages.IDX10400, strSpan.ToString()))); - int lastCharPosition = offset + length - 1; - - // Compute useful length (i.e. ignore padding characters) - if (strSpan[lastCharPosition] == '=') - { - lastCharPosition--; - if (strSpan[lastCharPosition] == '=') - lastCharPosition--; - } - - int effectiveLength = 1 + (lastCharPosition - offset); - int outputSize = effectiveLength % 4; - if (outputSize > 0) - outputSize--; - - outputSize += (effectiveLength / 4) * 3; - return outputSize; - } - - private static void WriteEncodedOutput(byte[] inputBytes, int offset, int length, Span output) - { - uint curBlock = 0x000000FFu; - int outputPointer = 0; - - for (int i = offset; i < offset + length; i++) - { - curBlock = (curBlock << 8) | inputBytes[i]; - - if ((curBlock & 0xFF000000u) == 0xFF000000u) - { - output[outputPointer++] = Base64Table[(curBlock & 0x00FC0000u) >> 18]; - output[outputPointer++] = Base64Table[(curBlock & 0x00030000u | curBlock & 0x0000F000u) >> 12]; - output[outputPointer++] = Base64Table[(curBlock & 0x00000F00u | curBlock & 0x000000C0u) >> 6]; - output[outputPointer++] = Base64Table[curBlock & 0x0000003Fu]; - - curBlock = 0x000000FFu; - } - } - - if ((curBlock & 0x00FF0000u) == 0x00FF0000u) - { - // 2 shifts, 3 output characters. - output[outputPointer++] = Base64Table[(curBlock & 0x0000FC00u) >> 10]; - output[outputPointer++] = Base64Table[(curBlock & 0x000003F0u) >> 4]; - output[outputPointer++] = Base64Table[(curBlock & 0x0000000Fu) << 2]; - } - else if ((curBlock & 0x0000FF00u) == 0x0000FF00u) - { - // 1 shift, 2 output characters. - output[outputPointer++] = Base64Table[(curBlock & 0x000000FCu) >> 2]; - output[outputPointer++] = Base64Table[(curBlock & 0x00000003u) << 4]; - } + return strSpan.Slice(offset, length); } } } diff --git a/src/Microsoft.IdentityModel.Tokens/LogMessages.cs b/src/Microsoft.IdentityModel.Tokens/LogMessages.cs index 8a282dd2fc..03488753a6 100644 --- a/src/Microsoft.IdentityModel.Tokens/LogMessages.cs +++ b/src/Microsoft.IdentityModel.Tokens/LogMessages.cs @@ -252,10 +252,6 @@ internal static class LogMessages public const string IDX10815 = "IDX10815: Depth of JSON: '{0}' exceeds max depth of '{1}'."; public const string IDX10816 = "IDX10816: Decompressing would result in a token with a size greater than allowed. Maximum size allowed: '{0}'."; - // Base64UrlEncoding - public const string IDX10820 = "IDX10820: Invalid character found in Base64UrlEncoding. Character: '{0}', Encoding: '{1}'."; - public const string IDX10821 = "IDX10821: Incorrect padding detected in Base64UrlEncoding. Encoding: '{0}'."; - //EventBasedLRUCache errors public const string IDX10900 = "IDX10900: EventBasedLRUCache._eventQueue encountered an error while processing a cache operation. Exception '{0}'."; public const string IDX10901 = "IDX10901: CryptoProviderCacheOptions.SizeLimit must be greater than 10. Value: '{0}'"; diff --git a/src/Microsoft.IdentityModel.Tokens/Microsoft.IdentityModel.Tokens.csproj b/src/Microsoft.IdentityModel.Tokens/Microsoft.IdentityModel.Tokens.csproj index 6636af5a56..72200ff4ec 100644 --- a/src/Microsoft.IdentityModel.Tokens/Microsoft.IdentityModel.Tokens.csproj +++ b/src/Microsoft.IdentityModel.Tokens/Microsoft.IdentityModel.Tokens.csproj @@ -39,6 +39,7 @@ + diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/Base64UrlEncodingTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/Base64UrlEncodingTests.cs index 71883d4822..264edd4347 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/Base64UrlEncodingTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/Base64UrlEncodingTests.cs @@ -2,330 +2,30 @@ // Licensed under the MIT License. using System; -using System.Text; -using Microsoft.IdentityModel.TestUtils; using Xunit; namespace Microsoft.IdentityModel.Tokens.UrlEncoding.Tests { public class Base64UrlEncoderTests { - [Theory, MemberData(nameof(EncodeTestCases), DisableDiscoveryEnumeration = true)] - public void EncodeTests(Base64UrlEncoderTheoryData theoryData) - { - var context = TestUtilities.WriteHeader("EncodeTests", theoryData); - string encoderString = null; - string encoderBytes = null; - string encoderBytesUsingOffset = null; - string encoderBytesUsingSpan = null; - - try - { - // to get to error code in Base64UrlEncoding, we need to skip Encoder - if (!theoryData.EncodingOnly) - { - encoderString = Base64UrlEncoder.Encode(theoryData.Json); - encoderBytes = Base64UrlEncoder.Encode(theoryData.Bytes); - encoderBytesUsingOffset = Base64UrlEncoder.Encode(theoryData.OffsetBytes, theoryData.Offset, theoryData.Length); - int encodedCharsCount = Base64UrlEncoder.Encode(theoryData.OffsetBytes.AsSpan().Slice(theoryData.Offset, theoryData.Length), theoryData.Chars.AsSpan()); - encoderBytesUsingSpan = new string(theoryData.Chars, 0, encodedCharsCount); - } - - string encodingString = Base64UrlEncoding.Encode(theoryData.Bytes); - string encodingBytesUsingOffset = Base64UrlEncoding.Encode(theoryData.OffsetBytes, theoryData.Offset, theoryData.Length); - - theoryData.ExpectedException.ProcessNoException(context); - - if (!theoryData.EncodingOnly) - { - IdentityComparer.AreStringsEqual(encoderString, encoderBytes, "encoderString", "encoderBytes", context); - IdentityComparer.AreStringsEqual(encoderBytesUsingOffset, encoderBytes, "encoderBytesUsingOffset", "encoderBytes", context); - IdentityComparer.AreStringsEqual(encoderBytesUsingSpan, encoderBytes, "encoderBytesUsingSpan", "encoderBytes", context); - IdentityComparer.AreStringsEqual(encodingString, encoderBytes, "encodingString", "encoderBytes", context); - } - - IdentityComparer.AreStringsEqual(encodingBytesUsingOffset, encodingString, "encodingBytesUsingOffset", "encodingString", context); - IdentityComparer.AreStringsEqual(theoryData.ExpectedValue, encodingString, "theoryData.ExpectedValue", "encodingString", context); - - } - catch (Exception ex) - { - theoryData.ExpectedException.ProcessException(ex, context); - } - - TestUtilities.AssertFailIfErrors(context); - } - - public static TheoryData EncodeTestCases - { - get - { - TheoryData theoryData = new TheoryData(); - - // These values are sourced from https://datatracker.ietf.org/doc/html/rfc7519#section-6.1 - string json = $@"{{""alg"":""none""}}"; - string expectedValue = "eyJhbGciOiJub25lIn0"; - byte[] utf8Bytes = Encoding.UTF8.GetBytes(json); - - theoryData.Add(new Base64UrlEncoderTheoryData("Header_Offset_0") - { - Bytes = utf8Bytes, - Chars = new char[1024], - ExpectedValue = expectedValue, - Json = json, - Length = utf8Bytes.Length, - Offset = 0, - OffsetBytes = utf8Bytes, - OffsetLength = utf8Bytes.Length - }); - - // NOTE the spec performs the encoding over the \r\n and space ' '. - json = "{\"iss\":\"joe\",\r\n \"exp\":1300819380,\r\n \"http://example.com/is_root\":true}"; - expectedValue = "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ"; - utf8Bytes = Encoding.UTF8.GetBytes(json); - - theoryData.Add(new Base64UrlEncoderTheoryData("Payload_Offset_0") - { - Bytes = utf8Bytes, - Chars = new char[1024], - ExpectedValue = expectedValue, - Json = json, - Length = utf8Bytes.Length, - Offset = 0, - OffsetBytes = utf8Bytes, - OffsetLength = utf8Bytes.Length - }); - - byte[] utf8BytesOffset = new byte[utf8Bytes.Length * 2]; - int count = Encoding.UTF8.GetBytes(json, 0, json.Length, utf8BytesOffset, 5); - theoryData.Add(new Base64UrlEncoderTheoryData("Payload_Offset_5") - { - Bytes = utf8Bytes, - Chars = new char[1024], - ExpectedValue = expectedValue, - Json = json, - Length = count, - Offset = 5, - OffsetBytes = utf8BytesOffset, - OffsetLength = count - }); - - theoryData.Add(new Base64UrlEncoderTheoryData("JsonNULL") - { - Bytes = utf8Bytes, - Chars = new char[1024], - ExpectedException = ExpectedException.ArgumentNullException("IDX10000:"), - ExpectedValue = expectedValue, - Json = null, - Length = count, - Offset = 5, - OffsetBytes = utf8BytesOffset, - OffsetLength = count - }); - - theoryData.Add(new Base64UrlEncoderTheoryData("BytesNULL") - { - Bytes = null, - Chars = new char[1024], - ExpectedException = ExpectedException.ArgumentNullException("IDX10000:"), - ExpectedValue = expectedValue, - Json = json, - Length = count, - Offset = 5, - OffsetBytes = utf8BytesOffset, - OffsetLength = count - }); - - theoryData.Add(new Base64UrlEncoderTheoryData("OffsetBytesNULL") - { - Bytes = utf8Bytes, - Chars = new char[1024], - ExpectedException = ExpectedException.ArgumentNullException("IDX10000:"), - ExpectedValue = expectedValue, - Json = json, - Length = count, - Offset = 5, - OffsetBytes = null, - OffsetLength = count - }); - - theoryData.Add(new Base64UrlEncoderTheoryData("Length_Negative") - { - Bytes = utf8Bytes, - Chars = new char[1024], - ExpectedException = ExpectedException.ArgumentOutOfRangeException("IDX10716:"), - ExpectedValue = expectedValue, - Json = json, - Length = -1, - Offset = 5, - OffsetBytes = utf8BytesOffset, - OffsetLength = 5 - }); - - theoryData.Add(new Base64UrlEncoderTheoryData("Length_Zero") - { - Bytes = new byte[0], - Chars = new char[1024], - ExpectedValue = string.Empty, - Json = string.Empty, - Length = 0, - Offset = 0, - OffsetBytes = new byte[0], - OffsetLength = 0 - }); - - theoryData.Add(new Base64UrlEncoderTheoryData("Bytes_Zero") - { - Bytes = new byte[0], - Chars = new char[1024], - ExpectedValue = string.Empty, - Json = string.Empty, - Length = 0, - Offset = 0, - OffsetBytes = new byte[0], - OffsetLength = 0 - }); - - theoryData.Add(new Base64UrlEncoderTheoryData("Input_LessThan_Offset_Length") - { - Bytes = utf8Bytes, - Chars = new char[1024], - ExpectedException = ExpectedException.ArgumentOutOfRangeException("IDX10717:"), - ExpectedValue = expectedValue, - Json = json, - Length = count, - Offset = utf8BytesOffset.Length, - OffsetBytes = utf8BytesOffset, - OffsetLength = utf8BytesOffset.Length - }); - - theoryData.Add(new Base64UrlEncoderTheoryData("BytesNULL_Encoding") - { - Bytes = null, - Chars = new char[1024], - ExpectedException = ExpectedException.ArgumentNullException("IDX10000:"), - ExpectedValue = expectedValue, - Length = count, - Json = json, - Offset = 5, - OffsetBytes = utf8BytesOffset, - OffsetLength = count, - EncodingOnly = true - }); - - theoryData.Add(new Base64UrlEncoderTheoryData("OffsetBytesNULL_Encoding") - { - Bytes = utf8Bytes, - Chars = new char[1024], - ExpectedException = ExpectedException.ArgumentNullException("IDX10000:"), - ExpectedValue = expectedValue, - Json = json, - Length = count, - Offset = 5, - OffsetBytes = null, - OffsetLength = count, - EncodingOnly = true - }); - - theoryData.Add(new Base64UrlEncoderTheoryData("Offset_Negative_Encoding") - { - Bytes = utf8Bytes, - Chars = new char[1024], - ExpectedException = ExpectedException.ArgumentOutOfRangeException("IDX10716:"), - ExpectedValue = expectedValue, - Json = json, - Length = count, - Offset = -1, - OffsetBytes = utf8BytesOffset, - OffsetLength = count, - EncodingOnly = true - }); - - theoryData.Add(new Base64UrlEncoderTheoryData("Length_Negative_Encoding") - { - Bytes = utf8Bytes, - Chars = new char[1024], - ExpectedException = ExpectedException.ArgumentOutOfRangeException("IDX10716:"), - ExpectedValue = expectedValue, - Json = json, - Length = -1, - Offset = 5, - OffsetBytes = utf8BytesOffset, - OffsetLength = -1, - EncodingOnly = true - }); - - theoryData.Add(new Base64UrlEncoderTheoryData("Length_Zero_Encoding") - { - Bytes = new byte[0], - Chars = new char[1024], - ExpectedValue = string.Empty, - Json = string.Empty, - Length = 0, - Offset = 0, - OffsetBytes = new byte[0], - OffsetLength = 0 - }); - - theoryData.Add(new Base64UrlEncoderTheoryData("Bytes_Zero_Encoding") - { - Bytes = new byte[0], - Chars = new char[1024], - ExpectedValue = string.Empty, - Json = string.Empty, - Length = 0, - Offset = 0, - OffsetBytes = new byte[0], - OffsetLength = 0, - EncodingOnly = true - }); - - - return theoryData; - } - } - - public class Base64UrlEncoderTheoryData : TheoryDataBase - { - public Base64UrlEncoderTheoryData(string testId) : base(testId) { } - - public byte[] Bytes { get; set; } - - public char[] Chars { get; set; } - - public string ExpectedValue { get; set; } - - public string Json { get; set; } - - public int Length { get; set; } - - public int Offset { get; set; } - - public byte[] OffsetBytes { get; set; } - - public int OffsetLength { get; set; } - - public bool EncodingOnly { get; set; } - } - [Fact] - public void ValidateAndGetOutputSizeTests() + public void ValidateTests() { string input = string.Empty; - Assert.Throws(() => Base64UrlEncoding.ValidateAndGetOutputSize(input.AsSpan(), 0, 0)); - Assert.Throws(() => Base64UrlEncoding.ValidateAndGetOutputSize("abc".AsSpan(), -1, 3)); - Assert.Throws(() => Base64UrlEncoding.ValidateAndGetOutputSize("abc".AsSpan(), 0, -1)); - Assert.Throws(() => Base64UrlEncoding.ValidateAndGetOutputSize("abc".AsSpan(), 0, 4)); - Assert.Throws(() => Base64UrlEncoding.ValidateAndGetOutputSize("abcde".AsSpan(), 0, 5)); + Assert.Throws(() => Base64UrlEncoding.Validate(input.AsSpan(), 0, 0)); + Assert.Throws(() => Base64UrlEncoding.Validate("abc".AsSpan(), -1, 3)); + Assert.Throws(() => Base64UrlEncoding.Validate("abc".AsSpan(), 0, -1)); + Assert.Throws(() => Base64UrlEncoding.Validate("abc".AsSpan(), 0, 4)); + Assert.Throws(() => Base64UrlEncoding.Validate("abcde".AsSpan(), 0, 5)); - int actualOutputSize = Base64UrlEncoding.ValidateAndGetOutputSize("abc".AsSpan(), 0, 0); + int actualOutputSize = Base64UrlEncoding.Validate("abc".AsSpan(), 0, 0).Length; Assert.Equal(0, actualOutputSize); - actualOutputSize = Base64UrlEncoding.ValidateAndGetOutputSize("abcd".AsSpan(), 0, 4); - Assert.Equal(3, actualOutputSize); + actualOutputSize = Base64UrlEncoding.Validate("abcd".AsSpan(), 0, 4).Length; + Assert.Equal(4, actualOutputSize); - actualOutputSize = Base64UrlEncoding.ValidateAndGetOutputSize("abc=".AsSpan(), 0, 4); - Assert.Equal(2, actualOutputSize); + actualOutputSize = Base64UrlEncoding.Validate("abc=".AsSpan(), 0, 4).Length; + Assert.Equal(4, actualOutputSize); } } }