Skip to content

Commit a21f86c

Browse files
committed
fix #59 - endianness inconsistency for Base32.Encode(ulong) and DecodeUInt64()
1 parent eabb9f0 commit a21f86c

File tree

5 files changed

+105
-30
lines changed

5 files changed

+105
-30
lines changed

CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
# 4.0.2
2+
3+
## Fixes
4+
- Fixes #59 - Base32's `Encode(ulong)` and `DecodeUInt64()` works consistently among platforms with different endianness
5+
16
# 4.0.1
27

38
## Fixes

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Features
2626
- One-shot memory buffer based APIs for simple use cases.
2727
- Stream-based async APIs for more advanced scenarios.
2828
- Lightweight: No dependencies.
29+
- Support for big-endian CPUs like IBM s390x (zArchitecture).
2930
- Thread-safe.
3031
- Simple to use.
3132

src/Base32.cs

+33-10
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
using System;
77
using System.IO;
88
using System.Linq;
9-
using System.Reflection;
109
using System.Threading.Tasks;
1110

1211
namespace SimpleBase;
@@ -20,6 +19,9 @@ public sealed class Base32 : IBaseCoder, IBaseStreamCoder, INonAllocatingBaseCod
2019
private const int bitsPerByte = 8;
2120
private const int bitsPerChar = 5;
2221

22+
// this is an instance variable to allow unit tests to test this behavior
23+
internal readonly bool IsBigEndian;
24+
2325
private static readonly Lazy<Base32> crockford = new(() => new Base32(Base32Alphabet.Crockford));
2426
private static readonly Lazy<Base32> rfc4648 = new(() => new Base32(Base32Alphabet.Rfc4648));
2527
private static readonly Lazy<Base32> extendedHex = new(() => new Base32(Base32Alphabet.ExtendedHex));
@@ -34,15 +36,21 @@ public sealed class Base32 : IBaseCoder, IBaseStreamCoder, INonAllocatingBaseCod
3436
/// </summary>
3537
/// <param name="alphabet">Alphabet to use.</param>
3638
public Base32(Base32Alphabet alphabet)
39+
: this(alphabet, !BitConverter.IsLittleEndian)
40+
{
41+
}
42+
43+
internal Base32(Base32Alphabet alphabet, bool isBigEndian)
3744
{
3845
if (alphabet.PaddingPosition != PaddingPosition.End)
3946
{
4047
throw new ArgumentException(
41-
"Only alphabets with paddings at the end are supported by this implementation",
48+
"Only encoding alphabets with paddings at the end are supported by this implementation",
4249
nameof(alphabet));
4350
}
4451

4552
Alphabet = alphabet;
53+
IsBigEndian = isBigEndian;
4654
}
4755

4856
private enum DecodeResult
@@ -129,15 +137,14 @@ public string Encode(ulong number)
129137

130138
// skip zeroes for encoding
131139
int i;
132-
bool bigEndian = !BitConverter.IsLittleEndian;
133-
if (bigEndian)
140+
if (IsBigEndian)
134141
{
135142
for (i = 0; buffer[i] == 0 && i < numBytes; i++)
136143
{
137144
}
138-
var span = buffer.AsSpan();
145+
var span = buffer.AsSpan()[i..];
139146
span.Reverse(); // so the encoding is consistent between systems with different endianness
140-
return Encode(buffer.AsSpan()[i..]);
147+
return Encode(span);
141148
}
142149

