Skip to content

Base64 Encode / Decode — Swift source

Encode text to Base64 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.

// base64 — UTF-8 safe Base64 encode/decode.
//
// Language: Swift (5.9, standard library only)
// Source:   CosmoDev polyglot showcase port of the `base64` tool, ported
//           from src/lib/base64.ts (the canonical TypeScript implementation);
//           algorithm and structure mirror src/tool-sources/base64/rust.rs.
// License:  display source — part of CosmoDev's polyglot tool pages.
//
// Foundation's Data(base64Encoded:) quietly ignores characters outside the
// alphabet — too lenient for this tool's strict "throw on invalid input"
// contract — so, like rust.rs, this port hand-rolls the codec: a 256-entry
// lookup table with sentinels, explicit length and padding checks, and a
// UTF-8 validity gate on the way out.
//
// Swift strings are natively Unicode: `input.utf8` is already the UTF-8 byte
// sequence Base64 operates on (the TextEncoder role), and
// String(validating:as:) returns nil for invalid UTF-8 (the TextDecoder
// role).
//
// Run: swift swift.swift

/// Standard Base64 alphabet (RFC 4648). The index of each byte is its 6-bit
/// value — the same alphabet btoa emits in the browser.
let alphabet = Array("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".utf8)

/// Sentinels for the decode table (a namespace avoids any case-pattern
/// ambiguity in the comparisons below).
enum Sextet {
    /// Value for "this byte is not part of the Base64 alphabet".
    static let invalid: Int8 = -1
    /// Value for "this byte is the '=' padding character".
    static let padding: Int8 = -2
}

/// A decode failure. Messages mirror the error strings of the TS/Rust ports.
enum Base64Error: Error, CustomStringConvertible {
    case illegalCharacter(at: Int)
    case badLength
    case invalidUtf8

    var description: String {
        switch self {
        case .illegalCharacter(let at):
            return "invalid base64: illegal character at byte \(at)"
        case .badLength:
            return "invalid base64: length is not a multiple of 4"
        case .invalidUtf8:
            return "invalid base64: decoded bytes are not valid UTF-8"
        }
    }
}

/// 256-entry lookup table mapping an ASCII byte to its 6-bit value or one of
/// the sentinels. Built once and reused for every decode.
let decodeTable: [Int8] = {
    var table = [Int8](repeating: Sextet.invalid, count: 256)
    for (value, byte) in alphabet.enumerated() {
        table[Int(byte)] = Int8(value)
    }
    table[Int(UInt8(ascii: "="))] = Sextet.padding
    return table
}()

/// Encode a Unicode string into standard, padded Base64 over its UTF-8 bytes.
///
/// Walks the bytes in 3-byte groups, emitting four 6-bit indices per group.
/// A trailing partial group (1 or 2 bytes) is padded with "=" so the output
/// length is always a multiple of 4 — the same shape as btoa in the browser.
func b64encode(_ input: String) -> String {
    let bytes = Array(input.utf8)
    var out: [UInt8] = []
    out.reserveCapacity((bytes.count + 2) / 3 * 4)

    var i = 0
    while i + 3 <= bytes.count {
        let triple = (UInt32(bytes[i]) << 16) | (UInt32(bytes[i + 1]) << 8)
            | UInt32(bytes[i + 2])
        out.append(alphabet[Int((triple >> 18) & 0x3F)])
        out.append(alphabet[Int((triple >> 12) & 0x3F)])
        out.append(alphabet[Int((triple >> 6) & 0x3F)])
        out.append(alphabet[Int(triple & 0x3F)])
        i += 3
    }

    // Trailing 1 or 2 bytes, padded so the output stays a multiple of 4.
    let remainder = bytes.count - i
    if remainder == 1 {
        let triple = UInt32(bytes[i]) << 16
        out.append(alphabet[Int((triple >> 18) & 0x3F)])
        out.append(alphabet[Int((triple >> 12) & 0x3F)])
        out.append(UInt8(ascii: "="))
        out.append(UInt8(ascii: "="))
    } else if remainder == 2 {
        let triple = (UInt32(bytes[i]) << 16) | (UInt32(bytes[i + 1]) << 8)
        out.append(alphabet[Int((triple >> 18) & 0x3F)])
        out.append(alphabet[Int((triple >> 12) & 0x3F)])
        out.append(alphabet[Int((triple >> 6) & 0x3F)])
        out.append(UInt8(ascii: "="))
    }

    return String(decoding: out, as: UTF8.self)
}

/// Decode standard Base64 back into the original Unicode text.
///
/// Whitespace inside the input is stripped first (Character.isWhitespace
/// matches everything \s does), so line-wrapped Base64 decodes cleanly. Any
/// malformed input — an illegal character, a length that is not a multiple
/// of 4, or decoded bytes that are not valid UTF-8 — throws, matching the
/// TS port's "throw on invalid input" contract.
func b64decode(_ input: String) throws -> String {
    // Drop every whitespace character, then collect the surviving ASCII bytes.
    let cleaned = Array(input.filter { !$0.isWhitespace }.utf8)

    // Standard Base64 with padding is always a multiple of 4 characters.
    if cleaned.count % 4 != 0 { throw Base64Error.badLength }

    // Count trailing "=" padding (0, 1, or 2 in well-formed input).
    var padding = 0
    while padding < 2, cleaned.count - padding > 0,
        cleaned[cleaned.count - 1 - padding] == UInt8(ascii: "=")
    {
        padding += 1
    }

    var bytes: [UInt8] = []
    bytes.reserveCapacity(cleaned.count * 3 / 4)

    let n = cleaned.count
    var i = 0
    while i < n {
        // Read four sextets, validating each against the lookup table.
        var sextets = [UInt8](repeating: 0, count: 4)
        for j in 0..<4 {
            let entry = decodeTable[Int(cleaned[i + j])]
            if entry == Sextet.padding {
                sextets[j] = 0  // padding contributes zero bits
            } else if entry == Sextet.invalid {
                throw Base64Error.illegalCharacter(at: i + j)
            } else {
                sextets[j] = UInt8(bitPattern: entry)
            }
        }

        let triple = (UInt32(sextets[0]) << 18) | (UInt32(sextets[1]) << 12)
            | (UInt32(sextets[2]) << 6) | UInt32(sextets[3])
        let isLastGroup = i + 4 == n

        bytes.append(UInt8((triple >> 16) & 0xFF))  // byte 0: always present
        if !(isLastGroup && padding == 2) {
            bytes.append(UInt8((triple >> 8) & 0xFF))  // byte 1
        }
        if !(isLastGroup && padding >= 1) {
            bytes.append(UInt8(triple & 0xFF))  // byte 2
        }

        i += 4
    }

    // Re-interpret the decoded bytes as UTF-8; nil means invalid.
    guard let text = String(validating: bytes, as: UTF8.self) else {
        throw Base64Error.invalidUtf8
    }
    return text
}

// Demo — `swift swift.swift`
print(b64encode("Hello, world!"))  // SGVsbG8sIHdvcmxkIQ==

do {
    print(try b64decode("aGVs\nbG8g d29ybGQ="))  // hello world
    print(try b64decode(b64encode("héllo 🌍")))  // multi-byte UTF-8 survives
} catch {
    print("unexpected failure: \(error)")
}

do {
    _ = try b64decode("SGVsbG8*")
} catch {
    print(error)
}

// "/w==" decodes to the single byte 0xFF, which is not valid UTF-8.
do {
    _ = try b64decode("/w==")
} catch {
    print(error)
}

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 →