Base32 / Base58 / Base62 / Base85 Encoder — JavaScript 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 JavaScript 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.
*
* Language: JavaScript (ES2020+: BigInt + TextEncoder/TextDecoder; runs
* unmodified in Node 16+ and modern browsers)
* 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 (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: stdlib only (no npm dependencies).
*
* Arbitrary-precision note: Base58 and Base62 base-convert the whole byte
* array. JS has native BigInt, so we get the exact same semantics as the Go
* twin's math/big for free - no manual bignum code is needed (unlike the
* dependency-free Rust/PHP ports).
*/
'use strict';
/**
* One of the four supported byte-array base encodings. Mirrors the Go twin's
* `Scheme` type and the TS `Scheme` union.
* @typedef {('base32' | 'base58' | 'base62' | 'base85')} Scheme
*/
const B32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
const B58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
const 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 `outLen` table.
const OUT_LEN_32 = [0, 2, 4, 5, 7];
// ---------------------------------------------------------------------------
// Base32 - RFC 4648 alphabet, padded to a multiple of 8 chars with '='.
// ---------------------------------------------------------------------------
function encode32(bytes) {
let out = '';
for (let i = 0; i < bytes.length; i += 5) {
const chunk = bytes.subarray(i, i + 5);
const b = [0, 0, 0, 0, 0];
for (let j = 0; j < chunk.length; j++) b[j] = chunk[j];
// Pack 5 bytes (40 bits) into 8 base32 digits (5 bits each, big-endian).
const digits = [
(b[0] >> 3) & 0x1f,
((b[0] << 2) & 0x1f) | ((b[1] >> 6) & 0x1f),
(b[1] >> 1) & 0x1f,
((b[1] << 4) & 0x1f) | ((b[2] >> 4) & 0x1f),
((b[2] << 1) & 0x1f) | ((b[3] >> 7) & 0x1f),
(b[3] >> 2) & 0x1f,
((b[3] << 3) & 0x1f) | ((b[4] >> 5) & 0x1f),
b[4] & 0x1f,
];
let group = '';
for (const d of digits) group += B32_ALPHABET[d];
if (chunk.length < 5) {
// Truncate to the data chars and pad with '=' to 8 total.
const outLen = OUT_LEN_32[chunk.length];
group = group.slice(0, outLen) + '='.repeat(8 - outLen);
}
out += group;
}
return out;
}
function decode32(s) {
const bytes = [];
let buffer = 0;
let bits = 0;
for (const c of s) {
if (c === '=') break; // padding marks the end
const idx = B32_ALPHABET.indexOf(c);
if (idx === -1) return null;
buffer = (buffer << 5) | idx;
bits += 5;
if (bits >= 8) {
bits -= 8;
bytes.push((buffer >>> bits) & 0xff);
buffer &= (1 << bits) - 1; // keep only the leftover bits
}
}
return new Uint8Array(bytes);
}
// ---------------------------------------------------------------------------
// Base58 - Bitcoin alphabet. Leading 0x00 bytes -> leading '1' (count preserved).
// ---------------------------------------------------------------------------
function encode58(bytes) {
// Count leading zero bytes - each maps to a leading '1'.
let zeros = 0;
while (zeros < bytes.length && bytes[zeros] === 0) zeros++;
// Big-endian byte array (skipping the leading zeros) -> BigInt.
let num = 0n;
for (let i = zeros; i < bytes.length; i++) num = (num << 8n) | BigInt(bytes[i]);
// Base-convert to 58 digits (collected least-significant first).
const digits = [];
let v = num;
while (v > 0n) {
const rem = v % 58n;
digits.push(Number(rem));
v = v / 58n;
}
let out = '1'.repeat(zeros);
for (let i = digits.length - 1; i >= 0; i--) out += B58_ALPHABET[digits[i]];
return out;
}
function decode58(s) {
// Count leading '1's - each maps to a 0x00 byte.
let zeros = 0;
while (zeros < s.length && s[zeros] === '1') zeros++;
let num = 0n;
for (let i = zeros; i < s.length; i++) {
const idx = B58_ALPHABET.indexOf(s[i]);
if (idx === -1) return null;
num = num * 58n + BigInt(idx);
}
// BigInt -> big-endian bytes.
const bytes = [];
let v = num;
while (v > 0n) {
bytes.unshift(Number(v & 0xffn));
v = v >> 8n;
}
const out = new Uint8Array(zeros + bytes.length);
for (let i = 0; i < bytes.length; i++) out[zeros + i] = bytes[i];
return out;
}
// ---------------------------------------------------------------------------
// Base62 - standard BigInt base-conversion of the byte array (no leading-zero
// special-casing beyond the standard big-int).
// ---------------------------------------------------------------------------
function encode62(bytes) {
if (bytes.length === 0) return '';
let num = 0n;
for (const b of bytes) num = (num << 8n) | BigInt(b);
if (num === 0n) return '0';
const digits = [];
let v = num;
while (v > 0n) {
const rem = v % 62n;
digits.push(Number(rem));
v = v / 62n;
}
let out = '';
for (let i = digits.length - 1; i >= 0; i--) out += B62_ALPHABET[digits[i]];
return out;
}
function decode62(s) {
if (s.length === 0) return new Uint8Array(0);
let num = 0n;
for (const c of s) {
const idx = B62_ALPHABET.indexOf(c);
if (idx === -1) return null;
num = num * 62n + BigInt(idx);
}
const bytes = [];
let v = num;
while (v > 0n) {
bytes.unshift(Number(v & 0xffn));
v = v >> 8n;
}
return new Uint8Array(bytes);
}
// ---------------------------------------------------------------------------
// 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).
// ---------------------------------------------------------------------------
function encode85(bytes) {
let out = '';
for (let i = 0; i < bytes.length; i += 4) {
const chunk = bytes.subarray(i, i + 4);
const isFull = chunk.length === 4;
const b = [0, 0, 0, 0];
for (let j = 0; j < chunk.length; j++) b[j] = chunk[j];
const u = b[0] * 16777216 + b[1] * 65536 + b[2] * 256 + b[3];
if (isFull && u === 0) {
out += 'z'; // zero-group shorthand
continue;
}
const digits = [0, 0, 0, 0, 0];
let v = u;
for (let k = 4; k >= 0; k--) {
digits[k] = v % 85;
v = Math.floor(v / 85);
}
let chars = '';
for (const d of digits) chars += String.fromCharCode(d + 33);
if (!isFull) chars = chars.slice(0, chunk.length + 1); // n bytes -> n+1 chars
out += chars;
}
return out;
}
function decode85(s) {
const bytes = [];
let group = [];
for (let i = 0; i < s.length; i++) {
const c = s[i];
if (c === 'z') {
// 'z' is only valid at a group boundary (an empty accumulator).
if (group.length !== 0) return null;
bytes.push(0, 0, 0, 0);
continue;
}
const code = c.charCodeAt(0);
if (code < 33 || code > 117) return null;
group.push(code - 33);
if (group.length === 5) {
let v = 0;
for (const d of group) v = v * 85 + d;
if (v > 0xffffffff) return null; // a 5-char group must fit in 32 bits
bytes.push((v >>> 24) & 0xff, (v >>> 16) & 0xff, (v >>> 8) & 0xff, v & 0xff);
group = [];
}
}
// Handle a partial final group (2-4 chars -> 1-3 bytes).
if (group.length > 0) {
const m = group.length;
if (m < 2) return null; // a lone trailing char is malformed
while (group.length < 5) group.push(84); // pad with 'u'
let v = 0;
for (const d of group) v = v * 85 + d;
if (v > 0xffffffff) return null;
const all = [(v >>> 24) & 0xff, (v >>> 16) & 0xff, (v >>> 8) & 0xff, v & 0xff];
for (let k = 0; k < m - 1; k++) bytes.push(all[k]);
}
return new Uint8Array(bytes);
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
function encodeBytes(bytes, scheme) {
switch (scheme) {
case 'base32':
return encode32(bytes);
case 'base58':
return encode58(bytes);
case 'base62':
return encode62(bytes);
case 'base85':
return encode85(bytes);
default:
return '';
}
}
function decodeBytes(encoded, 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;
}
}
/**
* Encode the UTF-8 bytes of `text` per `scheme`. Empty text -> ''.
* Mirrors `Encode` in cli/base-encoder/base-encoder.go.
*
* @param {string} text
* @param {Scheme} scheme
* @returns {string}
*/
function encode(text, scheme) {
const bytes = new TextEncoder().encode(text);
return encodeBytes(bytes, scheme);
}
/**
* Decode `encoded` back to UTF-8 text. Invalid chars / malformed -> null
* (mirrors the Go twin's errInvalid and the TS lib's null).
* Mirrors `Decode` in cli/base-encoder/base-encoder.go.
*
* @param {string} encoded
* @param {Scheme} scheme
* @returns {string | null}
*/
function decode(encoded, scheme) {
const bytes = decodeBytes(encoded, scheme);
if (bytes === null) return null;
// TextDecoder is non-fatal by default, so a structurally-valid-but-non-UTF-8
// payload never throws a second error (mirrors Go's string(data)).
return new TextDecoder().decode(bytes);
}
// CommonJS export so the file is consumable from Node without a build step,
// while staying dependency-free and framework-agnostic.
module.exports = {
encode,
decode,
encodeBytes,
decodeBytes,
B32_ALPHABET,
B58_ALPHABET,
B62_ALPHABET,
};
// ---------------------------------------------------------------------------
// Showcase self-test - mirrors cli/base-encoder/base-encoder_test.go vectors.
// Run directly: `node js.js`
// ---------------------------------------------------------------------------
if (require.main === module) {
const assert = require('assert');
// Base32 - known values + RFC 4648 padding + case sensitivity.
assert.strictEqual(encode('hello', 'base32'), 'NBSWY3DP');
assert.strictEqual(encode('foo', 'base32'), 'MZXW6==='); // 3 bytes -> 5 chars + 3 '='
assert.strictEqual(decode('NBSWY3DP', 'base32'), 'hello');
assert.strictEqual(decode('nbswy3dp', 'base32'), null); // lowercase not in RFC 4648
// Base58 - each leading 0x00 byte -> a leading '1'.
assert.strictEqual(encode('\x00', 'base58'), '1');
assert.ok(encode('\x00\x00A', 'base58').startsWith('11'));
assert.strictEqual(decode('1', 'base58'), '\x00');
assert.strictEqual(decode(encode('\x00\x00A', 'base58'), 'base58'), '\x00\x00A');
// Base62 - plain big-int base conversion (no leading-zero preservation).
assert.strictEqual(encode('A', 'base62'), '13'); // 1*62 + 3
assert.strictEqual(decode('13', 'base62'), 'A');
assert.strictEqual(encode('\x00', 'base62'), '0');
assert.strictEqual(decode('0', 'base62'), ''); // minimal rep of 0 is empty
// Base85 - Ascii85 'z' shorthand + 32-bit overflow rejection.
assert.strictEqual(encode('hello', 'base85'), 'BOu!rDZ');
assert.strictEqual(encode('\x00\x00\x00\x00', 'base85'), 'z');
assert.strictEqual(encode('\x00'.repeat(8), 'base85'), 'zz');
assert.strictEqual(decode('uuuuu', 'base85'), null); // 5-char group overflows 32 bits
assert.strictEqual(decode('B', 'base85'), null); // lone trailing char is malformed
// Cross-scheme - empty, multibyte round-trip, and invalid rejection.
for (const scheme of ['base32', 'base58', 'base62', 'base85']) {
assert.strictEqual(encode('', scheme), '');
assert.strictEqual(decode('', scheme), '');
assert.strictEqual(decode(encode('CosmoDev 🚀', scheme), scheme), 'CosmoDev 🚀');
assert.strictEqual(decode('~!not-valid!~', scheme), null); // '~' outside every alphabet
}
console.log('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 →