Skip to content

Commit e6c8f1e

Browse files
committed
Merge branch 'main' into next
2 parents 1ad38b0 + ae9820c commit e6c8f1e

27 files changed

+2273
-149
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@ packages/
1212
*.userprefs
1313
SimpleBase.xml
1414
.idea/
15-
BenchmarkDotNet.Artifacts/
15+
BenchmarkDotNet.Artifacts/
16+
TestResults/

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ math worked.
1212

1313
Features
1414
--------
15-
- [Multibase](https://github.com/multiformats/multibase) support. All formats
16-
covered by SimpleBase including a few Base64 variants are supported. Base2, Base8, and Base10 are also supported.
15+
- **[Multibase](https://github.com/multiformats/multibase)**: All formats
16+
covered by SimpleBase including a few Base64 variants are supported. **Base2**, **Base8**, and **Base10** are also supported.
1717
- **Base32**: RFC 4648, BECH32, Crockford, z-base-32, Geohash, FileCoin and Extended Hex
1818
(BASE32-HEX) flavors with Crockford character substitution, or any other
1919
custom flavors.
@@ -40,6 +40,10 @@ To install it from [NuGet](https://www.nuget.org/packages/SimpleBase/):
4040

4141
`Install-Package SimpleBase`
4242

43+
If you need .NET Standard 2.0 compatible version for targeting older .NET Framework or .NET Core, use the 2.x release line instead. It's missing newer features, but still supported:
44+
45+
`Install-Package SimpleBase -MaximumVersion 2.999.0`
46+
4347
Usage
4448
------
4549
The basic usage for encoding a buffer into, say, Base32, is as simple as:

benchmark/Benchmark.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
</ItemGroup>
1919
<ItemGroup>
2020
<PackageReference Include="BenchmarkDotNet" Version="0.15.5" />
21+
<PackageReference Include="BenchmarkDotNet" Version="0.15.2" />
2122
</ItemGroup>
2223
<ItemGroup>
2324
<ProjectReference Include="..\src\SimpleBase.csproj" />

src/Base16Alphabet.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@ public Base16Alphabet(string alphabet)
3232
/// Initializes a new instance of the <see cref="Base16Alphabet"/> class.
3333
/// </summary>
3434
/// <param name="alphabet">Encoding alphabet.</param>
35-
/// <param name="caseSensitive">If the decoding should be performed case sensitive.</param>
35+
/// <param name="caseSensitive">
36+
/// <see langword="true"/> if the decoding should be performed case-sensitive,
37+
/// <see langword="false"/> otherwise.
38+
/// </param>
3639
public Base16Alphabet(string alphabet, bool caseSensitive)
3740
: base(16, alphabet)
3841
{

src/Base256Emoji.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ static string[] createDefaultAlphabet()
4747
/// <summary>
4848
/// Create a new instance of Base256Emoji.
4949
/// </summary>
50-
/// <param name="alphabet">An array that contains 256 elements with emoji values corresponding to every byte.</param>
50+
/// <param name="alphabet">A string that contains 256 emojis corresponding to every byte.</param>
5151
public Base256Emoji(string[] alphabet)
5252
{
5353
if (alphabet.Length != 256)

src/Base32.cs

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55

66
using System;
77
using System.IO;
8+
using System.Runtime.Intrinsics.X86;
89
using System.Threading.Tasks;
10+
using static System.Runtime.InteropServices.JavaScript.JSType;
911

1012
namespace SimpleBase;
1113

@@ -115,6 +117,12 @@ public int GetSafeByteCountForDecoding(ReadOnlySpan<char> text)
115117
}
116118

117119
/// <inheritdoc/>
120+
///<remarks>
121+
///This formula overestimates the required size to the next multiplier of 8 characters
122+
///to leave space for the padding characters at the end. If this kind of generous
123+
///allocation is a problem, a different formula can be used with non-allocating encoding
124+
///functions like <see cref="TryEncode(ReadOnlySpan{byte}, Span{char}, out int)" />.
125+
///</remarks>
118126
public int GetSafeCharCountForEncoding(ReadOnlySpan<byte> buffer)
119127
{
120128
return (((buffer.Length - 1) / bitsPerChar) + 1) * bitsPerByte;
@@ -229,7 +237,10 @@ public string Encode(ReadOnlySpan<byte> bytes)
229237
/// Encode a memory span into a Base32 string.
230238
/// </summary>
231239
/// <param name="bytes">Buffer to be encoded.</param>
232-
/// <param name="padding">Append padding characters in the output.</param>
240+
/// <param name="padding">
241+
/// <see langword="true"/> if padding characters should be appended to the return value,
242+
/// <see langword="false"/> otherwise.
243+
/// </param>
233244
/// <returns>Encoded string.</returns>
234245
public string Encode(ReadOnlySpan<byte> bytes, bool padding)
235246
{
@@ -300,7 +311,10 @@ public void Encode(Stream input, TextWriter output)
300311
/// </summary>
301312
/// <param name="input">Input bytes.</param>
302313
/// <param name="output">The writer the output is written to.</param>
303-
/// <param name="padding">Whether to use padding at the end of the output.</param>
314+
/// <param name="padding">
315+
/// <see langword="true"/> if padding characters should be appended to <paramref name="output"/>,
316+
/// <see langword="false"/> otherwise.
317+
/// </param>
304318
public void Encode(Stream input, TextWriter output, bool padding)
305319
{
306320
StreamHelper.Encode(input, output, (buffer, lastBlock) =>
@@ -326,7 +340,10 @@ public Task EncodeAsync(Stream input, TextWriter output)
326340
/// </summary>
327341
/// <param name="input">Input bytes.</param>
328342
/// <param name="output">The writer the output is written to.</param>
329-
/// <param name="padding">Whether to use padding at the end of the output.</param>
343+
/// <param name="padding">
344+
/// <see langword="true"/> if padding characters should be appended to <paramref name="output"/>,
345+
/// <see langword="false"/> otherwise.
346+
/// </param>
330347
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
331348
public async Task EncodeAsync(Stream input, TextWriter output, bool padding)
332349
{
@@ -370,9 +387,12 @@ public bool TryEncode(ReadOnlySpan<byte> bytes, Span<char> output, out int numCh
370387
/// </summary>
371388
/// <param name="bytes">Input bytes.</param>
372389
/// <param name="output">Output buffer.</param>
373-
/// <param name="padding">Whether to use padding characters at the end.</param>
374-
/// <param name="numCharsWritten">Number of characters written to the output.</param>
375-
/// <returns>True if encoding is successful, false if the output is invalid.</returns>
390+
/// <param name="padding">
391+
/// <see langword="true"/> if padding characters should be appended to <paramref name="output"/>,
392+
/// <see langword="false"/> otherwise.
393+
/// </param>
394+
/// <param name="numCharsWritten">Number of characters written to <paramref name="output"/>.</param>
395+
/// <returns><see langword="true"/> if encoding is successful, <see langword="false"/> if the output is invalid.</returns>
376396
public bool TryEncode(
377397
ReadOnlySpan<byte> bytes,
378398
Span<char> output,

src/Base58.cs

Lines changed: 30 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55

66
using System;
77
using System.Diagnostics;
8-
using System.Runtime.Serialization;
9-
using System.Security.Cryptography;
108

119
namespace SimpleBase;
1210

@@ -29,9 +27,7 @@ public sealed class Base58(Base58Alphabet alphabet) : DividingCoder<Base58Alphab
2927
const int reductionFactor = 733;
3028

3129
const int maxCheckPayloadLength = 256;
32-
const int minCheckDecodedBufferSize = 5;
33-
const int sha256Bytes = 32;
34-
const int sha256DigestBytes = 4;
30+
3531
static readonly Lazy<Base58> bitcoin = new(() => new Base58(Base58Alphabet.Bitcoin));
3632
static readonly Lazy<Base58> ripple = new(() => new Base58(Base58Alphabet.Ripple));
3733
static readonly Lazy<Base58> flickr = new(() => new Base58(Base58Alphabet.Flickr));
@@ -102,20 +98,20 @@ public string EncodeCheck(ReadOnlySpan<byte> payload, byte version)
10298
public string EncodeCheck(ReadOnlySpan<byte> payload, ReadOnlySpan<byte> prefix)
10399
{
104100
int totalLength = prefix.Length + payload.Length;
105-
int outputLen = totalLength + sha256DigestBytes;
101+
int outputLen = totalLength + Sha256.DigestBytes;
106102
Span<byte> output = (outputLen < Bits.SafeStackMaxAllocSize) ? stackalloc byte[outputLen] : new byte[outputLen];
107103
prefix.CopyTo(output);
108104
payload.CopyTo(output[prefix.Length..]);
109-
Span<byte> sha256 = stackalloc byte[sha256Bytes];
110-
computeDoubleSha256(output[..totalLength], sha256);
111-
sha256[..sha256DigestBytes].CopyTo(output[totalLength..]);
105+
Span<byte> sha256 = stackalloc byte[Sha256.Bytes];
106+
Sha256.ComputeTwice(output[..totalLength], sha256);
107+
sha256[..Sha256.DigestBytes].CopyTo(output[totalLength..]);
112108
return Encode(output);
113109
}
114110

115111
/// <summary>
116112
/// Generate a Base58Check string out of a prefix buffer and payload
117113
/// by skipping leading zeroes in <paramref name="payload"/>.
118-
/// Platforms like Tezos expects this behavior.
114+
/// Platforms like Tezos expect this behavior.
119115
/// </summary>
120116
/// <param name="payload">Address data.</param>
121117
/// <param name="prefix">Prefix buffer.</param>
@@ -145,8 +141,11 @@ public string EncodeCheckSkipZeroes(ReadOnlySpan<byte> payload, ReadOnlySpan<byt
145141
/// <param name="address">Address string.</param>
146142
/// <param name="payload">Output address buffer.</param>
147143
/// <param name="version">Address version.</param>
148-
/// <param name="bytesWritten">Number of bytes written in the output payload.</param>
149-
/// <returns>True if address was decoded successfully and passed validation. False, otherwise.</returns>
144+
/// <param name="bytesWritten">Number of bytes written to <paramref name="payload"/>.</param>
145+
/// <returns>
146+
/// <see langword="true"/> if address was decoded successfully and passed validation.
147+
/// <see langword="false"/> otherwise.
148+
/// </returns>
150149
public bool TryDecodeCheck(
151150
ReadOnlySpan<char> address,
152151
Span<byte> payload,
@@ -166,31 +165,30 @@ public bool TryDecodeCheck(
166165
/// <param name="payload">Output address buffer.</param>
167166
/// <param name="prefix">Prefix decoded, must have the exact size of the expected prefix.</param>
168167
/// <param name="payloadBytesWritten">Number of bytes written in the output payload.</param>
169-
/// <returns>True if address was decoded successfully and passed validation. False, otherwise.</returns>
168+
/// <returns><see langword="true"/> if address was decoded successfully and passed validation, <see langword="false"/> otherwise.</returns>
170169
public bool TryDecodeCheck(
171170
ReadOnlySpan<char> address,
172171
Span<byte> payload,
173172
Span<byte> prefix,
174173
out int payloadBytesWritten)
175174
{
176-
Span<byte> buffer = stackalloc byte[maxCheckPayloadLength + sha256DigestBytes + 1];
177-
if (!TryDecode(address, buffer, out int decodedBufferSize) || decodedBufferSize < minCheckDecodedBufferSize)
175+
payloadBytesWritten = 0;
176+
Span<byte> buffer = stackalloc byte[maxCheckPayloadLength + Sha256.DigestBytes + 1];
177+
if (!TryDecode(address, buffer, out int decodedBufferSize))
178178
{
179-
payloadBytesWritten = 0;
180179
return false;
181180
}
182181

183182
buffer = buffer[..decodedBufferSize];
184183
buffer[..prefix.Length].CopyTo(prefix);
185-
Span<byte> sha256 = stackalloc byte[sha256Bytes];
186-
computeDoubleSha256(buffer[..^sha256DigestBytes], sha256);
187-
if (!sha256[..sha256DigestBytes].SequenceEqual(buffer[^sha256DigestBytes..]))
184+
Span<byte> sha256 = stackalloc byte[Sha256.Bytes];
185+
Sha256.ComputeTwice(buffer[..^Sha256.DigestBytes], sha256);
186+
if (!sha256[..Sha256.DigestBytes].SequenceEqual(buffer[^Sha256.DigestBytes..]))
188187
{
189-
payloadBytesWritten = 0;
190188
return false;
191189
}
192190

193-
var finalBuffer = buffer[prefix.Length..^sha256DigestBytes];
191+
var finalBuffer = buffer[prefix.Length..^Sha256.DigestBytes];
194192
finalBuffer.CopyTo(payload);
195193
payloadBytesWritten = finalBuffer.Length;
196194
return true;
@@ -208,13 +206,13 @@ public string EncodeCb58(ReadOnlySpan<byte> payload)
208206
throw new ArgumentException($"Payload length {payload.Length} is greater than {maxCheckPayloadLength}", nameof(payload));
209207
}
210208

211-
int outputLen = payload.Length + sha256DigestBytes;
209+
int outputLen = payload.Length + Sha256.DigestBytes;
212210
Span<byte> output = (outputLen < Bits.SafeStackMaxAllocSize) ? stackalloc byte[outputLen] : new byte[outputLen];
213211
payload.CopyTo(output);
214-
Span<byte> sha256 = stackalloc byte[sha256Bytes];
215-
computeSha256(output[..payload.Length], sha256);
212+
Span<byte> sha256 = stackalloc byte[Sha256.Bytes];
213+
Sha256.Compute(output[..payload.Length], sha256);
216214

217-
sha256[^sha256DigestBytes..].CopyTo(output[payload.Length..]);
215+
sha256[^Sha256.DigestBytes..].CopyTo(output[payload.Length..]);
218216
return Encode(output);
219217
}
220218

@@ -223,51 +221,32 @@ public string EncodeCb58(ReadOnlySpan<byte> payload)
223221
/// </summary>
224222
/// <param name="address">Address string.</param>
225223
/// <param name="payload">Output address buffer.</param>
226-
/// <param name="bytesWritten">Number of bytes written in the output payload.</param>
227-
/// <returns>True if address was decoded successfully and passed validation. False, otherwise.</returns>
224+
/// <param name="bytesWritten">Number of bytes written to <paramref name="payload"/>.</param>
225+
/// <returns><see langword="true"/> if address was decoded successfully and passed validation. <see langword="false"/> otherwise.</returns>
228226
public bool TryDecodeCb58(
229227
ReadOnlySpan<char> address,
230228
Span<byte> payload,
231229
out int bytesWritten)
232230
{
233-
Span<byte> buffer = stackalloc byte[maxCheckPayloadLength + sha256DigestBytes];
231+
Span<byte> buffer = stackalloc byte[maxCheckPayloadLength + Sha256.DigestBytes];
234232
if (!TryDecode(address, buffer, out bytesWritten) || bytesWritten < 4)
235233
{
236234
return false;
237235
}
238236

239237
buffer = buffer[..bytesWritten];
240-
Span<byte> sha256 = stackalloc byte[sha256Bytes];
241-
computeSha256(buffer[..^sha256DigestBytes], sha256);
238+
Span<byte> sha256 = stackalloc byte[Sha256.Bytes];
239+
Sha256.Compute(buffer[..^Sha256.DigestBytes], sha256);
242240

243-
if (!sha256[^sha256DigestBytes..].SequenceEqual(buffer[^sha256DigestBytes..]))
241+
if (!sha256[^Sha256.DigestBytes..].SequenceEqual(buffer[^Sha256.DigestBytes..]))
244242
{
245243
return false;
246244
}
247245

248-
var finalBuffer = buffer[..^sha256DigestBytes];
246+
var finalBuffer = buffer[..^Sha256.DigestBytes];
249247
finalBuffer.CopyTo(payload);
250248
bytesWritten = finalBuffer.Length;
251249
return true;
252250
}
253251

254-
static void computeDoubleSha256(ReadOnlySpan<byte> buffer, Span<byte> output)
255-
{
256-
Span<byte> tempResult = stackalloc byte[sha256Bytes];
257-
computeSha256(buffer, tempResult);
258-
computeSha256(tempResult, output);
259-
}
260-
261-
static void computeSha256(ReadOnlySpan<byte> buffer, Span<byte> output)
262-
{
263-
if (!SHA256.TryHashData(buffer, output, out int bytesWritten))
264-
{
265-
throw new InvalidOperationException("Couldn't compute SHA256");
266-
}
267-
268-
if (bytesWritten != sha256Bytes)
269-
{
270-
throw new InvalidOperationException("Invalid SHA256 length");
271-
}
272-
}
273252
}

src/Base85Ipv6.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@ namespace SimpleBase;
1414
/// Base85 implementation with additional IPv6 coding functions.
1515
/// </summary>
1616
/// <remarks>
17-
/// RFC 1924 sucks, arguably because it's a very early proposal in the history of IPv6:
17+
/// RFC 1924 sucks, arguably because it was a very early proposal in the history of IPv6:
18+
///
1819
/// - It contains special chars: It's prone to be confused with other syntactical elements.
1920
/// It can even cause security issues due to poor escaping, let alone UX problems.
2021
/// - Length gains are usually marginal: IPv6 uses zero elimination to reduce the address representation.
2122
/// - Slow. The algorithm is division based, instead of faster bitwise operations.
23+
///
2224
/// So, that's why I only included a proof of concept implementation instead of working on optimizing it.
2325
/// RFC 1924 should die, and this code should only be used to support some obscure standard or code somewhere.
2426
/// </remarks>
@@ -97,7 +99,7 @@ public IPAddress DecodeIPv6(string text)
9799
/// </summary>
98100
/// <param name="text">Encoded text.</param>
99101
/// <param name="ip">Resulting IPv6 address.</param>
100-
/// <returns>True if successful, false otherwise.</returns>
102+
/// <returns><see langword="true"/> if successful, <see langword="false"/> otherwise.</returns>
101103
public bool TryDecodeIPv6(string text, out IPAddress ip)
102104
{
103105
if (text.Length != ipv6chars)

src/Bits.cs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
// </copyright>
55

66
using System;
7+
using System.Runtime.CompilerServices;
78

89
namespace SimpleBase
910
{
@@ -15,6 +16,7 @@ static class Bits
1516
/// <summary>
1617
/// Safe one-shot maximum amount to be allocated on stack for temporary buffers and alike.
1718
/// </summary>
19+
/// <see href="https://learn.microsoft.com/en-us/dotnet/standard/unsafe-code/best-practices#:~:text=For%20example%2C%201024%20bytes%20could%20be%20considered%20a%20reasonable%20upper%20bound." />
1820
internal const int SafeStackMaxAllocSize = 1024;
1921

2022
/// <summary>
@@ -23,10 +25,11 @@ static class Bits
2325
internal const int MaxUInt64Digits = 20;
2426

2527
/// <summary>
26-
/// Converts a byte array to a hexadecimal string.
28+
/// Converts a variable length byte array to a 64-bit unsigned integer.
2729
/// </summary>
2830
/// <param name="bytes"></param>
29-
/// <returns></returns>
31+
/// <returns>Unsigned integer representation of the bytes.</returns>
32+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
3033
internal static ulong PartialBigEndianBytesToUInt64(ReadOnlySpan<byte> bytes)
3134
{
3235
if (bytes.Length > sizeof(ulong))
@@ -41,5 +44,20 @@ internal static ulong PartialBigEndianBytesToUInt64(ReadOnlySpan<byte> bytes)
4144
}
4245
return result;
4346
}
47+
48+
/// <summary>
49+
/// Count the number of consecutive zero bytes at the beginning of the given buffer.
50+
/// </summary>
51+
/// <param name="bytes">Buffer for prefixing zeroes to be counted.</param>
52+
/// <returns>Number of zeroes at the beginning of the buffer.</returns>
53+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
54+
internal static int CountPrefixingZeroes(ReadOnlySpan<byte> bytes)
55+
{
56+
int i = 0;
57+
for (; i < bytes.Length && bytes[i] == 0; i++)
58+
{
59+
}
60+
return i;
61+
}
4462
}
4563
}

0 commit comments

Comments
 (0)