From 312219d42fecacd113298c8c475dd63caa8656f0 Mon Sep 17 00:00:00 2001 From: just Date: Wed, 6 Aug 2025 22:02:38 +0400 Subject: [PATCH] minor refactoring and tests --- Core.Tests/Base64UrlConversions/Decode.cs | 54 +++++++++- Core.Tests/Base64UrlConversions/Encode.cs | 21 +++- Core/Base32.cs | 56 +++++++---- Core/Base64Url.cs | 117 ++++++++++++++-------- Core/Collections/ImmutableSequence.cs | 17 +--- 5 files changed, 189 insertions(+), 76 deletions(-) diff --git a/Core.Tests/Base64UrlConversions/Decode.cs b/Core.Tests/Base64UrlConversions/Decode.cs index d872e22..6baf0ea 100644 --- a/Core.Tests/Base64UrlConversions/Decode.cs +++ b/Core.Tests/Base64UrlConversions/Decode.cs @@ -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(); } + [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(); + } + [Theory] [InlineData(null)] [InlineData("")] - public void WhenCalledWithNullString_ShouldReturnEmptyArray(string testString) + public void WhenCalledWithNullString_ShouldReturnEmptyArray(string? testString) { Base64Url.Decode(testString).Should().BeEmpty(); } diff --git a/Core.Tests/Base64UrlConversions/Encode.cs b/Core.Tests/Base64UrlConversions/Encode.cs index 4a56122..9b9fa25 100644 --- a/Core.Tests/Base64UrlConversions/Encode.cs +++ b/Core.Tests/Base64UrlConversions/Encode.cs @@ -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']; diff --git a/Core/Base32.cs b/Core/Base32.cs index 2a09384..816b961 100644 --- a/Core/Base32.cs +++ b/Core/Base32.cs @@ -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 alphabet = Alphabet; + Span 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 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 input, ref int offset, Span 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; } diff --git a/Core/Base64Url.cs b/Core/Base64Url.cs index e94a47d..f732d15 100644 --- a/Core/Base64Url.cs +++ b/Core/Base64Url.cs @@ -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 value) + { + ArgumentOutOfRangeException.ThrowIfNotEqual(value.Length, 11); + + Span longBytes = stackalloc byte[8]; + Span 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(longBytes); + } + [Pure] public static Guid DecodeGuid(ReadOnlySpan value) { ArgumentOutOfRangeException.ThrowIfNotEqual(value.Length, 22); Span guidBytes = stackalloc byte[16]; Span 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 input) { if (input.IsEmpty) return []; - + Span 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 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 longBytes = stackalloc byte[8]; + MemoryMarshal.Write(longBytes, id); + + Span 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 guidBytes = stackalloc byte[16]; @@ -69,15 +91,7 @@ public static class Base64Url Span 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 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 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 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 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; } } diff --git a/Core/Collections/ImmutableSequence.cs b/Core/Collections/ImmutableSequence.cs index e69972c..1fe1463 100644 --- a/Core/Collections/ImmutableSequence.cs +++ b/Core/Collections/ImmutableSequence.cs @@ -8,18 +8,12 @@ public class ImmutableSequence : IReadOnlyList, IEquatable> { - private static readonly int InitialHash = typeof(ImmutableSequence).GetHashCode(); private static readonly Func CompareItem = EqualityComparer.Default.Equals; private readonly ImmutableList _values; + public ImmutableSequence() => _values = []; public ImmutableSequence(ImmutableList values) => _values = values; - public ImmutableSequence() : this(ImmutableArray.Empty) - { - } - public ImmutableSequence(IEnumerable values) - { - _values = [..values]; - } + public ImmutableSequence(IEnumerable values) => _values = [..values]; public ImmutableSequence(ReadOnlySpan values) : this(ImmutableList.Create(values)) { } @@ -37,10 +31,10 @@ public class ImmutableSequence : } } - protected virtual ImmutableSequence ConstructNew(ImmutableList values) => new(values); + protected virtual ImmutableSequence ConstructNew(ImmutableList values) => [..values]; - public ImmutableSequence Add(T value) => ConstructNew([.._values, value]); - public ImmutableSequence AddFront(T value) => ConstructNew([value, .._values]); + public ImmutableSequence Add(T value) => ConstructNew(_values.Add(value)); + public ImmutableSequence AddFront(T value) => ConstructNew(_values.Insert(0, value)); public ImmutableList.Enumerator GetEnumerator() => _values.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_values).GetEnumerator(); @@ -75,7 +69,6 @@ public class ImmutableSequence : public override int GetHashCode() { HashCode hash = new(); - hash.Add(InitialHash); foreach (var value in _values) {