Skip to content

Commit

Permalink
fix #59 - endianness inconsistency for Base32.Encode(ulong) and Decod…
Browse files Browse the repository at this point in the history
…eUInt64()
  • Loading branch information
ssg committed Sep 16, 2024
1 parent eabb9f0 commit a21f86c
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 30 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
43 changes: 33 additions & 10 deletions src/Base32.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;

namespace SimpleBase;
Expand All @@ -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<Base32> crockford = new(() => new Base32(Base32Alphabet.Crockford));
private static readonly Lazy<Base32> rfc4648 = new(() => new Base32(Base32Alphabet.Rfc4648));
private static readonly Lazy<Base32> extendedHex = new(() => new Base32(Base32Alphabet.ExtendedHex));
Expand All @@ -34,15 +36,21 @@ public sealed class Base32 : IBaseCoder, IBaseStreamCoder, INonAllocatingBaseCod
/// </summary>
/// <param name="alphabet">Alphabet to use.</param>
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
Expand Down Expand Up @@ -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--)
Expand All @@ -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);
}

/// <inheritdoc/>
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;
}

/// <summary>
Expand Down
4 changes: 2 additions & 2 deletions src/SimpleBase.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<AssemblyOriginatorKeyFile>..\SimpleBase.snk</AssemblyOriginatorKeyFile>
<DelaySign>false</DelaySign>

<PackageVersion>4.0.1</PackageVersion>
<PackageVersion>4.0.2</PackageVersion>
<DocumentationFile>SimpleBase.xml</DocumentationFile>
<PackageProjectUrl>https://github.com/ssg/SimpleBase</PackageProjectUrl>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
Expand All @@ -24,7 +24,7 @@
<PackageReleaseNotes>
<![CDATA[
## Fixes
- Fixes #58 - `Encode(long)` failing -- reported by Pascal Schwarz <@pschwarzpp>
- Fixes #59 - Base32's `Encode(ulong)` and `DecodeUInt64()` works consistently among platforms with different endianness
]]>
</PackageReleaseNotes>
</PropertyGroup>
Expand Down
82 changes: 64 additions & 18 deletions test/Base32/Rfc4648Test.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,26 +71,72 @@ public void Decode_InvalidInput_ThrowsArgumentException()
_ = Assert.Throws<ArgumentException>(() => 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]
Expand Down

0 comments on commit a21f86c

Please sign in to comment.