// modified from // https://github.com/dotnet/aspnetcore/blob/fd060ce8c36ffe195b9e9a69a1bbd8fb53cc6d7c/src/Shared/WebEncoders/WebEncoders.cs // 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. #if NETCOREAPP using System.Buffers; #endif using System.Diagnostics; using System.Diagnostics.CodeAnalysis; namespace Jint.Extensions; /// /// Contains utility APIs to assist with common encoding and decoding operations. /// [SuppressMessage("Maintainability", "CA1510:Use ArgumentNullException throw helper")] [SuppressMessage("Maintainability", "CA1512:Use ArgumentOutOfRangeException throw helper")] internal static class WebEncoders { private static readonly byte[] EmptyBytes = []; /// /// Decodes a base64url-encoded string. /// /// 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 input) { // Special-case empty input if (input.Length == 0) { return EmptyBytes; } // Create array large enough for the Base64 characters, not just shorter Base64-URL-encoded form. var buffer = new char[GetArraySizeRequiredToDecode(input.Length)]; return Base64UrlDecode(input, buffer); } /// /// Decodes a base64url-encoded into a byte[]. /// public static byte[] Base64UrlDecode(ReadOnlySpan input, char[] buffer) { if (input.Length == 0) { return EmptyBytes; } // Assumption: input is base64url encoded without padding and contains no whitespace. var paddingCharsToAdd = GetNumBase64PaddingCharsToAddForDecode(input.Length); var arraySizeRequired = checked(input.Length + paddingCharsToAdd); Debug.Assert(arraySizeRequired % 4 == 0, "Invariant: Array length must be a multiple of 4."); // Copy input into buffer, fixing up '-' -> '+' and '_' -> '/'. var i = 0; for (var j = 0; i < input.Length; i++, j++) { var ch = input[j]; if (ch == '-') { buffer[i] = '+'; } else if (ch == '_') { buffer[i] = '/'; } else { buffer[i] = ch; } } // Add the padding characters back. for (; paddingCharsToAdd > 0; i++, paddingCharsToAdd--) { buffer[i] = '='; } // Decode. // If the caller provided invalid base64 chars, they'll be caught here. return Convert.FromBase64CharArray(buffer, 0, arraySizeRequired); } private static int GetArraySizeRequiredToDecode(int count) { if (count == 0) { return 0; } var numPaddingCharsToAdd = GetNumBase64PaddingCharsToAddForDecode(count); return checked(count + numPaddingCharsToAdd); } /// /// Encodes using base64url encoding. /// /// The binary input to encode. /// /// The base64url-encoded form of . public static string Base64UrlEncode(byte[] input, bool omitPadding) { if (input == null) { throw new ArgumentNullException(nameof(input)); } return Base64UrlEncode(input, offset: 0, count: input.Length, omitPadding); } /// /// 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, bool omitPadding) { if (input == null) { throw new ArgumentNullException(nameof(input)); } #if NETCOREAPP return Base64UrlEncode(input.AsSpan(offset, count), omitPadding); #else // Special-case empty input if (count == 0) { return string.Empty; } var buffer = new char[GetArraySizeRequiredToEncode(count)]; var numBase64Chars = Base64UrlEncode(input, offset, buffer, outputOffset: 0, count: count, omitPadding); return new string(buffer, startIndex: 0, length: numBase64Chars); #endif } /// /// Encodes using base64url encoding. /// /// The binary input to encode. /// The offset into at which to begin encoding. /// /// Buffer to receive the base64url-encoded form of . Array must be large enough to /// hold characters and the full base64-encoded form of /// , including padding characters. /// /// /// The offset into at which to begin writing the base64url-encoded form of /// . /// /// The number of bytes from to encode. /// /// /// The number of characters written to , less any padding characters. /// public static int Base64UrlEncode(byte[] input, int offset, char[] output, int outputOffset, int count, bool omitPadding) { if (input == null) { throw new ArgumentNullException(nameof(input)); } if (output == null) { throw new ArgumentNullException(nameof(output)); } if (outputOffset < 0) { throw new ArgumentOutOfRangeException(nameof(outputOffset)); } var arraySizeRequired = GetArraySizeRequiredToEncode(count); if (output.Length - outputOffset < arraySizeRequired) { throw new ArgumentException("invalid", nameof(count)); } #if NETCOREAPP return Base64UrlEncode(input.AsSpan(offset, count), output.AsSpan(outputOffset), omitPadding); #else // Special-case empty input. if (count == 0) { return 0; } // Use base64url encoding with no padding characters. See RFC 4648, Sec. 5. // Start with default Base64 encoding. var numBase64Chars = Convert.ToBase64CharArray(input, offset, count, output, outputOffset); // Fix up '+' -> '-' and '/' -> '_'. Drop padding characters. for (var i = outputOffset; i - outputOffset < numBase64Chars; i++) { var ch = output[i]; if (ch == '+') { output[i] = '-'; } else if (ch == '/') { output[i] = '_'; } else if (omitPadding && ch == '=') { // We've reached a padding character; truncate the remainder. return i - outputOffset; } } return numBase64Chars; #endif } /// /// Get the minimum output char[] size required for encoding /// s with the method. /// /// The number of characters to encode. /// /// The minimum output char[] size required for encoding s. /// public static int GetArraySizeRequiredToEncode(int count) { var numWholeOrPartialInputBlocks = checked(count + 2) / 3; return checked(numWholeOrPartialInputBlocks * 4); } #if NETCOREAPP /// /// Encodes using base64url encoding. /// /// The binary input to encode. /// /// The base64url-encoded form of . public static string Base64UrlEncode(ReadOnlySpan input, bool omitPadding) { if (input.IsEmpty) { return string.Empty; } int bufferSize = GetArraySizeRequiredToEncode(input.Length); char[]? bufferToReturnToPool = null; Span buffer = bufferSize <= 128 ? stackalloc char[bufferSize] : bufferToReturnToPool = ArrayPool.Shared.Rent(bufferSize); var numBase64Chars = Base64UrlEncode(input, buffer, omitPadding); var base64Url = new string(buffer.Slice(0, numBase64Chars)); if (bufferToReturnToPool != null) { ArrayPool.Shared.Return(bufferToReturnToPool); } return base64Url; } private static int Base64UrlEncode(ReadOnlySpan input, Span output, bool omitPadding) { Debug.Assert(output.Length >= GetArraySizeRequiredToEncode(input.Length)); if (input.IsEmpty) { return 0; } // Use base64url encoding with no padding characters. See RFC 4648, Sec. 5. Convert.TryToBase64Chars(input, output, out int charsWritten); // Fix up '+' -> '-' and '/' -> '_'. Drop padding characters. for (var i = 0; i < charsWritten; i++) { var ch = output[i]; if (ch == '+') { output[i] = '-'; } else if (ch == '/') { output[i] = '_'; } else if (omitPadding && ch == '=') { // We've reached a padding character; truncate the remainder. return i; } } return charsWritten; } #endif private static int GetNumBase64PaddingCharsInString(string str) { // Assumption: input contains a well-formed base64 string with no whitespace. // base64 guaranteed have 0 - 2 padding characters. if (str[str.Length - 1] == '=') { if (str[str.Length - 2] == '=') { return 2; } return 1; } return 0; } private static int GetNumBase64PaddingCharsToAddForDecode(int inputLength) { switch (inputLength % 4) { case 0: return 0; case 2: return 2; case 3: return 1; default: throw new FormatException("invalid length"); } } }