Skip to content

Base32 / Base58 / Base62 / Base85 Encoder — Java 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 Java 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: Java (Java 17, 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 Optional.empty() 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: JDK only — no external dependencies.
//
// Arbitrary-precision note: Base58 and Base62 base-convert the whole byte
// array. java.math.BigInteger (the JDK 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).

import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Optional;

/** One of the four supported byte-array base encodings. Mirrors the Go
 * twin's {@code Scheme} type and the TS {@code Scheme} union. */
enum Scheme {
    BASE32, BASE58, BASE62, BASE85
}

/** The Base Encoder tool ported to idiomatic Java. It is the Java twin of
 * {@code Encode}/{@code Decode} in cli/base-encoder/base-encoder.go. */
class BaseEncoder {

    private static final String B32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
    private static final String B58_ALPHABET =
            "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
    private static final String B62_ALPHABET =
            "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";

    /** Data characters emitted by a final (partial) 5-byte chunk before '='
     * padding, per RFC 4648. Index = byte count (0..4). Matches the TS
     * {@code outLen} table. */
    private static final int[] OUT_LEN_32 = {0, 2, 4, 5, 7};

    private static final BigInteger BASE_58 = BigInteger.valueOf(58);
    private static final BigInteger BASE_62 = BigInteger.valueOf(62);

    // -----------------------------------------------------------------------
    // BigInteger helper — minimal big-endian byte output, matching Go's
    // big.Int.Bytes() (and Python's int.to_bytes).
    // -----------------------------------------------------------------------

    /** BigInteger -> minimal big-endian bytes. {@link BigInteger#toByteArray}
     * returns big-endian two's-complement and may carry an extra sign byte;
     * we drop it so the output is minimal. */
    private static byte[] toBigEndianBytes(BigInteger num) {
        if (num.signum() == 0) {
            return new byte[0];
        }
        byte[] twoc = num.toByteArray();
        int from = twoc[0] == 0 ? 1 : 0; // drop the sign byte when present
        return Arrays.copyOfRange(twoc, from, twoc.length);
    }

    // -----------------------------------------------------------------------
    // Base32 — RFC 4648 alphabet, padded to a multiple of 8 chars with '='.
    // -----------------------------------------------------------------------

    private static String encode32(byte[] data) {
        StringBuilder out = new StringBuilder();
        for (int i = 0; i < data.length; i += 5) {
            int n = Math.min(5, data.length - i);
            int[] b = new int[5];
            for (int j = 0; j < n; j++) {
                b[j] = data[i + j] & 0xFF;
            }
            // Pack 5 bytes (40 bits) into 8 base32 digits (5 bits each, big-endian).
            int[] 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 : OUT_LEN_32[n];
            for (int k = 0; k < outLen; k++) {
                out.append(B32_ALPHABET.charAt(digits[k]));
            }
            for (int k = outLen; k < 8; k++) {
                out.append('=');
            }
        }
        return out.toString();
    }

    private static byte[] decode32(String s) {
        // null return = invalid input (the TS lib's `null`).
        java.util.List<Byte> out = new java.util.ArrayList<>();
        int buffer = 0;
        int bits = 0;
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            if (c == '=') {
                break; // padding marks the end
            }
            int idx = B32_ALPHABET.indexOf(c);
            if (idx < 0) {
                return null;
            }
            buffer = (buffer << 5) | idx;
            bits += 5;
            if (bits >= 8) {
                bits -= 8;
                out.add((byte) ((buffer >> bits) & 0xFF));
                buffer &= (1 << bits) - 1; // keep only the leftover bits
            }
        }
        byte[] bytes = new byte[out.size()];
        for (int i = 0; i < bytes.length; i++) {
            bytes[i] = out.get(i);
        }
        return bytes;
    }

    // -----------------------------------------------------------------------
    // 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.shiftLeft(8).or(BigInteger.valueOf(data[i] & 0xFF));
        }
        // Base-convert to 58 digits (collected least-significant first).
        StringBuilder digits = new StringBuilder();
        while (num.signum() > 0) {
            BigInteger[] qr = num.divideAndRemainder(BASE_58);
            num = qr[0];
            digits.append(B58_ALPHABET.charAt(qr[1].intValue()));
        }
        return "1".repeat(zeros) + digits.reverse();
    }

    private static byte[] decode58(String s) {
        // Count leading '1's — each maps to a 0x00 byte.
        int zeros = 0;
        while (zeros < s.length() && s.charAt(zeros) == '1') {
            zeros++;
        }
        BigInteger num = BigInteger.ZERO;
        for (int i = zeros; i < s.length(); i++) {
            int idx = B58_ALPHABET.indexOf(s.charAt(i));
            if (idx < 0) {
                return null;
            }
            num = num.multiply(BASE_58).add(BigInteger.valueOf(idx));
        }
        // BigInteger -> minimal big-endian bytes.
        byte[] body = toBigEndianBytes(num);
        byte[] out = new byte[zeros + body.length];
        System.arraycopy(body, 0, out, zeros, body.length);
        return out;
    }

    // -----------------------------------------------------------------------
    // 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;
        for (byte datum : data) {
            num = num.shiftLeft(8).or(BigInteger.valueOf(datum & 0xFF));
        }
        if (num.signum() == 0) {
            return "0";
        }
        StringBuilder digits = new StringBuilder();
        while (num.signum() > 0) {
            BigInteger[] qr = num.divideAndRemainder(BASE_62);
            num = qr[0];
            digits.append(B62_ALPHABET.charAt(qr[1].intValue()));
        }
        return digits.reverse().toString();
    }

    private static byte[] decode62(String s) {
        if (s.isEmpty()) {
            return new byte[0];
        }
        BigInteger num = BigInteger.ZERO;
        for (int i = 0; i < s.length(); i++) {
            int idx = B62_ALPHABET.indexOf(s.charAt(i));
            if (idx < 0) {
                return null;
            }
            num = num.multiply(BASE_62).add(BigInteger.valueOf(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) {
        StringBuilder out = new StringBuilder();
        for (int i = 0; i < data.length; i += 4) {
            int n = Math.min(4, data.length - i);
            boolean isFull = n == 4;
            long[] b = new long[4];
            for (int j = 0; j < n; j++) {
                b[j] = data[i + j] & 0xFFL;
            }
            long u = b[0] * 16777216L + b[1] * 65536L + b[2] * 256L + b[3];
            if (isFull && u == 0) {
                out.append('z'); // zero-group shorthand
                continue;
            }
            long[] digits = new long[5];
            long 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) {
        java.util.List<byte[]> out = new java.util.ArrayList<>();
        java.util.List<Integer> group = new java.util.ArrayList<>(); // digit values (0..84)
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            if (c == 'z') {
                // 'z' is only valid at a group boundary (an empty accumulator).
                if (!group.isEmpty()) {
                    return null;
                }
                out.add(new byte[] {0, 0, 0, 0});
                continue;
            }
            if (c < 33 || c > 117) {
                return null;
            }
            group.add(c - 33);
            if (group.size() == 5) {
                long v = 0;
                for (int d : group) {
                    v = v * 85 + d;
                }
                if (v > 0xFFFFFFFFL) {
                    return null; // a 5-char group must fit in 32 bits
                }
                out.add(new byte[] {
                    (byte) ((v >> 24) & 0xFF),
                    (byte) ((v >> 16) & 0xFF),
                    (byte) ((v >> 8) & 0xFF),
                    (byte) (v & 0xFF),
                });
                group.clear();
            }
        }
        // Handle a partial final group (2-4 chars -> 1-3 bytes).
        if (!group.isEmpty()) {
            int m = group.size();
            if (m < 2) {
                return null; // a lone trailing char is malformed
            }
            while (group.size() < 5) {
                group.add(84); // pad with 'u'
            }
            long v = 0;
            for (int d : group) {
                v = v * 85 + d;
            }
            if (v > 0xFFFFFFFFL) {
                return null;
            }
            byte[] all = {
                (byte) ((v >> 24) & 0xFF),
                (byte) ((v >> 16) & 0xFF),
                (byte) ((v >> 8) & 0xFF),
                (byte) (v & 0xFF),
            };
            out.add(Arrays.copyOfRange(all, 0, m - 1));
        }
        int total = 0;
        for (byte[] part : out) {
            total += part.length;
        }
        byte[] bytes = new byte[total];
        int pos = 0;
        for (byte[] part : out) {
            System.arraycopy(part, 0, bytes, pos, part.length);
            pos += part.length;
        }
        return bytes;
    }

    // -----------------------------------------------------------------------
    // Public API
    // -----------------------------------------------------------------------

    /** Dispatch raw bytes to the chosen scheme's encoder. Mirrors the Go
     * twin's private {@code encodeBytes}. */
    private static String encodeBytes(byte[] data, Scheme scheme) {
        switch (scheme) {
            case BASE32: return encode32(data);
            case BASE58: return encode58(data);
            case BASE62: return encode62(data);
            case BASE85: return encode85(data);
            default: return "";
        }
    }

    /** Dispatch an encoded string to the chosen scheme's decoder. An invalid
     * or malformed input yields null (mirroring the TS {@code null}).
     * Mirrors the Go twin's private {@code decodeBytes}. */
    private static byte[] decodeBytes(String encoded, Scheme scheme) {
        switch (scheme) {
            case BASE32: return decode32(encoded);
            case BASE58: return decode58(encoded);
            case BASE62: return decode62(encoded);
            case BASE85: return decode85(encoded);
            default: return null;
        }
    }

    /** Returns the chosen-scheme encoding of the UTF-8 bytes of {@code text}.
     * Empty text encodes to "". It is the Java twin of {@code Encode} in
     * cli/base-encoder/base-encoder.go. */
    public static String encode(String text, Scheme scheme) {
        return encodeBytes(text.getBytes(StandardCharsets.UTF_8), scheme);
    }

    /** Reverses an encoded string back to UTF-8 text. Invalid characters or a
     * malformed structure yield {@link Optional#empty()} — mirroring the Go
     * twin's {@code errInvalid} and the TS lib's {@code null}. It is the Java
     * twin of {@code Decode} in cli/base-encoder/base-encoder.go.
     *
     * <p>The decoded bytes are interpreted as UTF-8; the platform UTF-8
     * decoder replaces malformed input with U+FFD, so a
     * structurally-valid-but-non-UTF-8 payload never throws a second error
     * (mirroring Go's {@code string(data)}, which never fails). */
    public static Optional<String> decode(String encoded, Scheme scheme) {
        byte[] data = decodeBytes(encoded, scheme);
        if (data == null) {
            return Optional.empty();
        }
        return Optional.of(new String(data, StandardCharsets.UTF_8));
    }

    // -----------------------------------------------------------------------
    // Showcase self-test — mirrors cli/base-encoder/base-encoder_test.go
    // vectors. Run directly: `javac -encoding UTF-8 java.java && java
    // BaseEncoder`.
    // -----------------------------------------------------------------------
    public static void main(String[] args) {
        String nul = "\u0000";

        // Base32 — known values + RFC 4648 padding + case sensitivity.
        check(encode("hello", Scheme.BASE32).equals("NBSWY3DP"), "b32 hello");
        // 3 bytes -> 5 data chars + 3 '=' pads.
        check(encode("foo", Scheme.BASE32).equals("MZXW6==="), "b32 foo");
        check(decode("NBSWY3DP", Scheme.BASE32).equals(Optional.of("hello")), "b32 decode");
        // lowercase is not in the RFC 4648 alphabet
        check(decode("nbswy3dp", Scheme.BASE32).isEmpty(), "b32 lowercase");

        // Base58 — each leading 0x00 byte -> a leading '1'.
        check(encode(nul, Scheme.BASE58).equals("1"), "b58 zero byte");
        check(encode(nul + nul + "A", Scheme.BASE58).startsWith("11"), "b58 two zeros");
        check(decode("1", Scheme.BASE58).equals(Optional.of(nul)), "b58 decode 1");
        // round-trip preserves the leading zero bytes exactly
        check(decode(encode(nul + nul + "A", Scheme.BASE58), Scheme.BASE58)
                .equals(Optional.of(nul + nul + "A")), "b58 round-trip");

        // Base62 — plain big-int base conversion (no leading-zero preservation).
        check(encode("A", Scheme.BASE62).equals("13"), "b62 A"); // 1*62 + 3
        check(decode("13", Scheme.BASE62).equals(Optional.of("A")), "b62 decode");
        check(encode(nul, Scheme.BASE62).equals("0"), "b62 zero");
        // no leading-zero preservation: the minimal rep of 0 is empty
        check(decode("0", Scheme.BASE62).equals(Optional.of("")), "b62 minimal zero");

        // Base85 — Ascii85 'z' shorthand + 32-bit overflow rejection.
        check(encode("hello", Scheme.BASE85).equals("BOu!rDZ"), "b85 hello");
        check(encode(nul + nul + nul + nul, Scheme.BASE85).equals("z"), "b85 z");
        check(encode(nul.repeat(8), Scheme.BASE85).equals("zz"), "b85 zz");
        // a 5-char group must fit in 32 bits; "uuuuu" overflows
        check(decode("uuuuu", Scheme.BASE85).isEmpty(), "b85 overflow");
        // a lone trailing char is a malformed partial group
        check(decode("B", Scheme.BASE85).isEmpty(), "b85 lone char");

        // Cross-scheme — empty, multibyte round-trip, and invalid rejection.
        for (Scheme scheme : Scheme.values()) {
            check(encode("", scheme).isEmpty(), scheme + " empty");
            check(decode("", scheme).equals(Optional.of("")), scheme + " empty decode");
            // multibyte UTF-8 round-trips through every scheme
            check(decode(encode("CosmoDev 🚀", scheme), scheme)
                    .equals(Optional.of("CosmoDev 🚀")), scheme + " multibyte");
            // '~' is outside every supported alphabet
            check(decode("~!not-valid!~", scheme).isEmpty(), scheme + " invalid");
        }

        System.out.println("ok");
    }

    private static void check(boolean ok, String name) {
        if (!ok) {
            throw new AssertionError("FAIL: " + name);
        }
    }
}

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 →