Base32 / Base58 / Base62 / Base85 Encoder — Swift 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 Swift 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: Swift (5.9, 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 traps (encode always succeeds, decode
// returns nil 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 big-integer package (attaswift/BigInt
// is the ecosystem equivalent of Go's math/big).
//
// Arbitrary-precision note: Base58 and Base62 base-convert the whole byte
// array, which overflows any fixed-width integer for inputs longer than a
// few bytes. Swift has no stdlib big integer, so — exactly like the Rust
// sibling — we implement the same idea with a little-endian base-256
// [UInt8] and two primitives: divmodSmall (peel a base-N digit off the
// little end) and muladdSmall (reassemble a number from its base-N digits).
/// Selects a byte-array base encoding. Mirrors the Go twin's `Scheme` type
/// (and the TS `Scheme` union "base32" | "base58" | "base62" | "base85").
enum Scheme {
case base32, base58, base62, base85
}
enum BaseEncoder {
static let b32Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
static let b58Alphabet =
"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
static let b62Alphabet =
"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.
static let outLen32 = [0, 2, 4, 5, 7]
// ASCII-only alphabets, as byte arrays for O(1) indexing.
private static let b32 = Array(b32Alphabet.utf8)
private static let b58 = Array(b58Alphabet.utf8)
private static let b62 = Array(b62Alphabet.utf8)
// -----------------------------------------------------------------------
// Arbitrary-precision primitives (base-256, little-endian). Used by
// Base58 and Base62 so the port stays dependency-free.
// -----------------------------------------------------------------------
/// Divide a little-endian base-256 unsigned integer by a small `base`
/// (<= 256), storing the quotient back into `digits` (with high zero
/// limbs stripped) and returning the remainder. The long-division step
/// used to peel base-N digits off the little end during encoding.
private static func divmodSmall(_ digits: inout [UInt8], base: UInt32) -> UInt32 {
var rem: UInt32 = 0
for i in stride(from: digits.count - 1, through: 0, by: -1) {
let cur = rem * 256 + UInt32(digits[i])
digits[i] = UInt8(cur / base)
rem = cur % base
}
// Strip high (trailing in LE) zero limbs — keeps the representation
// minimal.
while let last = digits.last, last == 0 {
digits.removeLast()
}
return rem
}
/// Multiply a little-endian base-256 unsigned integer by `base` and add
/// `digit`, in place. The inverse of `divmodSmall`: reassembles a number
/// from its base-N digits (processed most-significant first).
private static func muladdSmall(_ digits: inout [UInt8], base: UInt32, digit: UInt32) {
var carry = digit
for i in 0..<digits.count {
let cur = UInt32(digits[i]) * base + carry
digits[i] = UInt8(truncatingIfNeeded: cur)
carry = cur >> 8
}
while carry > 0 {
digits.append(UInt8(carry & 0xff))
carry >>= 8
}
}
/// Little-endian base-256 -> minimal big-endian bytes (the form the
/// encoders emit and the decoders reconstruct). Strips any accidental
/// leading zero so the output matches Go's `big.Int.Bytes()` exactly.
private static func toBeBytes(_ digits: [UInt8]) -> [UInt8] {
var out = Array(digits.reversed())
while let first = out.first, first == 0 {
out.removeFirst()
}
return out
}
/// Position of a byte in an alphabet, nil when absent (the TS/Python
/// `.indexOf` / `.find` returning -1 or nil). Mirrors rust.rs's
/// `alpha.iter().position(...)`.
private static func indexOf(_ alphabet: [UInt8], _ c: UInt8) -> UInt32? {
for (i, a) in alphabet.enumerated() where a == c {
return UInt32(i)
}
return nil
}
// -----------------------------------------------------------------------
// Base32 — RFC 4648 alphabet, padded to a multiple of 8 chars with "=".
// -----------------------------------------------------------------------
static func encode32(_ data: [UInt8]) -> String {
var out = ""
var i = 0
while i < data.count {
let n = min(5, data.count - i)
var b = [UInt32](repeating: 0, count: 5)
for j in 0..<n {
b[j] = UInt32(data[i + j])
}
// Pack 5 bytes (40 bits) into 8 base32 digits (5 bits each, big-endian).
let digits: [UInt32] = [
(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,
]
let outLen = n == 5 ? 8 : outLen32[n]
for k in 0..<outLen {
out.append(Character(UnicodeScalar(b32[Int(digits[k])])))
}
for _ in outLen..<8 {
out.append("=")
}
i += 5
}
return out
}
static func decode32(_ s: String) -> [UInt8]? {
var out: [UInt8] = []
var buffer: UInt32 = 0
var bits = 0
for c in s.utf8 {
if c == 61 { // "=" — padding marks the end
break
}
guard let idx = indexOf(b32, c) else {
return nil
}
buffer = (buffer << 5) | idx
bits += 5
if bits >= 8 {
bits -= 8
out.append(UInt8((buffer >> bits) & 0xff))
buffer &= (1 << bits) - 1 // keep only the leftover bits
}
}
return out
}
// -----------------------------------------------------------------------
// Base58 — Bitcoin alphabet. Leading 0x00 bytes -> leading "1" (count
// preserved).
// -----------------------------------------------------------------------
static func encode58(_ data: [UInt8]) -> String {
// Count leading zero bytes — each maps to a leading "1".
var zeros = 0
while zeros < data.count && data[zeros] == 0 {
zeros += 1
}
// Big-endian byte array (skipping the leading zeros) -> LE base-256.
var le: [UInt8] = []
for i in zeros..<data.count {
muladdSmall(&le, base: 256, digit: UInt32(data[i]))
}
// Base-convert to 58 digits (collected least-significant first).
var digits: [UInt32] = []
while !le.isEmpty {
digits.append(divmodSmall(&le, base: 58))
}
var out = String(repeating: "1", count: zeros)
for d in digits.reversed() {
out.append(Character(UnicodeScalar(b58[Int(d)])))
}
return out
}
static func decode58(_ s: String) -> [UInt8]? {
let bytes = Array(s.utf8)
// Count leading "1"s — each maps to a 0x00 byte.
var zeros = 0
while zeros < bytes.count && bytes[zeros] == 49 {
zeros += 1
}
var le: [UInt8] = []
for c in bytes[zeros...] {
guard let idx = indexOf(b58, c) else {
return nil
}
muladdSmall(&le, base: 58, digit: idx)
}
// LE -> minimal big-endian bytes.
var out = [UInt8](repeating: 0, count: zeros)
out.append(contentsOf: toBeBytes(le))
return out
}
// -----------------------------------------------------------------------
// Base62 — standard base-conversion of the byte array (no leading-zero
// special-casing beyond the standard big-int).
// -----------------------------------------------------------------------
static func encode62(_ data: [UInt8]) -> String {
if data.isEmpty {
return ""
}
var le: [UInt8] = []
for byte in data {
muladdSmall(&le, base: 256, digit: UInt32(byte))
}
if le.isEmpty {
return "0" // value zero
}
var digits: [UInt32] = []
while !le.isEmpty {
digits.append(divmodSmall(&le, base: 62))
}
var out = ""
for d in digits.reversed() {
out.append(Character(UnicodeScalar(b62[Int(d)])))
}
return out
}
static func decode62(_ s: String) -> [UInt8]? {
if s.isEmpty {
return []
}
var le: [UInt8] = []
for c in s.utf8 {
guard let idx = indexOf(b62, c) else {
return nil
}
muladdSmall(&le, base: 62, digit: idx)
}
return toBeBytes(le)
}
// -----------------------------------------------------------------------
// 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).
// -----------------------------------------------------------------------
static func encode85(_ data: [UInt8]) -> String {
var out = ""
var i = 0
while i < data.count {
let n = min(4, data.count - i)
let isFull = n == 4
var b = [UInt32](repeating: 0, count: 4)
for j in 0..<n {
b[j] = UInt32(data[i + j])
}
let u = b[0] * 16777216 + b[1] * 65536 + b[2] * 256 + b[3]
i += 4
if isFull && u == 0 {
out.append("z") // zero-group shorthand
continue
}
var digits = [UInt32](repeating: 0, count: 5)
var v = u
for k in stride(from: 4, through: 0, by: -1) {
digits[k] = v % 85
v /= 85
}
let emit = isFull ? 5 : n + 1 // n bytes -> n+1 chars
for k in 0..<emit {
out.append(Character(UnicodeScalar(UInt8(digits[k] + 33))))
}
}
return out
}
static func decode85(_ s: String) -> [UInt8]? {
var out: [UInt8] = []
var group: [UInt32] = [] // accumulated digit values (0..84)
for c in s.utf8 {
if c == 122 { // "z" — only valid at a group boundary
if !group.isEmpty {
return nil
}
out.append(contentsOf: [0, 0, 0, 0])
continue
}
if c < 33 || c > 117 {
return nil
}
group.append(UInt32(c - 33))
if group.count == 5 {
var v: UInt64 = 0
for d in group {
v = v * 85 + UInt64(d)
}
if v > 0xFFFFFFFF {
return nil // a 5-char group must fit in 32 bits
}
out.append(UInt8((v >> 24) & 0xff))
out.append(UInt8((v >> 16) & 0xff))
out.append(UInt8((v >> 8) & 0xff))
out.append(UInt8(v & 0xff))
group.removeAll()
}
}
// Handle a partial final group (2-4 chars -> 1-3 bytes).
if !group.isEmpty {
let m = group.count
if m < 2 {
return nil // a lone trailing char is malformed
}
while group.count < 5 {
group.append(84) // pad with "u"
}
var v: UInt64 = 0
for d in group {
v = v * 85 + UInt64(d)
}
if v > 0xFFFFFFFF {
return nil
}
let all: [UInt8] = [
UInt8((v >> 24) & 0xff),
UInt8((v >> 16) & 0xff),
UInt8((v >> 8) & 0xff),
UInt8(v & 0xff),
]
out.append(contentsOf: all[0..<(m - 1)])
}
return out
}
// -----------------------------------------------------------------------
// Public API
// -----------------------------------------------------------------------
/// Dispatch raw bytes to the chosen scheme's encoder. Mirrors the Go
/// twin's private `encodeBytes`.
static func encodeBytes(_ data: [UInt8], _ 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)
}
}
/// Dispatch an encoded string to the chosen scheme's decoder. An invalid
/// or malformed input yields nil (mirroring the TS `null`). Mirrors the
/// Go twin's private `decodeBytes`.
static func decodeBytes(_ encoded: String, _ scheme: Scheme) -> [UInt8]? {
switch scheme {
case .base32: return decode32(encoded)
case .base58: return decode58(encoded)
case .base62: return decode62(encoded)
case .base85: return decode85(encoded)
}
}
/// Returns the chosen-scheme encoding of the UTF-8 bytes of `text`.
/// Empty text encodes to "". It is the Swift twin of `Encode` in
/// cli/base-encoder/base-encoder.go.
static func encode(_ text: String, _ scheme: Scheme) -> String {
encodeBytes(Array(text.utf8), scheme)
}
/// Reverses an encoded string back to UTF-8 text. Invalid characters or
/// a malformed structure yield nil — mirroring the Go twin's
/// `errInvalid` and the TS lib's `null`. It is the Swift twin of
/// `Decode` in cli/base-encoder/base-encoder.go.
///
/// The decoded bytes are interpreted as UTF-8; `String(decoding:as:)`
/// replaces ill-formed sequences with U+FFD, so a
/// structurally-valid-but-non-UTF-8 payload never fails a second time
/// (mirroring Go's `string(data)`, which never fails).
static func decode(_ encoded: String, _ scheme: Scheme) -> String? {
guard let data = decodeBytes(encoded, scheme) else {
return nil
}
return String(decoding: data, as: UTF8.self)
}
}
// ---------------------------------------------------------------------------
// Showcase self-test — mirrors cli/base-encoder/base-encoder_test.go vectors.
// Run directly: `swift swift.swift`.
// ---------------------------------------------------------------------------
func check(_ ok: Bool, _ name: String) {
if !ok {
fatalError("FAIL: \(name)")
}
}
let nul = "\u{0}"
// Base32 — known values + RFC 4648 padding + case sensitivity.
check(BaseEncoder.encode("hello", .base32) == "NBSWY3DP", "b32 hello")
// 3 bytes -> 5 data chars + 3 "=" pads.
check(BaseEncoder.encode("foo", .base32) == "MZXW6===", "b32 foo")
check(BaseEncoder.decode("NBSWY3DP", .base32) == "hello", "b32 decode")
// lowercase is not in the RFC 4648 alphabet
check(BaseEncoder.decode("nbswy3dp", .base32) == nil, "b32 lowercase")
// Base58 — each leading 0x00 byte -> a leading "1".
check(BaseEncoder.encode(nul, .base58) == "1", "b58 zero byte")
check(BaseEncoder.encode(nul + nul + "A", .base58).hasPrefix("11"), "b58 two zeros")
check(BaseEncoder.decode("1", .base58) == nul, "b58 decode 1")
// round-trip preserves the leading zero bytes exactly
check(
BaseEncoder.decode(BaseEncoder.encode(nul + nul + "A", .base58), .base58)
== nul + nul + "A",
"b58 round-trip")
// Base62 — plain big-int base conversion (no leading-zero preservation).
check(BaseEncoder.encode("A", .base62) == "13", "b62 A") // 1*62 + 3
check(BaseEncoder.decode("13", .base62) == "A", "b62 decode")
check(BaseEncoder.encode(nul, .base62) == "0", "b62 zero")
// no leading-zero preservation: the minimal rep of 0 is empty
check(BaseEncoder.decode("0", .base62) == "", "b62 minimal zero")
// Base85 — Ascii85 "z" shorthand + 32-bit overflow rejection.
check(BaseEncoder.encode("hello", .base85) == "BOu!rDZ", "b85 hello")
check(BaseEncoder.encode(nul + nul + nul + nul, .base85) == "z", "b85 z")
check(BaseEncoder.encode(String(repeating: nul, count: 8), .base85) == "zz", "b85 zz")
// a 5-char group must fit in 32 bits; "uuuuu" overflows
check(BaseEncoder.decode("uuuuu", .base85) == nil, "b85 overflow")
// a lone trailing char is a malformed partial group
check(BaseEncoder.decode("B", .base85) == nil, "b85 lone char")
// Cross-scheme — empty, multibyte round-trip, and invalid rejection.
for scheme in [Scheme.base32, .base58, .base62, .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 🚀", scheme), scheme)
== "CosmoDev 🚀",
"\(scheme) multibyte")
// "~" is outside every supported alphabet
check(BaseEncoder.decode("~!not-valid!~", scheme) == nil, "\(scheme) invalid")
}
print("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 →