From a21f86ca9db02f5f61e6249d0188b40b9b6f0c1d Mon Sep 17 00:00:00 2001 From: Sedat Kapanoglu Date: Mon, 16 Sep 2024 07:31:34 -0700 Subject: [PATCH] fix #59 - endianness inconsistency for Base32.Encode(ulong) and DecodeUInt64() --- CHANGELOG.md | 5 +++ README.md | 1 + src/Base32.cs | 43 +++++++++++++++----- src/SimpleBase.csproj | 4 +- test/Base32/Rfc4648Test.cs | 82 +++++++++++++++++++++++++++++--------- 5 files changed, 105 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c3d5a5..90385a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# 4.0.2 + +## Fixes +- Fixes #59 - Base32's `Encode(ulong)` and `DecodeUInt64()` works consistently among platforms with different endianness + # 4.0.1 ## Fixes diff --git a/README.md b/README.md index 442455d..023ade0 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ Features - One-shot memory buffer based APIs for simple use cases. - Stream-based async APIs for more advanced scenarios. - Lightweight: No dependencies. + - Support for big-endian CPUs like IBM s390x (zArchitecture). - Thread-safe. - Simple to use. diff --git a/src/Base32.cs b/src/Base32.cs index 3ffe1d1..6beb82c 100644 --- a/src/Base32.cs +++ b/src/Base32.cs @@ -6,7 +6,6 @@ using System; using System.IO; using System.Linq; -using System.Reflection; using System.Threading.Tasks; namespace SimpleBase; @@ -20,6 +19,9 @@ public sealed class Base32 : IBaseCoder, IBaseStreamCoder, INonAllocatingBaseCod private const int bitsPerByte = 8; private const int bitsPerChar = 5; + // this is an instance variable to allow unit tests to test this behavior + internal readonly bool IsBigEndian; + private static readonly Lazy crockford = new(() => new Base32(Base32Alphabet.Crockford)); private static readonly Lazy rfc4648 = new(() => new Base32(Base32Alphabet.Rfc4648)); private static readonly Lazy extendedHex = new(() => new Base32(Base32Alphabet.ExtendedHex)); @@ -34,15 +36,21 @@ public sealed class Base32 : IBaseCoder, IBaseStreamCoder, INonAllocatingBaseCod /// /// Alphabet to use. public Base32(Base32Alphabet alphabet) + : this(alphabet, !BitConverter.IsLittleEndian) + { + } + + internal Base32(Base32Alphabet alphabet, bool isBigEndian) { if (alphabet.PaddingPosition != PaddingPosition.End) { throw new ArgumentException( - "Only alphabets with paddings at the end are supported by this implementation", + "Only encoding alphabets with paddings at the end are supported by this implementation", nameof(alphabet)); } Alphabet = alphabet; + IsBigEndian = isBigEndian; } private enum DecodeResult @@ -129,15 +137,14 @@ public string Encode(ulong number) // skip zeroes for encoding int i; - bool bigEndian = !BitConverter.IsLittleEndian; - if (bigEndian) + if (IsBigEndian) { for (i = 0; buffer[i] == 0 && i < numBytes; i++) { } - var span = buffer.AsSpan(); + var span = buffer.AsSpan()[i..]; span.Reverse(); // so the encoding is consistent between systems with different endianness - return Encode(buffer.AsSpan()[i..]); + return Encode(span); } for (i = numBytes - 1; buffer[i] == 0 && i > 0; i--) @@ -150,15 +157,31 @@ public string Encode(ulong number) public ulong DecodeUInt64(string text) { var buffer = Decode(text); - return buffer.Length <= sizeof(ulong) - ? BitConverter.ToUInt64(buffer) - : throw new InvalidOperationException("Decoded text is too long to fit in a buffer"); + if (buffer.Length > sizeof(ulong)) + { + throw new InvalidOperationException("Decoded text is too long to fit in a buffer"); + } + + var span = buffer.AsSpan(); + var newSpan = new byte[sizeof(ulong)].AsSpan(); + span.CopyTo(newSpan); + if (IsBigEndian) + { + newSpan.Reverse(); + } + + return BitConverter.ToUInt64(newSpan); } /// public long DecodeInt64(string text) { - return (long)DecodeUInt64(text); + var result = DecodeUInt64(text); + if (result > long.MaxValue) + { + throw new ArgumentOutOfRangeException("Decoded buffer is out of Int64 range"); + } + return (long)result; } /// diff --git a/src/SimpleBase.csproj b/src/SimpleBase.csproj index 2fff0a2..06ae28b 100644 --- a/src/SimpleBase.csproj +++ b/src/SimpleBase.csproj @@ -12,7 +12,7 @@ ..\SimpleBase.snk false - 4.0.1 + 4.0.2 SimpleBase.xml https://github.com/ssg/SimpleBase Apache-2.0 @@ -24,7 +24,7 @@ +- Fixes #59 - Base32's `Encode(ulong)` and `DecodeUInt64()` works consistently among platforms with different endianness ]]> diff --git a/test/Base32/Rfc4648Test.cs b/test/Base32/Rfc4648Test.cs index 73268e5..95aa192 100644 --- a/test/Base32/Rfc4648Test.cs +++ b/test/Base32/Rfc4648Test.cs @@ -71,26 +71,72 @@ public void Decode_InvalidInput_ThrowsArgumentException() _ = Assert.Throws(() => Base32.Rfc4648.Decode("[];',m.")); } + private static readonly TestCaseData[] ulongTestCases = + [ + new TestCaseData(0UL, "AA"), + new TestCaseData(0x0000000000000011UL, "CE"), + new TestCaseData(0x0000000000001122UL, "EIIQ"), + new TestCaseData(0x0000000000112233UL, "GMRBC"), + new TestCaseData(0x0000000011223344UL, "IQZSEEI"), + new TestCaseData(0x0000001122334455UL, "KVCDGIQR"), + new TestCaseData(0x0000112233445566UL, "MZKUIMZCCE"), + new TestCaseData(0x0011223344556677UL, "O5TFKRBTEIIQ"), + new TestCaseData(0x1122334455667788UL, "RB3WMVKEGMRBC"), + new TestCaseData(0x1100000000000000UL, "AAAAAAAAAAABC"), + new TestCaseData(0x1122000000000000UL, "AAAAAAAAAARBC"), + new TestCaseData(0x1122330000000000UL, "AAAAAAAAGMRBC"), + new TestCaseData(0x1122334400000000UL, "AAAAAACEGMRBC"), + new TestCaseData(0x1122334455000000UL, "AAAAAVKEGMRBC"), + new TestCaseData(0x1122334455660000UL, "AAAGMVKEGMRBC"), + new TestCaseData(0x1122334455667700UL, "AB3WMVKEGMRBC"), + ]; + + [Test] + [TestCaseSource(nameof(ulongTestCases))] + public void Encode_ulong_ReturnsExpectedValues(ulong number, string expectedOutput) + { + Assert.That(Base32.Rfc4648.Encode(number), Is.EqualTo(expectedOutput)); + } + + [Test] + [TestCaseSource(nameof(ulongTestCases))] + public void Encode_BigEndian_ulong_ReturnsExpectedValues(ulong number, string expectedOutput) + { + if (!BitConverter.IsLittleEndian) + { + throw new InvalidOperationException("big endian tests are only supported on little endian archs"); + } + number = reverseBytes(number); + + var bigEndian = new Base32(Base32Alphabet.Rfc4648, isBigEndian: true); + Assert.That(bigEndian.Encode(number), Is.EqualTo(expectedOutput)); + } + + private static ulong reverseBytes(ulong number) + { + var span = BitConverter.GetBytes(number).AsSpan(); + span.Reverse(); + return BitConverter.ToUInt64(span); + } + + [Test] + [TestCaseSource(nameof(ulongTestCases))] + public void DecodeUInt64_ReturnsExpectedValues(ulong expectedNumber, string input) + { + Assert.That(Base32.Rfc4648.DecodeUInt64(input), Is.EqualTo(expectedNumber)); + } + [Test] - [TestCase(0, ExpectedResult = "AA")] - [TestCase(0x0000000000000011, ExpectedResult = "CE")] - [TestCase(0x0000000000001122, ExpectedResult = "EIIQ")] - [TestCase(0x0000000000112233, ExpectedResult = "GMRBC")] - [TestCase(0x0000000011223344, ExpectedResult = "IQZSEEI")] - [TestCase(0x0000001122334455, ExpectedResult = "KVCDGIQR")] - [TestCase(0x0000112233445566, ExpectedResult = "MZKUIMZCCE")] - [TestCase(0x0011223344556677, ExpectedResult = "O5TFKRBTEIIQ")] - [TestCase(0x1122334455667788, ExpectedResult = "RB3WMVKEGMRBC")] - [TestCase(0x1100000000000000, ExpectedResult = "AAAAAAAAAAABC")] - [TestCase(0x1122000000000000, ExpectedResult = "AAAAAAAAAARBC")] - [TestCase(0x1122330000000000, ExpectedResult = "AAAAAAAAGMRBC")] - [TestCase(0x1122334400000000, ExpectedResult = "AAAAAACEGMRBC")] - [TestCase(0x1122334455000000, ExpectedResult = "AAAAAVKEGMRBC")] - [TestCase(0x1122334455660000, ExpectedResult = "AAAGMVKEGMRBC")] - [TestCase(0x1122334455667700, ExpectedResult = "AB3WMVKEGMRBC")] - public string Encode_long_ReturnsExpectedValues(long number) + [TestCaseSource(nameof(ulongTestCases))] + public void DecodeUInt64_BigEndian_ReturnsExpectedValues(ulong expectedNumber, string input) { - return Base32.Rfc4648.Encode(number); + if (!BitConverter.IsLittleEndian) + { + throw new InvalidOperationException("big endian tests are only supported on little endian archs"); + } + expectedNumber = reverseBytes(expectedNumber); + var bigEndian = new Base32(Base32Alphabet.Rfc4648, isBigEndian: true); + Assert.That(bigEndian.DecodeUInt64(input), Is.EqualTo(expectedNumber)); } [Test]