Skip to content

Base32 / Base58 / Base62 / Base85 Encoder — Go 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 Go implementation — the same logic the interactive tool runs, in a shareable, citable form.

// Package baseencoder is the Go twin of CosmoDev's src/lib/base-encoder.ts
// (dual source: the web lib is TypeScript, the CLI lib is Go — kept in
// lock-step). Pure + deterministic, never panics. The table-driven tests in
// base-encoder_test.go share vectors with src/lib/base-encoder.test.ts so the
// two implementations are held to the same contract.
//
// All four schemes operate on the UTF-8 bytes of the input text, mirroring the
// TS lib exactly: Base32 (RFC 4648, padded), Base58 (Bitcoin, leading-zero
// preserving), Base62 (big.Int base-conversion, no leading-zero special-casing),
// and Base85 (Ascii85, 'z' shorthand for full zero groups).
package baseencoder

import (
	"errors"
	"math/big"
	"strings"
)

// Scheme selects a byte-array base encoding. It mirrors the TS `Scheme` union
// ('base32' | 'base58' | 'base62' | 'base85').
type Scheme string

const (
	Base32 Scheme = "base32"
	Base58 Scheme = "base58"
	Base62 Scheme = "base62"
	Base85 Scheme = "base85"
)

// errInvalid is returned when an encoded string contains a character outside
// the scheme's alphabet or is otherwise malformed. It mirrors the TS libs'
// `null` return from the internal decode helpers.
var errInvalid = errors.New("baseencoder: invalid or malformed input")

const (
	b32Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
	b58Alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
	b62Alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
)

// outLen32 maps the size of a final (partial) 5-byte chunk to the number of
// data characters it emits before '=' padding, per RFC 4648. Index = byte
// count (1..4). Matches `outLen = [0, 2, 4, 5, 7][chunk.length]` in the TS.
var outLen32 = [5]int{0, 2, 4, 5, 7}

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

