Skip to content

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 →