Skip to content

Base64 Encode / Decode — Rust 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 Rust implementation — the same logic the interactive tool runs, in a shareable, citable form.

//! base64 — UTF-8 safe Base64 encode/decode.
//!
//! Language: Rust (std only — no external crates).
//! CosmoDev polyglot showcase port of the `base64` tool, ported from
//! src/lib/base64.ts (the canonical TypeScript implementation).
//!
//! Rust strings (`&str` / `String`) are guaranteed valid UTF-8, so
//! `s.as_bytes()` is already the UTF-8 byte sequence — there is no separate
//! "encode to UTF-8" step here, unlike the TypeScript port's TextEncoder.
//!
//! display source — part of CosmoDev's polyglot tool pages.

/// Standard Base64 alphabet (RFC 4648). The table position of each byte is its
/// 6-bit value. This is the same alphabet btoa emits in the browser.
const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

/// Sentinel for "this byte is not part of the Base64 alphabet".
const INVALID: i8 = -1;
/// Sentinel for "this byte is the '=' padding character".
const PADDING: i8 = -2;

/// Build a 256-entry lookup table mapping an ASCII byte to its 6-bit value, or
/// one of the sentinels. Constructed once and reused for every decode.
fn decode_table() -> [i8; 256] {
    let mut table = [INVALID; 256];
    for (value, &byte) in ALPHABET.iter().enumerate() {
        table[byte as usize] = value as i8;
    }
    table[b'=' as usize] = PADDING;
    table
}

/// Encode a Unicode string slice into standard, padded Base64.
///
/// Walks the UTF-8 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, matching `btoa` in the browser.
pub fn b64encode(input: &str) -> String {
    let bytes = input.as_bytes();
    let mut out = String::with_capacity((bytes.len() + 2) / 3 * 4);

    // Complete 3-byte chunks → four Base64 characters.
    let mut i = 0;
    while i + 3 <= bytes.len() {
        let triple = ((bytes[i] as u32) << 16)
            | ((bytes[i + 1] as u32) << 8)
            | (bytes[i + 2] as u32);
        out.push(ALPHABET[((triple >> 18) & 0x3F) as usize] as char);
        out.push(ALPHABET[((triple >> 12) & 0x3F) as usize] as char);
        out.push(ALPHABET[((triple >> 6) & 0x3F) as usize] as char);
        out.push(ALPHABET[(triple & 0x3F) as usize] as char);
        i += 3;
    }

    // Trailing 1 or 2 bytes, padded so the output stays a multiple of 4.
    let remainder = bytes.len() - i;
    match remainder {
        1 => {
            let triple = (bytes[i] as u32) << 16;
            out.push(ALPHABET[((triple >> 18) & 0x3F) as usize] as char);
            out.push(ALPHABET[((triple >> 12) & 0x3F) as usize] as char);
            out.push('=');
            out.push('=');
        }
        2 => {
            let triple = ((bytes[i] as u32) << 16) | ((bytes[i + 1] as u32) << 8);
            out.push(ALPHABET[((triple >> 18) & 0x3F) as usize] as char);
            out.push(ALPHABET[((triple >> 12) & 0x3F) as usize] as char);
            out.push(ALPHABET[((triple >> 6) & 0x3F) as usize] as char);
            out.push('=');
        }
        _ => {} // 0 remainder: nothing left to emit
    }

    out
}

/// Decode a standard Base64 string back into the original Unicode text.
///
/// Whitespace inside the input is stripped first via `char::is_whitespace`,
/// which mirrors JavaScript's `/\s/`, so line-wrapped Base64 decodes cleanly.
/// Any malformed input — an illegal character, an incorrect length/padding, or
/// decoded bytes that are not valid UTF-8 — is reported as an `Err`, matching
/// the TS port's "throw on invalid input" contract.
pub fn b64decode(input: &str) -> Result<String, String> {
    let table = decode_table();

    // Drop every whitespace character, then collect the surviving ASCII bytes.
    // Base64 input is ASCII, so narrowing to u8 after the whitespace filter is
    // safe for any well-formed input.
    let cleaned: Vec<u8> = input.chars().filter(|c| !c.is_whitespace()).map(|c| c as u8).collect();

    // Standard Base64 with padding is always a multiple of 4 characters.
    if cleaned.len() % 4 != 0 {
        return Err("invalid base64: length is not a multiple of 4".to_string());
    }

    // Count trailing '=' padding (0, 1, or 2 in well-formed input).
    let mut padding = 0usize;
    while padding < 2 && cleaned.len() - padding > 0 && cleaned[cleaned.len() - 1 - padding] == b'='
    {
        padding += 1;
    }

    let mut bytes = Vec::with_capacity(cleaned.len() * 3 / 4);
    let n = cleaned.len();
    let mut i = 0;
    while i < n {
        // Read four sextets, validating each against the lookup table.
        let mut sextets = [0u8; 4];
        let mut illegal_at: Option<usize> = None;
        for j in 0..4 {
            match table[cleaned[i + j] as usize] {
                PADDING => sextets[j] = 0, // padding contributes zero bits
                INVALID => {
                    illegal_at = Some(i + j);
                    break;
                }
                value => sextets[j] = value as u8,
            }
        }
        if let Some(pos) = illegal_at {
            return Err(format!("invalid base64: illegal character at byte {}", pos));
        }

        let triple = ((sextets[0] as u32) << 18)
            | ((sextets[1] as u32) << 12)
            | ((sextets[2] as u32) << 6)
            | (sextets[3] as u32);

        let is_last_group = i + 4 == n;
        bytes.push(((triple >> 16) & 0xFF) as u8); // byte 0: always present
        if !(is_last_group && padding == 2) {
            bytes.push(((triple >> 8) & 0xFF) as u8); // byte 1: absent only in a 2-pad final group (1 source byte)
        }
        if !(is_last_group && padding >= 1) {
            bytes.push((triple & 0xFF) as u8); // byte 2: absent whenever there is any padding
        }

        i += 4;
    }

    // Re-interpret the decoded bytes as UTF-8. Rust's String invariant is
    // "valid UTF-8", so we validate here — this is the role TextDecoder plays
    // in the TypeScript port.
    match String::from_utf8(bytes) {
        Ok(text) => Ok(text),
        Err(_) => Err("invalid base64: decoded bytes are not valid UTF-8".to_string()),
    }
}

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 →