Base32 / Base58 / Base62 / Base85 Encoder — C# source
Encode text to Base32, Base58, Base62, or Ascii85 - or decode it back. UTF-8 safe, runs entirely in your browser, with a shareable link to your exact input.
This is the C# implementation — the same logic the interactive tool runs, in a shareable, citable form.
// base-encoder — Base32 (RFC 4648), Base58 (Bitcoin), Base62, and Base85
// (Ascii85) byte-array encoders, operating on the UTF-8 bytes of the input
// text.
//
// Language: C# (C# 12 / .NET 8, standard library only)
// Source: CosmoDev polyglot showcase port of the Base Encoder tool, ported
// from cli/base-encoder/base-encoder.go (the authoritative Go twin).
// License: display source — part of CosmoDev's polyglot tool pages.
//
// Design goals:
// - Pure + deterministic; never throws (encode always succeeds, decode
// returns null for invalid or malformed input, mirroring the TS lib's
// `null` and the Go twin's `errInvalid`).
// - Functionally equivalent to the Go twin: same inputs -> same outputs.
// - Self-contained: BCL only — no NuGet packages.
//
// Arbitrary-precision note: Base58 and Base62 base-convert the whole byte
// array. System.Numerics.BigInteger (the BCL equivalent of Go's math/big and
// Python's int) is arbitrary-precision, so we get the exact same semantics
// for free — no manual bignum code (unlike the dependency-free Rust/C/C++
// siblings).
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Text;
/// <summary>One of the four supported byte-array base encodings. Mirrors the
/// Go twin's <c>Scheme</c> type and the TS <c>Scheme</c> union.</summary>
public enum Scheme
{
Base32,
Base58,
Base62,
Base85,
}
public static class BaseEncoder
{
private const string B32Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
private const string B58Alphabet =
"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
private const string B62Alphabet =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
/// <summary>Data characters emitted by a final (partial) 5-byte chunk
/// before '=' padding, per RFC 4648. Index = byte count (0..4). Matches
/// the TS <c>outLen</c> table.</summary>
private static readonly int[] OutLen32 = { 0, 2, 4, 5, 7 };
// -----------------------------------------------------------------------
// BigInteger helpers — minimal big-endian byte output, matching Go's
// big.Int.Bytes() (and Python's int.to_bytes).
// -----------------------------------------------------------------------
/// <summary>BigInteger -> minimal big-endian bytes. BigInteger
/// .ToByteArray() is little-endian and may carry an extra sign byte; we
/// reverse it and drop that byte so the output is minimal.</summary>
private static byte[] ToBigEndianBytes(BigInteger num)
{
if (num.IsZero)
{
return Array.Empty<byte>();
}
byte[] le = num.ToByteArray();
int n = le.Length;
if (le[n - 1] == 0)
{
n--; // drop the sign byte
}
byte[] be = new byte[n];
for (int i = 0; i < n; i++)
{
be[i] = le[n - 1 - i];
}
return be;
}
// -----------------------------------------------------------------------
// Base32 — RFC 4648 alphabet, padded to a multiple of 8 chars with '='.
// -----------------------------------------------------------------------
private static string Encode32(byte[] data)
{
var out_ = new StringBuilder();
for (int i = 0; i < data.Length; i += 5)
{
int n = Math.Min(5, data.Length - i);
var b = new uint[5];
for (int j = 0; j < n; j++)
{
b[j] = data[i + j];
}
// Pack 5 bytes (40 bits) into 8 base32 digits (5 bits each, big-endian).
uint[] digits =
{
(b[0] >> 3) & 0x1f,
((b[0] << 2) | (b[1] >> 6)) & 0x1f,
(b[1] >> 1) & 0x1f,
((b[1] << 4) | (b[2] >> 4)) & 0x1f,
((b[2] << 1) | (b[3] >> 7)) & 0x1f,
(b[3] >> 2) & 0x1f,
((b[3] << 3) | (b[4] >> 5)) & 0x1f,
b[4] & 0x1f,
};
int outLen = n == 5 ? 8 : OutLen32[n];
for (int k = 0; k < outLen; k++)
{
out_.Append(B32Alphabet[(int)digits[k]]);
}
for (int k = outLen; k < 8; k++)
{
out_.Append('=');
}
}
return out_.ToString();
}
private static byte[]? Decode32(string s)
{
var outBytes = new List<byte>();
uint buffer = 0;
int bits = 0;
foreach (char c in s)
{
if (c == '=')
{
break; // padding marks the end
}
int idx = B32Alphabet.IndexOf(c);
if (idx < 0)
{
return null;
}
buffer = (buffer << 5) | (uint)idx;
bits += 5;
if (bits >= 8)
{
bits -= 8;
outBytes.Add((byte)((buffer >> bits) & 0xff));
buffer &= (1u << bits) - 1u; // keep only the leftover bits
}
}
return outBytes.ToArray();
}
// -----------------------------------------------------------------------
// Base58 — Bitcoin alphabet. Leading 0x00 bytes -> leading '1' (count
// preserved).
// -----------------------------------------------------------------------
private static string Encode58(byte[] data)
{
// Count leading zero bytes — each maps to a leading '1'.
int zeros = 0;
while (zeros < data.Length && data[zeros] == 0)
{
zeros++;
}
// Big-endian byte array (skipping the leading zeros) -> BigInteger.
BigInteger num = BigInteger.Zero;
for (int i = zeros; i < data.Length; i++)
{
num = (num << 8) | data[i];
}
// Base-convert to 58 digits (collected least-significant first).
var digits = new List<char>();
while (num > BigInteger.Zero)
{
BigInteger rem = num % 58;
num /= 58;
digits.Add(B58Alphabet[(int)rem]);
}
digits.Reverse();
return new string('1', zeros) + string.Concat(digits);
}
private static byte[]? Decode58(string s)
{
// Count leading '1's — each maps to a 0x00 byte.
int zeros = 0;
while (zeros < s.Length && s[zeros] == '1')
{
zeros++;
}
BigInteger num = BigInteger.Zero;
for (int i = zeros; i < s.Length; i++)
{
int idx = B58Alphabet.IndexOf(s[i]);
if (idx < 0)
{
return null;
}
num = num * 58 + idx;
}
byte[] body = ToBigEndianBytes(num);
byte[] outBytes = new byte[zeros + body.Length];
body.CopyTo(outBytes, zeros);
return outBytes;
}
// -----------------------------------------------------------------------
// Base62 — standard base-conversion of the byte array (no leading-zero
// special-casing beyond the standard big-int).
// -----------------------------------------------------------------------
private static string Encode62(byte[] data)
{
if (data.Length == 0)
{
return "";
}
BigInteger num = BigInteger.Zero;
foreach (byte b in data)
{
num = (num << 8) | b;
}
if (num.IsZero)
{
return "0";
}
var digits = new List<char>();
while (num > BigInteger.Zero)
{
BigInteger rem = num % 62;
num /= 62;
digits.Add(B62Alphabet[(int)rem]);
}
digits.Reverse();
return string.Concat(digits);
}
private static byte[]? Decode62(string s)
{
if (s.Length == 0)
{
return Array.Empty<byte>();
}
BigInteger num = BigInteger.Zero;
foreach (char c in s)
{
int idx = B62Alphabet.IndexOf(c);
if (idx < 0)
{
return null;
}
num = num * 62 + idx;
}
return ToBigEndianBytes(num);
}
// -----------------------------------------------------------------------
// Base85 — Ascii85. 4 bytes -> 5 chars in '!'(33)..'u'(117); a full
// 4-zero group is shortened to 'z'. No <~ ~> delimiters. Partial final
// groups emit one fewer char than (bytes+1) would suggest; decode
// reverses, padding with 'u' (value 84).
// -----------------------------------------------------------------------
private static string Encode85(byte[] data)
{
var out_ = new StringBuilder();
for (int i = 0; i < data.Length; i += 4)
{
int n = Math.Min(4, data.Length - i);
bool isFull = n == 4;
var b = new uint[4];
for (int j = 0; j < n; j++)
{
b[j] = data[i + j];
}
uint u = b[0] * 16777216u + b[1] * 65536u + b[2] * 256u + b[3];
if (isFull && u == 0)
{
out_.Append('z'); // zero-group shorthand
continue;
}
var digits = new uint[5];
uint v = u;
for (int k = 4; k >= 0; k--)
{
digits[k] = v % 85;
v /= 85;
}
int emit = isFull ? 5 : n + 1; // n bytes -> n+1 chars
for (int k = 0; k < emit; k++)
{
out_.Append((char)(digits[k] + 33));
}
}
return out_.ToString();
}
private static byte[]? Decode85(string s)
{
var outBytes = new List<byte>();
var group = new List<uint>(); // accumulated digit values (0..84)
foreach (char ch in s)
{
if (ch == 'z')
{
// 'z' is only valid at a group boundary (an empty accumulator).
if (group.Count > 0)
{
return null;
}
outBytes.AddRange(new byte[] { 0, 0, 0, 0 });
continue;
}
if (ch < 33 || ch > 117)
{
return null;
}
group.Add((uint)(ch - 33));
if (group.Count == 5)
{
ulong v = 0;
foreach (uint d in group)
{
v = v * 85 + d;
}
if (v > 0xFFFFFFFFul)
{
return null; // a 5-char group must fit in 32 bits
}
outBytes.Add((byte)((v >> 24) & 0xff));
outBytes.Add((byte)((v >> 16) & 0xff));
outBytes.Add((byte)((v >> 8) & 0xff));
outBytes.Add((byte)(v & 0xff));
group.Clear();
}
}
// Handle a partial final group (2-4 chars -> 1-3 bytes).
if (group.Count > 0)
{
int m = group.Count;
if (m < 2)
{
return null; // a lone trailing char is malformed
}
while (group.Count < 5)
{
group.Add(84); // pad with 'u'
}
ulong v2 = 0;
foreach (uint d in group)
{
v2 = v2 * 85 + d;
}
if (v2 > 0xFFFFFFFFul)
{
return null;
}
byte[] all =
{
(byte)((v2 >> 24) & 0xff),
(byte)((v2 >> 16) & 0xff),
(byte)((v2 >> 8) & 0xff),
(byte)(v2 & 0xff),
};
outBytes.AddRange(all[..(m - 1)]);
}
return outBytes.ToArray();
}
// -----------------------------------------------------------------------
// Public API
// -----------------------------------------------------------------------
/// <summary>Dispatch raw bytes to the chosen scheme's encoder. Mirrors
/// the Go twin's private <c>encodeBytes</c>.</summary>
private static string EncodeBytes(byte[] data, Scheme scheme) => scheme switch
{
Scheme.Base32 => Encode32(data),
Scheme.Base58 => Encode58(data),
Scheme.Base62 => Encode62(data),
Scheme.Base85 => Encode85(data),
_ => "",
};
/// <summary>Dispatch an encoded string to the chosen scheme's decoder.
/// An invalid or malformed input yields null (mirroring the TS
/// <c>null</c>). Mirrors the Go twin's private
/// <c>decodeBytes</c>.</summary>
private static byte[]? DecodeBytes(string encoded, Scheme scheme) => scheme switch
{
Scheme.Base32 => Decode32(encoded),
Scheme.Base58 => Decode58(encoded),
Scheme.Base62 => Decode62(encoded),
Scheme.Base85 => Decode85(encoded),
_ => null,
};
/// <summary>Returns the chosen-scheme encoding of the UTF-8 bytes of
/// <paramref name="text"/>. Empty text encodes to "". It is the C# twin
/// of <c>Encode</c> in cli/base-encoder/base-encoder.go.</summary>
public static string Encode(string text, Scheme scheme)
{
return EncodeBytes(Encoding.UTF8.GetBytes(text), scheme);
}
/// <summary>Reverses an encoded string back to UTF-8 text. Invalid
/// characters or a malformed structure yield null — mirroring the Go
/// twin's <c>errInvalid</c> and the TS lib's <c>null</c>. It is the C#
/// twin of <c>Decode</c> in cli/base-encoder/base-encoder.go.
///
/// The decoded bytes are interpreted as UTF-8; the BCL UTF-8 decoder
/// uses replacement fallback (U+FFD), so a structurally-valid-but-non-
/// UTF-8 payload never produces a second error (mirroring Go's
/// <c>string(data)</c>, which never fails).</summary>
public static string? Decode(string encoded, Scheme scheme)
{
byte[]? data = DecodeBytes(encoded, scheme);
if (data is null)
{
return null;
}
return Encoding.UTF8.GetString(data);
}
}
/// <summary>Showcase self-test — mirrors cli/base-encoder/base-encoder_test.go
/// vectors. Run directly: <c>dotnet run csharp.cs</c>.</summary>
public static class Program
{
private static void Check(bool ok, string name)
{
if (!ok)
{
Console.Error.WriteLine($"FAIL: {name}");
Environment.Exit(1);
}
}
public static void Main()
{
string nul = "\u0000";
// Base32 — known values + RFC 4648 padding + case sensitivity.
Check(BaseEncoder.Encode("hello", Scheme.Base32) == "NBSWY3DP", "b32 hello");
// 3 bytes -> 5 data chars + 3 '=' pads.
Check(BaseEncoder.Encode("foo", Scheme.Base32) == "MZXW6===", "b32 foo");
Check(BaseEncoder.Decode("NBSWY3DP", Scheme.Base32) == "hello", "b32 decode");
// lowercase is not in the RFC 4648 alphabet
Check(BaseEncoder.Decode("nbswy3dp", Scheme.Base32) is null, "b32 lowercase");
// Base58 — each leading 0x00 byte -> a leading '1'.
Check(BaseEncoder.Encode(nul, Scheme.Base58) == "1", "b58 zero byte");
Check(BaseEncoder.Encode(nul + nul + "A", Scheme.Base58).StartsWith("11"), "b58 two zeros");
Check(BaseEncoder.Decode("1", Scheme.Base58) == nul, "b58 decode 1");
// round-trip preserves the leading zero bytes exactly
Check(
BaseEncoder.Decode(BaseEncoder.Encode(nul + nul + "A", Scheme.Base58), Scheme.Base58)
== nul + nul + "A",
"b58 round-trip");
// Base62 — plain big-int base conversion (no leading-zero preservation).
Check(BaseEncoder.Encode("A", Scheme.Base62) == "13", "b62 A"); // 1*62 + 3
Check(BaseEncoder.Decode("13", Scheme.Base62) == "A", "b62 decode");
Check(BaseEncoder.Encode(nul, Scheme.Base62) == "0", "b62 zero");
// no leading-zero preservation: the minimal rep of 0 is empty
Check(BaseEncoder.Decode("0", Scheme.Base62) == "", "b62 minimal zero");
// Base85 — Ascii85 'z' shorthand + 32-bit overflow rejection.
Check(BaseEncoder.Encode("hello", Scheme.Base85) == "BOu!rDZ", "b85 hello");
Check(BaseEncoder.Encode(nul + nul + nul + nul, Scheme.Base85) == "z", "b85 z");
Check(BaseEncoder.Encode(new string('\u0000', 8), Scheme.Base85) == "zz", "b85 zz");
// a 5-char group must fit in 32 bits; "uuuuu" overflows
Check(BaseEncoder.Decode("uuuuu", Scheme.Base85) is null, "b85 overflow");
// a lone trailing char is a malformed partial group
Check(BaseEncoder.Decode("B", Scheme.Base85) is null, "b85 lone char");
// Cross-scheme — empty, multibyte round-trip, and invalid rejection.
foreach (Scheme scheme in new[] { Scheme.Base32, Scheme.Base58, Scheme.Base62, Scheme.Base85 })
{
Check(BaseEncoder.Encode("", scheme) == "", $"{scheme} empty");
Check(BaseEncoder.Decode("", scheme) == "", $"{scheme} empty decode");
// multibyte UTF-8 round-trips through every scheme
Check(
BaseEncoder.Decode(BaseEncoder.Encode("CosmoDev \U0001F680", scheme), scheme)
== "CosmoDev \U0001F680",
$"{scheme} multibyte");
// '~' is outside every supported alphabet
Check(BaseEncoder.Decode("~!not-valid!~", scheme) is null, $"{scheme} invalid");
}
Console.WriteLine("ok");
}
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →