Files
Just.Core/Core/Base32.cs
T
just e37e9b65c3
.NET Test / .NET tests (push) Successful in 1m58s
.NET Publish / publish (push) Successful in 58s
documentation update
2026-07-10 22:16:20 +04:00

249 lines
8.0 KiB
C#

namespace Just.Core;
/// <summary>
/// Encoding options for <see cref="Base32.Encode(ReadOnlySpan{byte}, Base32EncodeOptions)"/>.
/// </summary>
[Flags]
public enum Base32EncodeOptions
{
/// <summary>Standard RFC 4648 encoding (uppercase with padding).</summary>
None = 0x00,
/// <summary>Use lowercase alphabet.</summary>
LowerCase = 0x01,
/// <summary>Omit padding characters.</summary>
NoPadding = 0x02,
/// <summary>Lowercase alphabet without padding (combines <see cref="LowerCase"/> | <see cref="NoPadding"/>).</summary>
LowerCaseNoPadding = 0x03,
}
/// <summary>
/// RFC 4648 Base32 encoder/decoder with span-based APIs.
/// Uses stack allocation for inputs up to <see cref="MaxBytesStack"/> bytes; heap allocation otherwise.
/// </summary>
public static class Base32
{
/// <summary>RFC 4648 Base32 alphabet (uppercase A-Z, 2-7).</summary>
public const string Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
/// <summary>Lowercase variant of the RFC 4648 alphabet.</summary>
public const string AlphabetLower = "abcdefghijklmnopqrstuvwxyz234567";
/// <summary>Padding character (=).</summary>
public const char Padding = '=';
/// <summary>Maximum input byte count that uses stack allocation instead of heap.</summary>
public const int MaxBytesStack = 250;
/// <summary>
/// Encodes a byte span into a Base32 string.
/// </summary>
/// <param name="input">The bytes to encode.</param>
/// <param name="options">Encoding options (casing, padding). Defaults to standard RFC 4648.</param>
/// <returns>The Base32-encoded string.</returns>
[Pure]
public static string Encode(ReadOnlySpan<byte> input, Base32EncodeOptions options = Base32EncodeOptions.None)
{
if (input.IsEmpty) return string.Empty;
int outLength = 8 * ((input.Length + 4) / 5);
Span<char> output = input.Length > MaxBytesStack
? new char[outLength]
: stackalloc char[outLength];
var size = Encode(input, output, options);
return new string(output[..size]);
}
[Pure]
public static int Encode(ReadOnlySpan<byte> input, Span<char> output, Base32EncodeOptions options = Base32EncodeOptions.None)
{
if (input.IsEmpty) return 0;
int outputLength = 8 * ((input.Length + 4) / 5);
if (output.Length < outputLength)
{
throw new ArgumentException("Encoded input can not fit in output span.", nameof(output));
}
output = output[..outputLength];
ReadOnlySpan<char> alphabet = (options & Base32EncodeOptions.LowerCase) == Base32EncodeOptions.LowerCase
? AlphabetLower
: Alphabet;
Span<byte> alphabetKeys = stackalloc byte[8];
int i = 0;
for (int offset = 0; offset < input.Length;)
{
alphabetKeys.Clear();
int numCharsToOutput = GetNextGroup(input, ref offset, alphabetKeys);
output[i++] = alphabet[alphabetKeys[0]];
output[i++] = alphabet[alphabetKeys[1]];
if (numCharsToOutput < 3)
{
i = FillWithPadding(output, i, numCharsToOutput, options);
break;
}
output[i++] = alphabet[alphabetKeys[2]];
output[i++] = alphabet[alphabetKeys[3]];
if (numCharsToOutput < 5)
{
i = FillWithPadding(output, i, numCharsToOutput, options);
break;
}
output[i++] = alphabet[alphabetKeys[4]];
if (numCharsToOutput < 6)
{
i = FillWithPadding(output, i, numCharsToOutput, options);
break;
}
output[i++] = alphabet[alphabetKeys[5]];
output[i++] = alphabet[alphabetKeys[6]];
if (numCharsToOutput < 8)
{
i = FillWithPadding(output, i, numCharsToOutput, options);
break;
}
output[i++] = alphabet[alphabetKeys[7]];
}
return i;
static int FillWithPadding(Span<char> output, int i, int numCharsToOutput, Base32EncodeOptions options)
{
if ((options & Base32EncodeOptions.NoPadding) == Base32EncodeOptions.NoPadding)
{
return i;
}
var pad = 8 - numCharsToOutput;
output[i..(i+pad)].Fill(Padding);
return i + pad;
}
}
[Pure]
public static byte[] Decode(ReadOnlySpan<char> input)
{
input = input.TrimEnd(Padding);
if (input.IsEmpty) return [];
var outputLength = 5 * input.Length / 8;
Span<byte> output = outputLength > MaxBytesStack
? new byte[outputLength]
: stackalloc byte[outputLength];
var size = Decode(input, output);
return output[..size].ToArray();
}
[Pure]
public static int Decode(ReadOnlySpan<char> input, Span<byte> output)
{
input = input.TrimEnd(Padding);
var rem = input.Length % 8;
if (rem == 1 || rem == 3 || rem == 6)
{
throw new FormatException("Invalid Base32 string length.");
}
var outputLength = 5 * input.Length / 8;
if (output.Length < outputLength)
{
throw new ArgumentException("Decoded input can not fit in output span.", nameof(output));
}
output = output[..outputLength];
output.Clear();
Span<char> inputspan = outputLength > MaxBytesStack
? new char[input.Length]
: stackalloc char[input.Length];
input.ToUpperInvariant(inputspan);
int bitIndex = 0;
int inputIndex = 0;
int outputBits = 0;
int outputIndex = 0;
int bitPos;
int outBitPos;
int bits;
ReadOnlySpan<char> alphabet = Alphabet;
while (inputIndex < input.Length)
{
var byteIndex = alphabet.IndexOf(inputspan[inputIndex]);
if (byteIndex < 0)
{
throw new FormatException("Provided string contains invalid characters.");
}
bitPos = 5 - bitIndex;
outBitPos = 8 - outputBits;
bits = bitPos < outBitPos ? bitPos : outBitPos;
output[outputIndex] <<= bits;
output[outputIndex] |= (byte)(byteIndex >> (bitPos - bits));
outputBits += bits;
if (outputBits >= 8)
{
outputIndex++;
outputBits = 0;
}
bitIndex += bits;
if (bitIndex >= 5)
{
inputIndex++;
bitIndex = 0;
}
else if (inputIndex == input.Length -1)
{
if ((byteIndex & ~(0x1f << (bitPos - bits))) > 0)
{
throw new FormatException("Invalid Base32 string. Inconsistent tail bits.");
}
break;
}
}
return outputIndex;
}
// returns the number of bytes that were output
[Pure]
private static int GetNextGroup(ReadOnlySpan<byte> input, ref int offset, Span<byte> alphabetKeys)
{
var retVal = (input.Length - offset) switch
{
1 => 2,
2 => 4,
3 => 5,
4 => 7,
_ => 8,
};
uint b1 = input[offset++];
uint b2 = (offset < input.Length) ? input[offset++] : 0U;
uint b3 = (offset < input.Length) ? input[offset++] : 0U;
uint b4 = (offset < input.Length) ? input[offset++] : 0U;
uint b5 = (offset < input.Length) ? input[offset++] : 0U;
alphabetKeys[0] = (byte)(b1 >> 3);
alphabetKeys[1] = (byte)(((b1 & 0x07) << 2) | (b2 >> 6));
alphabetKeys[2] = (byte)((b2 >> 1) & 0x1f);
alphabetKeys[3] = (byte)(((b2 & 0x01) << 4) | (b3 >> 4));
alphabetKeys[4] = (byte)(((b3 & 0x0f) << 1) | (b4 >> 7));
alphabetKeys[5] = (byte)((b4 >> 2) & 0x1f);
alphabetKeys[6] = (byte)(((b4 & 0x3) << 3) | (b5 >> 5));
alphabetKeys[7] = (byte)(b5 & 0x1f);
return retVal;
}
}