using System; using System.Globalization; using System.Text; namespace SpotifyAPI.Web { internal class Base64Util { internal const string WebEncoders_InvalidCountOffsetOrLength = "Invalid {0}, {1} or {2} length."; #if NET8_0_OR_GREATER internal static CompositeFormat WebEncoders_MalformedInput = CompositeFormat.Parse("Malformed input: {0} is an invalid input length."); #else internal const string WebEncoders_MalformedInput = "Malformed input: {0} is an invalid input length."; #endif public static string UrlEncode(byte[] input) { #if NET8_0_OR_GREATER ArgumentNullException.ThrowIfNull(input); #else if (input == null) { throw new ArgumentNullException(nameof(input)); } #endif // Special-case empty input if (input.Length == 0) { return string.Empty; } var buffer = new char[GetArraySizeRequiredToEncode(input.Length)]; var numBase64Chars = Convert.ToBase64CharArray(input, 0, input.Length, buffer, 0); // Fix up '+' -> '-' and '/' -> '_'. Drop padding characters. for (var i = 0; i < numBase64Chars; i++) { var ch = buffer[i]; if (ch == '+') { buffer[i] = '-'; } else if (ch == '/') { buffer[i] = '_'; } else if (ch == '=') { return new string(buffer, startIndex: 0, length: i); } } return new string(buffer, startIndex: 0, length: numBase64Chars); } public static byte[] UrlDecode(string input) { #if NET8_0_OR_GREATER ArgumentNullException.ThrowIfNull(input); #else if (input == null) { throw new ArgumentNullException(nameof(input)); } #endif var buffer = new char[GetArraySizeRequiredToDecode(input.Length)]; // Assumption: input is base64url encoded without padding and contains no whitespace. var paddingCharsToAdd = GetNumBase64PaddingCharsToAddForDecode(input.Length); var arraySizeRequired = checked(input.Length + paddingCharsToAdd); // 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 GetArraySizeRequiredToEncode(int count) { var numWholeOrPartialInputBlocks = checked(count + 2) / 3; return checked(numWholeOrPartialInputBlocks * 4); } private static int GetArraySizeRequiredToDecode(int count) { #if NET8_0_OR_GREATER ArgumentOutOfRangeException.ThrowIfNegative(count); #else if (count < 0) { throw new ArgumentOutOfRangeException(nameof(count)); } #endif if (count == 0) { return 0; } var numPaddingCharsToAdd = GetNumBase64PaddingCharsToAddForDecode(count); return checked(count + numPaddingCharsToAdd); } private static int GetNumBase64PaddingCharsToAddForDecode(int inputLength) { return (inputLength % 4) switch { 0 => 0, 2 => 2, 3 => 1, _ => throw new FormatException( string.Format( CultureInfo.CurrentCulture, WebEncoders_MalformedInput, inputLength)), }; } } }