Skip to content

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 →