143150
for (i = numBytes - 1; buffer[i] == 0 && i > 0; i--)
@@ -150,15 +157,31 @@ public string Encode(ulong number)
150157
public ulong DecodeUInt64(string text)
151158
{
152159
var buffer = Decode(text);
153-
return buffer.Length <= sizeof(ulong)
154-
? BitConverter.ToUInt64(buffer)
155-
: throw new InvalidOperationException("Decoded text is too long to fit in a buffer");
160+
if (buffer.Length > sizeof(ulong))
161+
{
162+
throw new InvalidOperationException("Decoded text is too long to fit in a buffer");
163+
}
164+
165+
var span = buffer.AsSpan();
166+
var newSpan = new byte[sizeof(ulong)].AsSpan();
167+
span.CopyTo(newSpan);
168+
if (IsBigEndian)
169+
{
170+
newSpan.Reverse();
171+
}
172+
173+
return BitConverter.ToUInt64(newSpan);
156174
}
157175

158176
/// <inheritdoc/>
159177
public long DecodeInt64(string text)
160178
{
161-
return (long)DecodeUInt64(text);
179+
var result = DecodeUInt64(text);
180+
if (result > long.MaxValue)
181+
{
182+
throw new ArgumentOutOfRangeException("Decoded buffer is out of Int64 range");
183+
}
184+
return (long)result;
162185
}
163186

164187
/// <summary>

src/SimpleBase.csproj

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
<AssemblyOriginatorKeyFile>..\SimpleBase.snk</AssemblyOriginatorKeyFile>
1313
<DelaySign>false</DelaySign>
1414

15-
<PackageVersion>4.0.1</PackageVersion>
15+
<PackageVersion>4.0.2</PackageVersion>
1616
<DocumentationFile>SimpleBase.xml</DocumentationFile>
1717
<PackageProjectUrl>https://github.com/ssg/SimpleBase</PackageProjectUrl>
1818
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
@@ -24,7 +24,7 @@
2424
<PackageReleaseNotes>
2525
<![CDATA[
2626
## Fixes
27-
- Fixes #58 - `Encode(long)` failing -- reported by Pascal Schwarz <@pschwarzpp>
27+
- Fixes #59 - Base32's `Encode(ulong)` and `DecodeUInt64()` works consistently among platforms with different endianness
2828
]]>
2929
</PackageReleaseNotes>
3030
</PropertyGroup>

test/Base32/Rfc4648Test.cs

+64-18
Original file line numberDiff line numberDiff line change
@@ -71,26 +71,72 @@ public void Decode_InvalidInput_ThrowsArgumentException()
7171
_ = Assert.Throws<ArgumentException>(() => Base32.Rfc4648.Decode("[];',m."));
7272
}
7373

74+
private static readonly TestCaseData[] ulongTestCases =
75+
[
76+
new TestCaseData(0UL, "AA"),
77+
new TestCaseData(0x0000000000000011UL, "CE"),
78+
new TestCaseData(0x0000000000001122UL, "EIIQ"),
79+
new TestCaseData(0x0000000000112233UL, "GMRBC"),
80+
new TestCaseData(0x0000000011223344UL, "IQZSEEI"),
81+
new TestCaseData(0x0000001122334455UL, "KVCDGIQR"),
82+
new TestCaseData(0x0000112233445566UL, "MZKUIMZCCE"),
83+
new TestCaseData(0x0011223344556677UL, "O5TFKRBTEIIQ"),
84+
new TestCaseData(0x1122334455667788UL, "RB3WMVKEGMRBC"),
85+
new TestCaseData(0x1100000000000000UL, "AAAAAAAAAAABC"),
86+
new TestCaseData(0x1122000000000000UL, "AAAAAAAAAARBC"),
87+
new TestCaseData(0x1122330000000000UL, "AAAAAAAAGMRBC"),
88+
new TestCaseData(0x1122334400000000UL, "AAAAAACEGMRBC"),
89+
new TestCaseData(0x1122334455000000UL, "AAAAAVKEGMRBC"),
90+
new TestCaseData(0x1122334455660000UL, "AAAGMVKEGMRBC"),
91+
new TestCaseData(0x1122334455667700UL, "AB3WMVKEGMRBC"),
92+
];
93+
94+
[Test]
95+
[TestCaseSource(nameof(ulongTestCases))]
96+
public void Encode_ulong_ReturnsExpectedValues(ulong number, string expectedOutput)
97+
{
98+
Assert.That(Base32.Rfc4648.Encode(number), Is.EqualTo(expectedOutput));
99+
}
100+
101+
[Test]
102+
[TestCaseSource(nameof(ulongTestCases))]
103+
public void Encode_BigEndian_ulong_ReturnsExpectedValues(ulong number, string expectedOutput)
104+
{
105+
if (!BitConverter.IsLittleEndian)
106+
{
107+
throw new InvalidOperationException("big endian tests are only supported on little endian archs");
108+
}
109+
number = reverseBytes(number);
110+
111+
var bigEndian = new Base32(Base32Alphabet.Rfc4648, isBigEndian: true);
112+
Assert.That(bigEndian.Encode(number), Is.EqualTo(expectedOutput));
113+
}
114+
115+
private static ulong reverseBytes(ulong number)
116+
{
117+
var span = BitConverter.GetBytes(number).AsSpan();
118+
span.Reverse();
119+
return BitConverter.ToUInt64(span);
120+
}
121+
122+
[Test]
123+
[TestCaseSource(nameof(ulongTestCases))]
124+
public void DecodeUInt64_ReturnsExpectedValues(ulong expectedNumber, string input)
125+
{
126+
Assert.That(Base32.Rfc4648.DecodeUInt64(input), Is.EqualTo(expectedNumber));
127+
}
128+
74129
[Test]
75-
[TestCase(0, ExpectedResult = "AA")]
76-
[TestCase(0x0000000000000011, ExpectedResult = "CE")]
77-
[TestCase(0x0000000000001122, ExpectedResult = "EIIQ")]
78-
[TestCase(0x0000000000112233, ExpectedResult = "GMRBC")]
79-
[TestCase(0x0000000011223344, ExpectedResult = "IQZSEEI")]
80-
[TestCase(0x0000001122334455, ExpectedResult = "KVCDGIQR")]
81-
[TestCase(0x0000112233445566, ExpectedResult = "MZKUIMZCCE")]
82-
[TestCase(0x0011223344556677, ExpectedResult = "O5TFKRBTEIIQ")]
83-
[TestCase(0x1122334455667788, ExpectedResult = "RB3WMVKEGMRBC")]
84-
[TestCase(0x1100000000000000, ExpectedResult = "AAAAAAAAAAABC")]
85-
[TestCase(0x1122000000000000, ExpectedResult = "AAAAAAAAAARBC")]
86-
[TestCase(0x1122330000000000, ExpectedResult = "AAAAAAAAGMRBC")]
87-
[TestCase(0x1122334400000000, ExpectedResult = "AAAAAACEGMRBC")]
88-
[TestCase(0x1122334455000000, ExpectedResult = "AAAAAVKEGMRBC")]
89-
[TestCase(0x1122334455660000, ExpectedResult = "AAAGMVKEGMRBC")]
90-
[TestCase(0x1122334455667700, ExpectedResult = "AB3WMVKEGMRBC")]
91-
public string Encode_long_ReturnsExpectedValues(long number)
130+
[TestCaseSource(nameof(ulongTestCases))]
131+
public void DecodeUInt64_BigEndian_ReturnsExpectedValues(ulong expectedNumber, string input)
92132
{
93-
return Base32.Rfc4648.Encode(number);
133+
if (!BitConverter.IsLittleEndian)
134+
{
135+
throw new InvalidOperationException("big endian tests are only supported on little endian archs");
136+
}
137+
expectedNumber = reverseBytes(expectedNumber);
138+
var bigEndian = new Base32(Base32Alphabet.Rfc4648, isBigEndian: true);
139+
Assert.That(bigEndian.DecodeUInt64(input), Is.EqualTo(expectedNumber));
94140
}
95141

96142
[Test]

0 commit comments

Comments
 (0)