Skip to content

Commit 361f979

Browse files
committed
add Multibase support
1 parent d5e1ef4 commit 361f979

File tree

8 files changed

+210
-77
lines changed

8 files changed

+210
-77
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
# 4.3.0
2+
3+
## New features
4+
- Added Multibase support that supports several Base16, Base32, Base58, and Base64 variants.
5+
6+
## Improvements
7+
- Eliminated more memory allocations (by @Henr1k80))
8+
19
# 4.2.0
210

311
## New features

README.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ Features
2323
can take the optimizations compared to .NET's implementations. It's quite
2424
fast now. It could also be used as a replacement for `SoapHexBinary.Parse` although
2525
.NET has [`Convert.FromHexString()`](https://learn.microsoft.com/en-us/dotnet/api/system.convert.fromhexstring?view=net-5.0) method since .NET 5.
26+
- [Multibase](https://github.com/multiformats/multibase) support. All formats
27+
covered by SimpleBase including a few Base64 variants are supported.
2628
- One-shot memory buffer based APIs for simple use cases.
2729
- Stream-based async APIs for more advanced scenarios.
2830
- Lightweight: No dependencies.
@@ -212,7 +214,7 @@ methods which receive input, output buffers as parameters.
212214
Encoding is like this:
213215

214216
```csharp
215-
byte[] input = new byte[] { 1, 2, 3, 4, 5 };
217+
byte[] input = [1, 2, 3, 4, 5];
216218
int outputBufferSize = Base58.Bitcoin.GetSafeCharCountForEncoding(input);
217219
var output = new char[outputBufferSize];
218220

@@ -235,6 +237,32 @@ if (Base58.Bitcoin.TryDecode(input, output, out int numBytesWritten))
235237
}
236238
```
237239

240+
### Multibase encoding/decoding
241+
In order to encode a Multibase string just specify the encoding
242+
you want to use:
243+
244+
```csharp
245+
byte[] input = [1, 2, 3, 4, 5];
246+
string result = Multibase.Encode(input, MultibaseEncoding.Base32);
247+
```
248+
249+
When decoding a multibase string, the encoding is automatically detected:
250+
251+
```csharp
252+
string input = "... some encoded multibase string ...";
253+
byte[] result = Multibase.Decode(input);
254+
```
255+
256+
If you don't want decoding to raise an exception, use TryDecode() method instead:
257+
258+
```csharp
259+
string input = "... some encoded multibase string ...";
260+
byte[] output = new byte[outputBufferSize]; // enough the fit the decoded buffer
261+
if (Multibase.TryDecode(input, output, out int numBytesWritten))
262+
{
263+
// et voila!
264+
}
265+
```
238266

239267
Benchmark Results
240268
-----------------

src/Base64.cs

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

66
using System;
7-
using System.Xml.Serialization;
87

98
namespace SimpleBase;
109

@@ -14,53 +13,57 @@ namespace SimpleBase;
1413
static class Base64
1514
{
1615
const char paddingChar = '=';
17-
static readonly char[] urlSource = ['+', '/'];
18-
static readonly char[] urlTarget = ['-', '_'];
1916

2017
internal static byte[] DecodeUrl(ReadOnlySpan<char> text)
2118
{
22-
var base64url = convertBase64UrlToBase64(text.ToString());
23-
return Convert.FromBase64String(base64url);
19+
string base64 = convertBase64UrlToBase64(text);
20+
return Convert.FromBase64String(base64);
2421
}
2522

26-
static string convertBase64UrlToBase64(string text)
23+
static string convertBase64UrlToBase64(ReadOnlySpan<char> text)
2724
{
28-
return replaceMultiple(text, urlTarget, urlSource);
29-
}
30-
31-
static string replaceMultiple(string text, char[] src, char[] dst)
32-
{
33-
if (text.Length == 0)
25+
int len = text.Length;
26+
if (len == 0)
3427
{
35-
return text;
28+
return string.Empty;
3629
}
37-
return string.Create(text.Length, (text, src, dst), static (span, state) =>
38-
{
39-
var text = state.text;
40-
var src = state.src;
41-
var dst = state.dst;
4230

43-
int lastPos = 0;
44-
int i;
45-
while (true)
31+
// .NET's Base64 decoder requires padding to be present
32+
int padLen = 0;
33+
if (text[len - 1] != paddingChar)
34+
{
35+
padLen = 4 - (len % 4);
36+
}
37+
len += padLen;
38+
Span<char> result = len < Bits.SafeStackMaxAllocSize ? stackalloc char[len] : new char[len];
39+
for (int i = 0; i < text.Length; i++)
40+
{
41+
result[i] = text[i] switch
4642
{
47-
i = text.IndexOfAny(src);
48-
if (i < 0)
49-
{
50-
text.AsSpan(lastPos).CopyTo(span[lastPos..]);
51-
break;
52-
}
53-
else
54-
{
43+
'-' => '+',
44+
'_' => '/',
45+
_ => text[i]
46+
};
47+
}
48+
if (padLen > 0)
49+
{
50+
result[text.Length..].Fill(paddingChar);
51+
}
52+
return result.ToString();
53+
}
5554

56-
}
57-
}
58-
});
55+
internal static bool TryDecodeUrl(ReadOnlySpan<char> text, Span<byte> bytes, out int bytesWritten)
56+
{
57+
string base64 = convertBase64UrlToBase64(text);
58+
return Convert.TryFromBase64Chars(base64.AsSpan(), bytes, out bytesWritten);
5959
}
6060

6161
static string convertBase64ToBase64Url(string text)
6262
{
63-
return replaceMultiple(text, urlSource, urlTarget);
63+
// i tried writing custom code to perform this replacement faster
64+
// but it turned out slower despite having half the allocation.
65+
// NOTE: padding char is the same between base64 and base64url
66+
return text.Replace('+', '-').Replace('/', '_');
6467
}
6568

6669
static string stripBase64Padding(string base64Text)

src/Multibase.cs

Lines changed: 40 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -8,32 +8,6 @@
88

99
namespace SimpleBase;
1010

11-
/// <summary>
12-
/// Currently supported Multibase encodings.
13-
/// </summary>
14-
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
15-
public enum MultibaseEncoding
16-
{
17-
// marked as "final" in the spec at https://github.com/multiformats/multibase/blob/master/multibase.csv
18-
Base16 = 'f',
19-
Base16Upper = 'F',
20-
Base32 = 'b',
21-
Base32Upper = 'B',
22-
Base58Bitcoin = 'z',
23-
Base64 = 'm',
24-
Base64Url = 'u',
25-
Base64UrlPad = 'U',
26-
27-
// marked as "draft"
28-
Base32Z = 'h',
29-
30-
// marked as "experimental"
31-
Base58Flickr = 'Z',
32-
Base32Hex = 'v',
33-
Base32HexUpper = 'V',
34-
}
35-
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
36-
3711
/// <summary>
3812
/// Multibase encoding and decoding.
3913
/// </summary>
@@ -57,11 +31,11 @@ public static byte[] Decode(ReadOnlySpan<char> text)
5731
var rest = text[1..];
5832
return encoding switch
5933
{
60-
MultibaseEncoding.Base16 => Base16.LowerCase.Decode(rest),
34+
MultibaseEncoding.Base16Lower => Base16.LowerCase.Decode(rest),
6135
MultibaseEncoding.Base16Upper => Base16.UpperCase.Decode(rest),
62-
MultibaseEncoding.Base32 => Base32.FileCoin.Decode(rest),
36+
MultibaseEncoding.Base32Lower => Base32.FileCoin.Decode(rest),
6337
MultibaseEncoding.Base32Upper => Base32.Rfc4648.Decode(rest),
64-
MultibaseEncoding.Base32Hex => Base32.ExtendedHexLower.Decode(rest),
38+
MultibaseEncoding.Base32HexLower => Base32.ExtendedHexLower.Decode(rest),
6539
MultibaseEncoding.Base32HexUpper => Base32.ExtendedHex.Decode(rest),
6640
MultibaseEncoding.Base32Z => Base32.ZBase32.Decode(rest),
6741
MultibaseEncoding.Base58Bitcoin => Base58.Bitcoin.Decode(rest),
@@ -72,6 +46,40 @@ public static byte[] Decode(ReadOnlySpan<char> text)
7246
};
7347
}
7448

49+
/// <summary>
50+
/// Tries to decode a multibase encoded string into a span of bytes.
51+
/// </summary>
52+
/// <param name="text">Input text.</param>
53+
/// <param name="bytes">Output span.</param>
54+
/// <param name="bytesWritten">Number of bytes written to the output span.</param>
55+
/// <returns>True if successful, false otherwise.</returns>
56+
public static bool TryDecode(ReadOnlySpan<char> text, Span<byte> bytes, out int bytesWritten)
57+
{
58+
bytesWritten = 0;
59+
if (text.Length == 0)
60+
{
61+
return false;
62+
}
63+
char c = text[0];
64+
var encoding = (MultibaseEncoding)c;
65+
var rest = text[1..];
66+
return encoding switch
67+
{
68+
MultibaseEncoding.Base16Lower => Base16.LowerCase.TryDecode(rest, bytes, out bytesWritten),
69+
MultibaseEncoding.Base16Upper => Base16.UpperCase.TryDecode(rest, bytes, out bytesWritten),
70+
MultibaseEncoding.Base32Lower => Base32.FileCoin.TryDecode(rest, bytes, out bytesWritten),
71+
MultibaseEncoding.Base32Upper => Base32.Rfc4648.TryDecode(rest, bytes, out bytesWritten),
72+
MultibaseEncoding.Base32HexLower => Base32.ExtendedHexLower.TryDecode(rest, bytes, out bytesWritten),
73+
MultibaseEncoding.Base32HexUpper => Base32.ExtendedHex.TryDecode(rest, bytes, out bytesWritten),
74+
MultibaseEncoding.Base32Z => Base32.ZBase32.TryDecode(rest, bytes, out bytesWritten),
75+
MultibaseEncoding.Base58Bitcoin => Base58.Bitcoin.TryDecode(rest, bytes, out bytesWritten),
76+
MultibaseEncoding.Base58Flickr => Base58.Flickr.TryDecode(rest, bytes, out bytesWritten),
77+
MultibaseEncoding.Base64 => Convert.TryFromBase64Chars(rest, bytes, out bytesWritten),
78+
MultibaseEncoding.Base64Url or MultibaseEncoding.Base64UrlPad => Base64.TryDecodeUrl(rest, bytes, out bytesWritten),
79+
_ => false,
80+
};
81+
}
82+
7583
/// <summary>
7684
/// Encodes a byte array into a multibase encoded string with given encoding.
7785
/// </summary>
@@ -85,11 +93,11 @@ public static string Encode(ReadOnlySpan<byte> bytes, MultibaseEncoding encoding
8593
.Append((char)encoding)
8694
.Append(encoding switch
8795
{
88-
MultibaseEncoding.Base16 => Base16.LowerCase.Encode(bytes),
96+
MultibaseEncoding.Base16Lower => Base16.LowerCase.Encode(bytes),
8997
MultibaseEncoding.Base16Upper => Base16.UpperCase.Encode(bytes),
90-
MultibaseEncoding.Base32 => Base32.FileCoin.Encode(bytes),
98+
MultibaseEncoding.Base32Lower => Base32.FileCoin.Encode(bytes),
9199
MultibaseEncoding.Base32Upper => Base32.Rfc4648.Encode(bytes),
92-
MultibaseEncoding.Base32Hex => Base32.ExtendedHexLower.Encode(bytes),
100+
MultibaseEncoding.Base32HexLower => Base32.ExtendedHexLower.Encode(bytes),
93101
MultibaseEncoding.Base32HexUpper => Base32.ExtendedHex.Encode(bytes),
94102
MultibaseEncoding.Base32Z => Base32.ZBase32.Encode(bytes),
95103
MultibaseEncoding.Base58Bitcoin => Base58.Bitcoin.Encode(bytes),

src/MultibaseEncoding.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// <copyright file="MultibaseEncoding.cs" company="Sedat Kapanoglu">
2+
// Copyright (c) 2014-2025 Sedat Kapanoglu
3+
// Licensed under Apache-2.0 License (see LICENSE.txt file for details)
4+
// </copyright>
5+
6+
namespace SimpleBase;
7+
8+
/// <summary>
9+
/// Currently supported Multibase encodings.
10+
/// </summary>
11+
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
12+
public enum MultibaseEncoding
13+
{
14+
// marked as "final" in the spec at https://github.com/multiformats/multibase/blob/master/multibase.csv
15+
Base16Lower = 'f',
16+
Base16Upper = 'F',
17+
Base32Lower = 'b',
18+
Base32Upper = 'B',
19+
Base58Bitcoin = 'z',
20+
Base64 = 'm',
21+
Base64Url = 'u',
22+
Base64UrlPad = 'U',
23+
24+
// marked as "draft"
25+
Base32Z = 'h',
26+
27+
// marked as "experimental"
28+
Base58Flickr = 'Z',
29+
Base32HexLower = 'v',
30+
Base32HexUpper = 'V',
31+
}
32+
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member

src/PublicAPI.Unshipped.txt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
SimpleBase.Multibase
2-
SimpleBase.Multibase.Multibase() -> void
32
SimpleBase.MultibaseEncoding
4-
SimpleBase.MultibaseEncoding.Base16 = 102 -> SimpleBase.MultibaseEncoding
3+
SimpleBase.MultibaseEncoding.Base16Lower = 102 -> SimpleBase.MultibaseEncoding
54
SimpleBase.MultibaseEncoding.Base16Upper = 70 -> SimpleBase.MultibaseEncoding
6-
SimpleBase.MultibaseEncoding.Base32 = 98 -> SimpleBase.MultibaseEncoding
7-
SimpleBase.MultibaseEncoding.Base32Hex = 118 -> SimpleBase.MultibaseEncoding
5+
SimpleBase.MultibaseEncoding.Base32HexLower = 118 -> SimpleBase.MultibaseEncoding
86
SimpleBase.MultibaseEncoding.Base32HexUpper = 86 -> SimpleBase.MultibaseEncoding
7+
SimpleBase.MultibaseEncoding.Base32Lower = 98 -> SimpleBase.MultibaseEncoding
98
SimpleBase.MultibaseEncoding.Base32Upper = 66 -> SimpleBase.MultibaseEncoding
109
SimpleBase.MultibaseEncoding.Base32Z = 104 -> SimpleBase.MultibaseEncoding
1110
SimpleBase.MultibaseEncoding.Base58Bitcoin = 122 -> SimpleBase.MultibaseEncoding
@@ -16,4 +15,5 @@ SimpleBase.MultibaseEncoding.Base64UrlPad = 85 -> SimpleBase.MultibaseEncoding
1615
static SimpleBase.Base32.ExtendedHexLower.get -> SimpleBase.Base32!
1716
static SimpleBase.Base32Alphabet.ExtendedHexLower.get -> SimpleBase.Base32Alphabet!
1817
static SimpleBase.Multibase.Decode(System.ReadOnlySpan<char> text) -> byte[]!
19-
static SimpleBase.Multibase.Encode(System.ReadOnlySpan<byte> bytes, SimpleBase.MultibaseEncoding encoding) -> string!
18+
static SimpleBase.Multibase.Encode(System.ReadOnlySpan<byte> bytes, SimpleBase.MultibaseEncoding encoding) -> string!
19+
static SimpleBase.Multibase.TryDecode(System.ReadOnlySpan<char> text, System.Span<byte> bytes, out int bytesWritten) -> bool

src/SimpleBase.csproj

Lines changed: 3 additions & 6 deletions
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.2.0</PackageVersion>
15+
<PackageVersion>4.3.0</PackageVersion>
1616
<DocumentationFile>SimpleBase.xml</DocumentationFile>
1717
<PackageProjectUrl>https://github.com/ssg/SimpleBase</PackageProjectUrl>
1818
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
@@ -25,13 +25,10 @@
2525
<PackageReleaseNotes>
2626
<![CDATA[
2727
## New features
28-
- Monero Base58 algorithm support with `MoneroBase58` class. It can be accessed as `Base58.Monero`
28+
- Added Multibase support that supports several Base16, Base32, Base58, and Base64 variants.
2929
3030
## Improvements
31-
- Eliminate some memory allocations
32-
33-
## Fixes
34-
- Throw `ArgumentOutOfRangeException` with correct parameters in `Base32.DecodeInt64()`
31+
- Eliminated more memory allocations (by @Henr1k80))
3532
]]>
3633
</PackageReleaseNotes>
3734
</PropertyGroup>

0 commit comments

Comments
 (0)