minor refactoring and tests
This commit is contained in:
@@ -7,7 +7,7 @@ public class Decode
|
||||
[InlineData(554121)]
|
||||
[InlineData(100454567)]
|
||||
[InlineData(3210589)]
|
||||
public void WhenEncodedToString_ShouldBeDecodedToTheSameByteArray(int seed)
|
||||
public void WhenBytesEncodedToString_ShouldBeDecodedToTheSameByteArray(int seed)
|
||||
{
|
||||
var rng = new Random(seed);
|
||||
|
||||
@@ -23,6 +23,43 @@ public class Decode
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(72121)]
|
||||
[InlineData(554121)]
|
||||
[InlineData(100454567)]
|
||||
[InlineData(3210589)]
|
||||
public void WhenLongEncodedToString_ShouldBeDecodedToTheSameLongArray(int seed)
|
||||
{
|
||||
var rng = new Random(seed);
|
||||
|
||||
for (int i = 1; i <= 512; i++)
|
||||
{
|
||||
var testLong = rng.NextInt64();
|
||||
|
||||
var resultString = Base64Url.Encode(testLong);
|
||||
var resultLong = Base64Url.DecodeLong(resultString);
|
||||
|
||||
resultLong.Should().Be(testLong);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("RgGxr0_n1ZI", -7866126844696657594L)]
|
||||
[InlineData("sxAPfpKB5kY", 5108913293478531251L)]
|
||||
[InlineData("lO4_uitvLCg", 2894810894091415188L)]
|
||||
[InlineData("awxjIqZWz10", 6759716837247880299L)]
|
||||
[InlineData("VjNe72vug4U", -8825948697371200682L)]
|
||||
[InlineData("AAAAAAAAAAA", 0L)]
|
||||
[InlineData("__________8", -1L)]
|
||||
[InlineData("AQAAAAAAAAA", 1L)]
|
||||
[InlineData("CgAAAAAAAAA", 10L)]
|
||||
[InlineData("6AMAAAAAAAA", 1000L)]
|
||||
public void WhenCalled_ShouldReturnValidLong(string testString, long expected)
|
||||
{
|
||||
var result = Base64Url.DecodeLong(testString);
|
||||
result.Should().Be(expected);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("5QrdUxDUVkCAEGw8pvLsEw", "53dd0ae5-d410-4056-8010-6c3ca6f2ec13")]
|
||||
[InlineData("6nE2uKQ4_0ar9kpmybgkdw", "b83671ea-38a4-46ff-abf6-4a66c9b82477")]
|
||||
@@ -75,10 +112,23 @@ public class Decode
|
||||
action.Should().Throw<FormatException>();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("RgG&r0_n1ZI")]
|
||||
[InlineData("sxA fpKB5kY")]
|
||||
[InlineData("lO4_uitvL)g")]
|
||||
[InlineData("awxjIqZ^z10")]
|
||||
[InlineData("VjNe7!vug4U")]
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1806:Do not ignore method results", Justification = "Test case")]
|
||||
public void WhenCalledWithInvalidLongString_ShouldThrowFormatException(string testString)
|
||||
{
|
||||
Action action = () => Base64Url.DecodeLong(testString);
|
||||
action.Should().Throw<FormatException>();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
public void WhenCalledWithNullString_ShouldReturnEmptyArray(string testString)
|
||||
public void WhenCalledWithNullString_ShouldReturnEmptyArray(string? testString)
|
||||
{
|
||||
Base64Url.Decode(testString).Should().BeEmpty();
|
||||
}
|
||||
|
||||
@@ -15,6 +15,23 @@ public class Encode
|
||||
result.Should().Be(expected);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("RgGxr0_n1ZI", -7866126844696657594L)]
|
||||
[InlineData("sxAPfpKB5kY", 5108913293478531251L)]
|
||||
[InlineData("lO4_uitvLCg", 2894810894091415188L)]
|
||||
[InlineData("awxjIqZWz10", 6759716837247880299L)]
|
||||
[InlineData("VjNe72vug4U", -8825948697371200682L)]
|
||||
[InlineData("AAAAAAAAAAA", 0L)]
|
||||
[InlineData("__________8", -1L)]
|
||||
[InlineData("AQAAAAAAAAA", 1L)]
|
||||
[InlineData("CgAAAAAAAAA", 10L)]
|
||||
[InlineData("6AMAAAAAAAA", 1000L)]
|
||||
public void WhenCalledWithLong_ShouldReturnValidString(string expected, long testLong)
|
||||
{
|
||||
var result = Base64Url.Encode(testLong);
|
||||
result.Should().Be(expected);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("IA", new byte[]{ 0x20, })]
|
||||
[InlineData("Ag", new byte[]{ 0x02, })]
|
||||
@@ -33,7 +50,7 @@ public class Encode
|
||||
[Theory]
|
||||
[InlineData(new byte[] { })]
|
||||
[InlineData(null)]
|
||||
public void WhenCalledWithEmptyByteArray_ShouldReturnEmptyString(byte[] testArray)
|
||||
public void WhenCalledWithEmptyByteArray_ShouldReturnEmptyString(byte[]? testArray)
|
||||
{
|
||||
var actualBase32 = Base64Url.Encode(testArray);
|
||||
actualBase32.Should().Be(string.Empty);
|
||||
@@ -42,7 +59,7 @@ public class Encode
|
||||
[Theory]
|
||||
[InlineData(new byte[] { })]
|
||||
[InlineData(null)]
|
||||
public void WhenCalledWithEmptyByteArray_ShouldReturnZeroAndNotChangeOutput(byte[] testArray)
|
||||
public void WhenCalledWithEmptyByteArray_ShouldReturnZeroAndNotChangeOutput(byte[]? testArray)
|
||||
{
|
||||
char[] output = ['1', '2', '3', '4'];
|
||||
|
||||
|
||||
@@ -16,9 +16,9 @@ public static class Base32
|
||||
? stackalloc char[outLength]
|
||||
: new char[outLength];
|
||||
|
||||
_ = Encode(input, output);
|
||||
var size = Encode(input, output);
|
||||
|
||||
return new string(output);
|
||||
return new string(output[..size]);
|
||||
}
|
||||
|
||||
[Pure]
|
||||
@@ -26,20 +26,31 @@ public static class Base32
|
||||
{
|
||||
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];
|
||||
|
||||
int i = 0;
|
||||
ReadOnlySpan<char> alphabet = Alphabet;
|
||||
Span<byte> alphabetKeys = stackalloc byte[8];
|
||||
|
||||
for (int offset = 0; offset < input.Length;)
|
||||
{
|
||||
int numCharsToOutput = GetNextGroup(input, ref offset, out byte a, out byte b, out byte c, out byte d, out byte e, out byte f, out byte g, out byte h);
|
||||
alphabetKeys.Clear();
|
||||
int numCharsToOutput = GetNextGroup(input, ref offset, alphabetKeys);
|
||||
|
||||
output[i++] = (numCharsToOutput > 0) ? alphabet[a] : Padding;
|
||||
output[i++] = (numCharsToOutput > 1) ? alphabet[b] : Padding;
|
||||
output[i++] = (numCharsToOutput > 2) ? alphabet[c] : Padding;
|
||||
output[i++] = (numCharsToOutput > 3) ? alphabet[d] : Padding;
|
||||
output[i++] = (numCharsToOutput > 4) ? alphabet[e] : Padding;
|
||||
output[i++] = (numCharsToOutput > 5) ? alphabet[f] : Padding;
|
||||
output[i++] = (numCharsToOutput > 6) ? alphabet[g] : Padding;
|
||||
output[i++] = (numCharsToOutput > 7) ? alphabet[h] : Padding;
|
||||
output[i++] = (numCharsToOutput > 0) ? alphabet[alphabetKeys[0]] : Padding;
|
||||
output[i++] = (numCharsToOutput > 1) ? alphabet[alphabetKeys[1]] : Padding;
|
||||
output[i++] = (numCharsToOutput > 2) ? alphabet[alphabetKeys[2]] : Padding;
|
||||
output[i++] = (numCharsToOutput > 3) ? alphabet[alphabetKeys[3]] : Padding;
|
||||
output[i++] = (numCharsToOutput > 4) ? alphabet[alphabetKeys[4]] : Padding;
|
||||
output[i++] = (numCharsToOutput > 5) ? alphabet[alphabetKeys[5]] : Padding;
|
||||
output[i++] = (numCharsToOutput > 6) ? alphabet[alphabetKeys[6]] : Padding;
|
||||
output[i++] = (numCharsToOutput > 7) ? alphabet[alphabetKeys[7]] : Padding;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
@@ -66,6 +77,11 @@ public static class Base32
|
||||
input = input.TrimEnd(Padding);
|
||||
|
||||
var outputLength = 5 * ((input.Length + 7) / 8);
|
||||
if (output.Length < outputLength)
|
||||
{
|
||||
throw new ArgumentException("Decoded input can not fit in output span.", nameof(output));
|
||||
}
|
||||
|
||||
output = output[..outputLength];
|
||||
output.Clear();
|
||||
|
||||
@@ -119,7 +135,7 @@ public static class Base32
|
||||
|
||||
// returns the number of bytes that were output
|
||||
[Pure]
|
||||
private static int GetNextGroup(ReadOnlySpan<byte> input, ref int offset, out byte a, out byte b, out byte c, out byte d, out byte e, out byte f, out byte g, out byte h)
|
||||
private static int GetNextGroup(ReadOnlySpan<byte> input, ref int offset, Span<byte> alphabetKeys)
|
||||
{
|
||||
var retVal = (input.Length - offset) switch
|
||||
{
|
||||
@@ -135,14 +151,14 @@ public static class Base32
|
||||
uint b4 = (offset < input.Length) ? input[offset++] : 0U;
|
||||
uint b5 = (offset < input.Length) ? input[offset++] : 0U;
|
||||
|
||||
a = (byte)(b1 >> 3);
|
||||
b = (byte)(((b1 & 0x07) << 2) | (b2 >> 6));
|
||||
c = (byte)((b2 >> 1) & 0x1f);
|
||||
d = (byte)(((b2 & 0x01) << 4) | (b3 >> 4));
|
||||
e = (byte)(((b3 & 0x0f) << 1) | (b4 >> 7));
|
||||
f = (byte)((b4 >> 2) & 0x1f);
|
||||
g = (byte)(((b4 & 0x3) << 3) | (b5 >> 5));
|
||||
h = (byte)(b5 & 0x1f);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,24 +1,41 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Just.Core;
|
||||
|
||||
public static class Base64Url
|
||||
{
|
||||
private const char Padding = '=';
|
||||
|
||||
[Pure] public static long DecodeLong(ReadOnlySpan<char> value)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNotEqual(value.Length, 11);
|
||||
|
||||
Span<byte> longBytes = stackalloc byte[8];
|
||||
Span<char> chars = stackalloc char[12];
|
||||
|
||||
value.CopyTo(chars);
|
||||
chars[^1] = Padding;
|
||||
|
||||
ReplaceNonUrlChars(chars);
|
||||
|
||||
if (!Convert.TryFromBase64Chars(chars, longBytes, out int _))
|
||||
throw new FormatException("Invalid Base64 string.");
|
||||
|
||||
return MemoryMarshal.Read<long>(longBytes);
|
||||
}
|
||||
|
||||
[Pure] public static Guid DecodeGuid(ReadOnlySpan<char> value)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNotEqual(value.Length, 22);
|
||||
|
||||
Span<byte> guidBytes = stackalloc byte[16];
|
||||
Span<char> chars = stackalloc char[24];
|
||||
|
||||
value.CopyTo(chars);
|
||||
for (int i = 0; i < value.Length; i++)
|
||||
{
|
||||
switch (value[i])
|
||||
{
|
||||
case '-': chars[i] = '+'; continue;
|
||||
case '_': chars[i] = '/'; continue;
|
||||
default: continue;
|
||||
}
|
||||
}
|
||||
chars[^2..].Fill('=');
|
||||
chars[^2..].Fill(Padding);
|
||||
|
||||
ReplaceNonUrlChars(chars);
|
||||
|
||||
if (!Convert.TryFromBase64Chars(chars, guidBytes, out int _))
|
||||
throw new FormatException("Invalid Base64 string.");
|
||||
|
||||
@@ -28,9 +45,9 @@ public static class Base64Url
|
||||
[Pure] public static byte[] Decode(ReadOnlySpan<char> input)
|
||||
{
|
||||
if (input.IsEmpty) return [];
|
||||
|
||||
|
||||
Span<byte> output = stackalloc byte[3 * ((input.Length + 3) / 4)];
|
||||
|
||||
|
||||
var size = Decode(input, output);
|
||||
|
||||
return output[..size].ToArray();
|
||||
@@ -40,28 +57,33 @@ public static class Base64Url
|
||||
{
|
||||
var padding = (4 - (value.Length & 3)) & 3;
|
||||
var charlen = value.Length + padding;
|
||||
var outputBytes = charlen / 4;
|
||||
var outputBytes = 3 * (charlen / 4);
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(output.Length, outputBytes);
|
||||
Span<char> chars = stackalloc char[charlen];
|
||||
|
||||
value.CopyTo(chars);
|
||||
for (int i = 0; i < value.Length; i++)
|
||||
{
|
||||
switch (value[i])
|
||||
{
|
||||
case '-': chars[i] = '+'; continue;
|
||||
case '_': chars[i] = '/'; continue;
|
||||
default: continue;
|
||||
}
|
||||
}
|
||||
chars[^padding..].Fill('=');
|
||||
chars[^padding..].Fill(Padding);
|
||||
|
||||
ReplaceNonUrlChars(chars);
|
||||
|
||||
if (!Convert.TryFromBase64Chars(chars, output, out outputBytes))
|
||||
throw new FormatException("Invalid Base64 string.");
|
||||
|
||||
return outputBytes;
|
||||
}
|
||||
|
||||
|
||||
[Pure] public static string Encode(in long id)
|
||||
{
|
||||
Span<byte> longBytes = stackalloc byte[8];
|
||||
MemoryMarshal.Write(longBytes, id);
|
||||
|
||||
Span<char> chars = stackalloc char[12];
|
||||
Convert.TryToBase64Chars(longBytes, chars, out int _);
|
||||
ReplaceUrlChars(chars[..^1]);
|
||||
|
||||
return new string(chars[..^1]);
|
||||
}
|
||||
|
||||
[Pure] public static string Encode(in Guid id)
|
||||
{
|
||||
Span<byte> guidBytes = stackalloc byte[16];
|
||||
@@ -69,15 +91,7 @@ public static class Base64Url
|
||||
Span<char> chars = stackalloc char[24];
|
||||
Convert.TryToBase64Chars(guidBytes, chars, out int _);
|
||||
|
||||
for (int i = 0; i < chars.Length - 2; i++)
|
||||
{
|
||||
switch (chars[i])
|
||||
{
|
||||
case '+': chars[i] = '-'; continue;
|
||||
case '/': chars[i] = '_'; continue;
|
||||
default: continue;
|
||||
}
|
||||
}
|
||||
ReplaceUrlChars(chars[..^2]);
|
||||
|
||||
return new string(chars[..^2]);
|
||||
}
|
||||
@@ -86,7 +100,7 @@ public static class Base64Url
|
||||
{
|
||||
if (input.IsEmpty) return string.Empty;
|
||||
|
||||
int outLength = 8 * ((input.Length + 5) / 6);
|
||||
int outLength = 4 * ((input.Length + 2) / 3);
|
||||
Span<char> output = stackalloc char[outLength];
|
||||
|
||||
int strlen = Encode(input, output);
|
||||
@@ -97,24 +111,47 @@ public static class Base64Url
|
||||
{
|
||||
if (input.IsEmpty) return 0;
|
||||
|
||||
var charlen = 8 * ((input.Length + 5) / 6);
|
||||
var charlen = 4 * ((input.Length + 2) / 3);
|
||||
Span<char> chars = stackalloc char[charlen];
|
||||
Convert.TryToBase64Chars(input, chars, out int charsWritten);
|
||||
|
||||
int i;
|
||||
for (i = 0; i < charsWritten; i++)
|
||||
int i = ReplaceUrlChars(chars[..charsWritten]);
|
||||
chars[..i].CopyTo(output);
|
||||
|
||||
return i;
|
||||
}
|
||||
|
||||
private static int ReplaceUrlChars(Span<char> chars)
|
||||
{
|
||||
int i = 0;
|
||||
for (; i < chars.Length; i++)
|
||||
{
|
||||
switch (chars[i])
|
||||
{
|
||||
case '+': chars[i] = '-'; continue;
|
||||
case '/': chars[i] = '_'; continue;
|
||||
case '=': goto exitLoop;
|
||||
case Padding: goto break_loop;
|
||||
default: continue;
|
||||
}
|
||||
}
|
||||
exitLoop:
|
||||
chars[..i].CopyTo(output);
|
||||
break_loop:
|
||||
return i;
|
||||
}
|
||||
|
||||
private static int ReplaceNonUrlChars(Span<char> chars)
|
||||
{
|
||||
int i = 0;
|
||||
for (; i < chars.Length; i++)
|
||||
{
|
||||
switch (chars[i])
|
||||
{
|
||||
case '-': chars[i] = '+'; continue;
|
||||
case '_': chars[i] = '/'; continue;
|
||||
case Padding: goto break_loop;
|
||||
default: continue;
|
||||
}
|
||||
}
|
||||
break_loop:
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,18 +8,12 @@ public class ImmutableSequence<T> :
|
||||
IReadOnlyList<T>,
|
||||
IEquatable<ImmutableSequence<T>>
|
||||
{
|
||||
private static readonly int InitialHash = typeof(ImmutableSequence<T>).GetHashCode();
|
||||
private static readonly Func<T?, T?, bool> CompareItem = EqualityComparer<T>.Default.Equals;
|
||||
private readonly ImmutableList<T> _values;
|
||||
|
||||
public ImmutableSequence() => _values = [];
|
||||
public ImmutableSequence(ImmutableList<T> values) => _values = values;
|
||||
public ImmutableSequence() : this(ImmutableArray<T>.Empty)
|
||||
{
|
||||
}
|
||||
public ImmutableSequence(IEnumerable<T> values)
|
||||
{
|
||||
_values = [..values];
|
||||
}
|
||||
public ImmutableSequence(IEnumerable<T> values) => _values = [..values];
|
||||
public ImmutableSequence(ReadOnlySpan<T> values) : this(ImmutableList.Create(values))
|
||||
{
|
||||
}
|
||||
@@ -37,10 +31,10 @@ public class ImmutableSequence<T> :
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual ImmutableSequence<T> ConstructNew(ImmutableList<T> values) => new(values);
|
||||
protected virtual ImmutableSequence<T> ConstructNew(ImmutableList<T> values) => [..values];
|
||||
|
||||
public ImmutableSequence<T> Add(T value) => ConstructNew([.._values, value]);
|
||||
public ImmutableSequence<T> AddFront(T value) => ConstructNew([value, .._values]);
|
||||
public ImmutableSequence<T> Add(T value) => ConstructNew(_values.Add(value));
|
||||
public ImmutableSequence<T> AddFront(T value) => ConstructNew(_values.Insert(0, value));
|
||||
|
||||
public ImmutableList<T>.Enumerator GetEnumerator() => _values.GetEnumerator();
|
||||
IEnumerator<T> IEnumerable<T>.GetEnumerator() => ((IEnumerable<T>)_values).GetEnumerator();
|
||||
@@ -75,7 +69,6 @@ public class ImmutableSequence<T> :
|
||||
public override int GetHashCode()
|
||||
{
|
||||
HashCode hash = new();
|
||||
hash.Add(InitialHash);
|
||||
|
||||
foreach (var value in _values)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user