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 →