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 →