func encode32(data []byte) string {
	var sb strings.Builder
	for i := 0; i < len(data); i += 5 {
		end := i + 5
		if end > len(data) {
			end = len(data)
		}
		chunk := data[i:end]
		var b [5]int
		for j := 0; j < len(chunk); j++ {
			b[j] = int(chunk[j])
		}
		// Pack 5 bytes (40 bits) into 8 base32 digits (5 bits each, big-endian).
		digits := [8]int{
			(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,
		}
		var group [8]byte
		for k, d := range digits {
			group[k] = b32Alphabet[d]
		}
		if len(chunk) == 5 {
			sb.Write(group[:])
		} else {
			n := outLen32[len(chunk)]
			sb.Write(group[:n])
			for p := 0; p < 8-n; p++ {
				sb.WriteByte('=')
			}
		}
	}
	return sb.String()
}

func decode32(s string) ([]byte, error) {
	var out []byte
	buffer := 0
	bits := 0
	for _, c := range s {
		if c == '=' {
			break // padding marks the end
		}
		idx := strings.IndexRune(b32Alphabet, c)
		if idx == -1 {
			return nil, errInvalid
		}
		buffer = (buffer << 5) | idx
		bits += 5
		if bits >= 8 {
			bits -= 8
			out = append(out, byte((buffer>>bits)&0xff))
			buffer &= (1 << bits) - 1 // keep only the leftover bits
		}
	}
	return out, nil
}

// ---------------------------------------------------------------------------
// Base58 — Bitcoin alphabet. Leading 0x00 bytes → leading '1' (count preserved).
// ---------------------------------------------------------------------------

func encode58(data []byte) string {
	// Count leading zero bytes — each maps to a leading '1'.
	zeros := 0
	for zeros < len(data) && data[zeros] == 0 {
		zeros++
	}
	// Big-endian byte array → big.Int (skipping the leading zeros).
	num := new(big.Int)
	for i := zeros; i < len(data); i++ {
		num.Lsh(num, 8)
		num.Or(num, big.NewInt(int64(data[i])))
	}
	// Base-convert to 58 digits (collected least-significant first).
	var digits []byte
	base := big.NewInt(58)
	v := new(big.Int).Set(num)
	for v.Sign() > 0 {
		mod := new(big.Int)
		v.DivMod(v, base, mod)
		digits = append(digits, byte(mod.Int64()))
	}
	var sb strings.Builder
	for i := 0; i < zeros; i++ {
		sb.WriteByte('1')
	}
	for i := len(digits) - 1; i >= 0; i-- {
		sb.WriteByte(b58Alphabet[digits[i]])
	}
	return sb.String()
}

func decode58(s string) ([]byte, error) {
	// Count leading '1's — each maps to a 0x00 byte.
	zeros := 0
	for zeros < len(s) && s[zeros] == '1' {
		zeros++
	}
	num := new(big.Int)
	base := big.NewInt(58)
	for i := zeros; i < len(s); i++ {
		idx := strings.IndexByte(b58Alphabet, s[i])
		if idx == -1 {
			return nil, errInvalid
		}
		num.Mul(num, base)
		num.Add(num, big.NewInt(int64(idx)))
	}
	// big.Int → big-endian bytes (minimal representation).
	body := num.Bytes()
	out := make([]byte, zeros+len(body))
	copy(out[zeros:], body)
	return out, nil
}

// ---------------------------------------------------------------------------
// Base62 — standard big.Int base-conversion of the byte array (no leading-zero
// special-casing beyond the standard big-int).
// ---------------------------------------------------------------------------

func encode62(data []byte) string {
	if len(data) == 0 {
		return ""
	}
	num := new(big.Int)
	for _, b := range data {
		num.Lsh(num, 8)
		num.Or(num, big.NewInt(int64(b)))
	}
	if num.Sign() == 0 {
		return "0"
	}
	var digits []byte
	base := big.NewInt(62)
	v := new(big.Int).Set(num)
	for v.Sign() > 0 {
		mod := new(big.Int)
		v.DivMod(v, base, mod)
		digits = append(digits, byte(mod.Int64()))
	}
	var sb strings.Builder
	for i := len(digits) - 1; i >= 0; i-- {
		sb.WriteByte(b62Alphabet[digits[i]])
	}
	return sb.String()
}

func decode62(s string) ([]byte, error) {
	if len(s) == 0 {
		return []byte{}, nil
	}
	num := new(big.Int)
	base := big.NewInt(62)
	for i := 0; i < len(s); i++ {
		idx := strings.IndexByte(b62Alphabet, s[i])
		if idx == -1 {
			return nil, errInvalid
		}
		num.Mul(num, base)
		num.Add(num, big.NewInt(int64(idx)))
	}
	return num.Bytes(), nil
}

// ---------------------------------------------------------------------------
// 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).
// ---------------------------------------------------------------------------

func encode85(data []byte) string {
	var sb strings.Builder
	for i := 0; i < len(data); i += 4 {
		end := i + 4
		if end > len(data) {
			end = len(data)
		}
		chunk := data[i:end]
		isFull := len(chunk) == 4
		var b [4]int
		for j := 0; j < len(chunk); j++ {
			b[j] = int(chunk[j])
		}
		u := b[0]*16777216 + b[1]*65536 + b[2]*256 + b[3]
		if isFull && u == 0 {
			sb.WriteByte('z') // zero-group shorthand
			continue
		}
		var digits [5]int
		v := u
		for k := 4; k >= 0; k-- {
			digits[k] = v % 85
			v /= 85
		}
		if isFull {
			for _, d := range digits {
				sb.WriteByte(byte(d + 33))
			}
		} else {
			n := len(chunk) + 1 // n bytes → n+1 chars
			for k := 0; k < n; k++ {
				sb.WriteByte(byte(digits[k] + 33))
			}
		}
	}
	return sb.String()
}

func decode85(s string) ([]byte, error) {
	var out []byte
	group := make([]int, 0, 5)
	for i := 0; i < len(s); i++ {
		c := s[i]
		if c == 'z' {
			// 'z' is only valid at a group boundary (an empty accumulator).
			if len(group) != 0 {
				return nil, errInvalid
			}
			out = append(out, 0, 0, 0, 0)
			continue
		}
		if c < 33 || c > 117 {
			return nil, errInvalid
		}
		group = append(group, int(c-33))
		if len(group) == 5 {
			v := 0
			for _, d := range group {
				v = v*85 + d
			}
			if v > 0xffffffff {
				return nil, errInvalid // a 5-char group must fit in 32 bits
			}
			out = append(out, byte((v>>24)&0xff), byte((v>>16)&0xff), byte((v>>8)&0xff), byte(v&0xff))
			group = group[:0]
		}
	}
	// Handle a partial final group (2–4 chars → 1–3 bytes).
	if len(group) > 0 {
		m := len(group)
		if m < 2 {
			return nil, errInvalid // a lone trailing char is malformed
		}
		for len(group) < 5 {
			group = append(group, 84) // pad with 'u'
		}
		v := 0
		for _, d := range group {
			v = v*85 + d
		}
		if v > 0xffffffff {
			return nil, errInvalid
		}
		all := [4]byte{byte((v >> 24) & 0xff), byte((v >> 16) & 0xff), byte((v >> 8) & 0xff), byte(v & 0xff)}
		out = append(out, all[:m-1]...)
	}
	return out, nil
}

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

// encodeBytes dispatches raw bytes to the chosen scheme's encoder.
func encodeBytes(data []byte, scheme Scheme) string {
	switch scheme {
	case Base32:
		return encode32(data)
	case Base58:
		return encode58(data)
	case Base62:
		return encode62(data)
	case Base85:
		return encode85(data)
	default:
		return ""
	}
}

// decodeBytes dispatches an encoded string to the chosen scheme's decoder. An
// invalid or malformed input yields errInvalid (mirroring the TS `null`).
func decodeBytes(encoded string, scheme Scheme) ([]byte, error) {
	switch scheme {
	case Base32:
		return decode32(encoded)
	case Base58:
		return decode58(encoded)
	case Base62:
		return decode62(encoded)
	case Base85:
		return decode85(encoded)
	default:
		return nil, errInvalid
	}
}

// Encode returns the chosen-scheme encoding of the UTF-8 bytes of text. Empty
// text encodes to "". It is the Go twin of encode() in src/lib/base-encoder.ts.
func Encode(text string, scheme Scheme) string {
	return encodeBytes([]byte(text), scheme)
}

// Decode reverses an encoded string back to UTF-8 text. Invalid characters or
// a malformed structure yield errInvalid — mirroring the TS lib's `null`. It
// is the Go twin of decode() in src/lib/base-encoder.ts.
//
// The decoded bytes are interpreted as UTF-8; for all shared round-trip
// vectors (which start from valid UTF-8 text) this matches the TS TextDecoder.
func Decode(encoded string, scheme Scheme) (string, error) {
	data, err := decodeBytes(encoded, scheme)
	if err != nil {
		return "", err
	}
	return string(data), nil
}

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 →