|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// To suppress warnings about the code styles the runtime team uses in the functions from them.
// <auto-generated />
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Microsoft.CodeAnalysis.Shared.Utilities;
// From https://github.com/dotnet/runtime/blob/6927fea7b4bca1dc2cea7a0afba0373c1303cedc/src/libraries/System.Private.CoreLib/src/System/Convert.cs#L2659
internal static class Base64Utilities
{
// Pre-computing this table using a custom string(s_characters) and GenerateDecodingMapAndVerify (found in tests)
private static ReadOnlySpan<sbyte> 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, 62, -1, -1, -1, 63, // 62 is placed at index 43 (for +), 63 at index 47 (for /)
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, // 52-61 are placed at index 48-57 (for 0-9), 64 at index 61 (for =)
-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, -1, // 0-25 are placed at index 65-90 (for A-Z)
-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, // 26-51 are placed at index 97-122 (for a-z)
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // Bytes over 122 ('z') are invalid and cannot be decoded
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // Hence, padding the map with 255, which indicates invalid input
-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 const byte EncodingPad = (byte)'='; // '=', for padding
private static bool IsSpace(this char c) => c == ' ' || c == '\t' || c == '\r' || c == '\n';
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteThreeLowOrderBytes(ref byte destination, int value)
{
destination = (byte)(value >> 16);
Unsafe.Add(ref destination, 1) = (byte)(value >> 8);
Unsafe.Add(ref destination, 2) = (byte)value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int Decode(ref char encodedChars, ref sbyte decodingMap)
{
int i0 = encodedChars;
int i1 = Unsafe.Add(ref encodedChars, 1);
int i2 = Unsafe.Add(ref encodedChars, 2);
int i3 = Unsafe.Add(ref encodedChars, 3);
if (((i0 | i1 | i2 | i3) & 0xffffff00) != 0)
return -1; // One or more chars falls outside the 00..ff range. This cannot be a valid Base64 character.
i0 = Unsafe.Add(ref decodingMap, i0);
i1 = Unsafe.Add(ref decodingMap, i1);
i2 = Unsafe.Add(ref decodingMap, i2);
i3 = Unsafe.Add(ref decodingMap, i3);
i0 <<= 18;
i1 <<= 12;
i2 <<= 6;
i0 |= i3;
i1 |= i2;
i0 |= i1;
return i0;
}
public static bool TryFromBase64Chars(ReadOnlySpan<char> chars, Span<byte> bytes, out int bytesWritten)
{
// This is actually local to one of the nested blocks but is being declared at the top as we don't want multiple stackallocs
// for each iteraton of the loop.
Span<char> tempBuffer = stackalloc char[4]; // Note: The tempBuffer size could be made larger than 4 but the size must be a multiple of 4.
bytesWritten = 0;
while (chars.Length != 0)
{
// Attempt to decode a segment that doesn't contain whitespace.
bool complete = TryDecodeFromUtf16(chars, bytes, out int consumedInThisIteration, out int bytesWrittenInThisIteration);
bytesWritten += bytesWrittenInThisIteration;
if (complete)
return true;
chars = chars.Slice(consumedInThisIteration);
bytes = bytes.Slice(bytesWrittenInThisIteration);
Debug.Assert(chars.Length != 0); // If TryDecodeFromUtf16() consumed the entire buffer, it could not have returned false.
if (chars[0].IsSpace())
{
// If we got here, the very first character not consumed was a whitespace. We can skip past any consecutive whitespace, then continue decoding.
int indexOfFirstNonSpace = 1;
while (true)
{
if (indexOfFirstNonSpace == chars.Length)
break;
if (!chars[indexOfFirstNonSpace].IsSpace())
break;
indexOfFirstNonSpace++;
}
chars = chars.Slice(indexOfFirstNonSpace);
if ((bytesWrittenInThisIteration % 3) != 0 && chars.Length != 0)
{
// If we got here, the last successfully decoded block encountered an end-marker, yet we have trailing non-whitespace characters.
// That is not allowed.
bytesWritten = default;
return false;
}
// We now loop again to decode the next run of non-space characters.
}
else
{
Debug.Assert(chars.Length != 0 && !chars[0].IsSpace());
// If we got here, it is possible that there is whitespace that occurred in the middle of a 4-byte chunk. That is, we still have
// up to three Base64 characters that were left undecoded by the fast-path helper because they didn't form a complete 4-byte chunk.
// This is hopefully the rare case (multiline-formatted base64 message with a non-space character width that's not a multiple of 4.)
// We'll filter out whitespace and copy the remaining characters into a temporary buffer.
CopyToTempBufferWithoutWhiteSpace(chars, tempBuffer, out int consumedFromChars, out int charsWritten);
if ((charsWritten & 0x3) != 0)
{
// Even after stripping out whitespace, the number of characters is not divisible by 4. This cannot be a legal Base64 string.
bytesWritten = default;
return false;
}
tempBuffer = tempBuffer.Slice(0, charsWritten);
if (!TryDecodeFromUtf16(tempBuffer, bytes, out int consumedFromTempBuffer, out int bytesWrittenFromTempBuffer))
{
bytesWritten = default;
return false;
}
bytesWritten += bytesWrittenFromTempBuffer;
chars = chars.Slice(consumedFromChars);
bytes = bytes.Slice(bytesWrittenFromTempBuffer);
if ((bytesWrittenFromTempBuffer % 3) != 0)
{
// If we got here, this decode contained one or more padding characters ('='). We can accept trailing whitespace after this
// but nothing else.
for (int i = 0; i < chars.Length; i++)
{
if (!chars[i].IsSpace())
{
bytesWritten = default;
return false;
}
}
return true;
}
// We now loop again to decode the next run of non-space characters.
}
}
return true;
}
private static void CopyToTempBufferWithoutWhiteSpace(ReadOnlySpan<char> chars, Span<char> tempBuffer, out int consumed, out int charsWritten)
{
Debug.Assert(tempBuffer.Length != 0); // We only bound-check after writing a character to the tempBuffer.
charsWritten = 0;
for (int i = 0; i < chars.Length; i++)
{
char c = chars[i];
if (!c.IsSpace())
{
tempBuffer[charsWritten++] = c;
if (charsWritten == tempBuffer.Length)
{
consumed = i + 1;
return;
}
}
}
consumed = chars.Length;
}
private static bool TryDecodeFromUtf16(ReadOnlySpan<char> utf16, Span<byte> bytes, out int consumed, out int written)
{
ref char srcChars = ref MemoryMarshal.GetReference(utf16);
ref byte destBytes = ref MemoryMarshal.GetReference(bytes);
int srcLength = utf16.Length & ~0x3; // only decode input up to the closest multiple of 4.
int destLength = bytes.Length;
int sourceIndex = 0;
int destIndex = 0;
if (utf16.Length == 0)
goto DoneExit;
ref sbyte decodingMap = ref MemoryMarshal.GetReference(DecodingMap);
// Last bytes could have padding characters, so process them separately and treat them as valid.
const int skipLastChunk = 4;
int maxSrcLength;
if (destLength >= (srcLength >> 2) * 3)
{
maxSrcLength = srcLength - skipLastChunk;
}
else
{
// This should never overflow since destLength here is less than int.MaxValue / 4 * 3 (i.e. 1610612733)
// Therefore, (destLength / 3) * 4 will always be less than 2147483641
maxSrcLength = (destLength / 3) * 4;
}
while (sourceIndex < maxSrcLength)
{
int result = Decode(ref Unsafe.Add(ref srcChars, sourceIndex), ref decodingMap);
if (result < 0)
goto InvalidExit;
WriteThreeLowOrderBytes(ref Unsafe.Add(ref destBytes, destIndex), result);
destIndex += 3;
sourceIndex += 4;
}
if (maxSrcLength != srcLength - skipLastChunk)
goto InvalidExit;
// If input is less than 4 bytes, srcLength == sourceIndex == 0
// If input is not a multiple of 4, sourceIndex == srcLength != 0
if (sourceIndex == srcLength)
{
goto InvalidExit;
}
int i0 = Unsafe.Add(ref srcChars, srcLength - 4);
int i1 = Unsafe.Add(ref srcChars, srcLength - 3);
int i2 = Unsafe.Add(ref srcChars, srcLength - 2);
int i3 = Unsafe.Add(ref srcChars, srcLength - 1);
if (((i0 | i1 | i2 | i3) & 0xffffff00) != 0)
goto InvalidExit;
i0 = Unsafe.Add(ref decodingMap, i0);
i1 = Unsafe.Add(ref decodingMap, i1);
i0 <<= 18;
i1 <<= 12;
i0 |= i1;
if (i3 != EncodingPad)
{
i2 = Unsafe.Add(ref decodingMap, i2);
i3 = Unsafe.Add(ref decodingMap, i3);
i2 <<= 6;
i0 |= i3;
i0 |= i2;
if (i0 < 0)
goto InvalidExit;
if (destIndex > destLength - 3)
goto InvalidExit;
WriteThreeLowOrderBytes(ref Unsafe.Add(ref destBytes, destIndex), i0);
destIndex += 3;
}
else if (i2 != EncodingPad)
{
i2 = Unsafe.Add(ref decodingMap, i2);
i2 <<= 6;
i0 |= i2;
if (i0 < 0)
goto InvalidExit;
if (destIndex > destLength - 2)
goto InvalidExit;
Unsafe.Add(ref destBytes, destIndex) = (byte)(i0 >> 16);
Unsafe.Add(ref destBytes, destIndex + 1) = (byte)(i0 >> 8);
destIndex += 2;
}
else
{
if (i0 < 0)
goto InvalidExit;
if (destIndex > destLength - 1)
goto InvalidExit;
Unsafe.Add(ref destBytes, destIndex) = (byte)(i0 >> 16);
destIndex++;
}
sourceIndex += 4;
if (srcLength != utf16.Length)
goto InvalidExit;
DoneExit:
consumed = sourceIndex;
written = destIndex;
return true;
InvalidExit:
consumed = sourceIndex;
written = destIndex;
Debug.Assert((consumed % 4) == 0);
return false;
}
public static bool TryGetDecodedLength(string encodedString, out int decodedLength)
{
decodedLength = -1;
// Base64 encoded strings may end with 0, 1, or 2 padding (=) characters
var padding = GetPadding(encodedString);
if (padding > 2)
return false;
decodedLength = (encodedString.Length * 3) / 4 - padding;
return true;
}
private static int GetPadding(string attributeData)
{
var padding = 0;
var index = attributeData.Length - 1;
while (index >= 0 && attributeData[index] == '=')
{
padding++;
index--;
}
return padding;
}
}
|