How do I work with Base64 for Encoding and Decoding in Rust?

Walkthrough

Base64 is a crate for encoding and decoding base64 data in Rust. Base64 is a binary-to-text encoding scheme that represents binary data in an ASCII string format. It's commonly used for encoding binary data in contexts that only support text, such as email attachments, JSON payloads, and data URLs.

Key concepts:

  • Encoding β€” Convert binary data to Base64 string
  • Decoding β€” Convert Base64 string back to binary
  • Character sets β€” Standard, URL-safe, and crypt variants
  • Line wrapping β€” Split output into multiple lines
  • Padding β€” Optional = padding characters

When to use Base64:

  • Encoding binary data for JSON/XMLδΌ θΎ“
  • Data URLs and web APIs
  • Email attachments (MIME)
  • Storing binary data in text formats
  • JWT tokens and cryptographic signatures

When NOT to use Base64:

  • When you need compact data (adds ~33% overhead)
  • For simple text encoding (use UTF-8)
  • For password storage (use proper hashing)

Code Examples

Basic Encoding and Decoding

use base64::{Engine as _, engine::general_purpose};
 
fn main() {
    let data = b"hello world";
    
    // Encode to base64
    let encoded = general_purpose::STANDARD.encode(data);
    println!("Encoded: {}", encoded);  // "aGVsbG8gd29ybGQ="
    
    // Decode from base64
    let decoded = general_purpose::STANDARD.decode(&encoded).unwrap();
    println!("Decoded: {}", String::from_utf8_lossy(&decoded));
}

URL-Safe Encoding

use base64::{Engine as _, engine::general_purpose};
 
fn main() {
    let data = b"hello world?param=value";
    
    // Standard encoding (contains + and /)
    let standard = general_purpose::STANDARD.encode(data);
    println!("Standard: {}", standard);
    
    // URL-safe encoding (uses - and _ instead)
    let url_safe = general_purpose::URL_SAFE.encode(data);
    println!("URL-safe: {}", url_safe);
    
    // URL-safe without padding
    let url_safe_no_pad = general_purpose::URL_SAFE_NO_PAD.encode(data);
    println!("URL-safe (no pad): {}", url_safe_no_pad);
}

No Padding

use base64::{Engine as _, engine::general_purpose};
 
fn main() {
    let data = b"hello";
    
    // With padding
    let with_pad = general_purpose::STANDARD.encode(data);
    println!("With padding: {}", with_pad);  // "aGVsbG8="
    
    // Without padding
    let no_pad = general_purpose::STANDARD_NO_PAD.encode(data);
    println!("No padding: {}", no_pad);  // "aGVsbG8"
    
    // Decoding works with or without padding
    let decoded = general_purpose::STANDARD.decode(&no_pad).unwrap();
    println!("Decoded: {:?}", decoded);
}

Encoding to String

use base64::{Engine as _, engine::general_purpose};
 
fn encode_string(input: &str) -> String {
    general_purpose::STANDARD.encode(input.as_bytes())
}
 
fn decode_to_string(encoded: &str) -> Option<String> {
    general_purpose::STANDARD
        .decode(encoded)
        .ok()
        .and_then(|bytes| String::from_utf8(bytes).ok())
}
 
fn main() {
    let original = "Hello, Rust!";
    let encoded = encode_string(original);
    println!("Encoded: {}", encoded);
    
    let decoded = decode_to_string(&encoded).unwrap();
    println!("Decoded: {}", decoded);
}

Working with Binary Data

use base64::{Engine as _, engine::general_purpose};
 
fn main() {
    // Binary data (e.g., image header)
    let binary_data: Vec<u8> = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
    
    let encoded = general_purpose::STANDARD.encode(&binary_data);
    println!("Encoded binary: {}", encoded);
    
    let decoded = general_purpose::STANDARD.decode(&encoded).unwrap();
    assert_eq!(binary_data, decoded);
}

Streaming Encoding

use base64::{Engine as _, engine::general_purpose, write::EncoderWriter};
use std::io::Write;
 
fn main() -> std::io::Result<()> {
    // Encode while writing to a Vec
    let mut output = Vec::new();
    {
        let mut encoder = EncoderWriter::new(&mut output, &general_purpose::STANDARD);
        encoder.write_all(b"hello world")?;
    }
    
    let encoded = String::from_utf8(output).unwrap();
    println!("Streamed: {}", encoded);
    
    Ok(())
}

Streaming Decoding

use base64::{Engine as _, engine::general_purpose, read::DecoderReader};
use std::io::Read;
 
fn main() -> std::io::Result<()> {
    let encoded = b"aGVsbG8gd29ybGQ=";
    
    // Decode while reading
    let mut decoder = DecoderReader::new(&encoded[..], &general_purpose::STANDARD);
    let mut decoded = String::new();
    decoder.read_to_string(&mut decoded)?;
    
    println!("Decoded: {}", decoded);
    
    Ok(())
}

Custom Configuration

use base64::{Engine as _, engine::general_purpose::GeneralPurpose, alphabet, Config};
 
fn main() {
    // Custom configuration
    let config = Config::new()
        .with_encode_padding(true)
        .with_decode_padding_mode(base64::engine::DecodePaddingMode::Indifferent);
    
    let engine = GeneralPurpose::new(&alphabet::STANDARD, config);
    
    let encoded = engine.encode(b"custom config");
    println!("Custom encoded: {}", encoded);
}

JWT Token Encoding

use base64::{Engine as _, engine::general_purpose};
 
#[derive(Debug)]
struct JwtHeader {
    alg: String,
    typ: String,
}
 
impl JwtHeader {
    fn to_json(&self) -> String {
        format!(r#"{{"alg":"{}","typ":"{}"}}"#", self.alg, self.typ)
    }
}
 
fn encode_jwt_part(data: &str) -> String {
    // URL-safe without padding for JWT
    general_purpose::URL_SAFE_NO_PAD.encode(data.as_bytes())
}
 
fn main() {
    let header = JwtHeader {
        alg: "HS256".to_string(),
        typ: "JWT".to_string(),
    };
    
    let payload = r#"{"sub":"1234567890","name":"John"}"#;
    
    let header_b64 = encode_jwt_part(&header.to_json());
    let payload_b64 = encode_jwt_part(payload);
    
    println!("Header: {}", header_b64);
    println!("Payload: {}", payload_b64);
    println!("JWT: {}.{}.SIGNATURE", header_b64, payload_b64);
}

Data URL Generation

use base64::{Engine as _, engine::general_purpose};
 
fn create_data_url(mime_type: &str, data: &[u8]) -> String {
    let encoded = general_purpose::STANDARD.encode(data);
    format!("data:{};base64,{}", mime_type, encoded)
}
 
fn main() {
    // Small PNG-like header
    let image_data = b"\x89PNG\r\n\x1a\nfake image data";
    
    let data_url = create_data_url("image/png", image_data);
    println!("Data URL: {}", data_url);
}

Error Handling

use base64::{Engine as _, engine::general_purpose};
 
fn decode_safe(encoded: &str) -> Result<Vec<u8>, base64::DecodeError> {
    general_purpose::STANDARD.decode(encoded)
}
 
fn main() {
    // Valid input
    match decode_safe("aGVsbG8=") {
        Ok(data) => println!("Decoded: {:?}", data),
        Err(e) => eprintln!("Error: {}", e),
    }
    
    // Invalid input
    match decode_safe("not-valid-base64!!!") {
        Ok(data) => println!("Decoded: {:?}", data),
        Err(e) => eprintln!("Expected error: {}", e),
    }
}

Encoding Structs for Storage

use base64::{Engine as _, engine::general_purpose};
use serde::{Serialize, Deserialize};
 
#[derive(Serialize, Deserialize, Debug)]
struct UserSession {
    user_id: u64,
    username: String,
    expires_at: u64,
}
 
fn encode_session(session: &UserSession) -> Result<String, serde_json::Error> {
    let json = serde_json::to_string(session)?;
    Ok(general_purpose::STANDARD.encode(json.as_bytes()))
}
 
fn decode_session(encoded: &str) -> Result<UserSession, Box<dyn std::error::Error>> {
    let decoded = general_purpose::STANDARD.decode(encoded)?;
    let json = String::from_utf8(decoded)?;
    Ok(serde_json::from_str(&json)?)
}
 
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let session = UserSession {
        user_id: 42,
        username: "alice".to_string(),
        expires_at: 1700000000,
    };
    
    let encoded = encode_session(&session)?;
    println!("Encoded session: {}", encoded);
    
    let decoded = decode_session(&encoded)?;
    println!("Decoded: {:?}", decoded);
    
    Ok(())
}

Hex to Base64 Conversion

use base64::{Engine as _, engine::general_purpose};
 
fn hex_to_base64(hex: &str) -> Result<String, hex::FromHexError> {
    let bytes = hex::decode(hex)?;
    Ok(general_purpose::STANDARD.encode(&bytes))
}
 
fn base64_to_hex(encoded: &str) -> Result<String, base64::DecodeError> {
    let bytes = general_purpose::STANDARD.decode(encoded)?;
    Ok(hex::encode(&bytes))
}
 
fn main() {
    let hex_str = "48656c6c6f";  // "Hello" in hex
    
    match hex_to_base64(hex_str) {
        Ok(b64) => println!("Base64: {}", b64),
        Err(e) => eprintln!("Error: {}", e),
    }
}

Cryptographic Key Encoding

use base64::{Engine as _, engine::general_purpose};
 
struct CryptoKey {
    key_data: Vec<u8>,
}
 
impl CryptoKey {
    fn new(data: Vec<u8>) -> Self {
        Self { key_data: data }
    }
    
    fn to_base64(&self) -> String {
        general_purpose::STANDARD.encode(&self.key_data)
    }
    
    fn from_base64(encoded: &str) -> Result<Self, base64::DecodeError> {
        let decoded = general_purpose::STANDARD.decode(encoded)?;
        Ok(Self { key_data: decoded })
    }
}
 
fn main() {
    // 32-byte key
    let key = CryptoKey::new(vec![0u8; 32]);
    let encoded = key.to_base64();
    
    println!("Encoded key: {}", encoded);
    
    let decoded = CryptoKey::from_base64(&encoded).unwrap();
    assert_eq!(key.key_data, decoded.key_data);
}

Email Attachment Encoding

use base64::{Engine as _, engine::general_purpose};
 
struct EmailAttachment {
    filename: String,
    mime_type: String,
    data: Vec<u8>,
}
 
impl EmailAttachment {
    fn to_mime(&self) -> String {
        let encoded = general_purpose::STANDARD.encode(&self.data);
        let chunked = encoded
            .as_bytes()
            .chunks(76)  // Standard MIME line length
            .map(|chunk| std::str::from_utf8(chunk).unwrap())
            .collect::<Vec<_>>()
            .join("\r\n");
        
        format!(
            "Content-Type: {}\r\n\
             Content-Transfer-Encoding: base64\r\n\
             Content-Disposition: attachment; filename=\"{}\"\r\n\
             \r\n{}",
            self.mime_type,
            self.filename,
            chunked
        )
    }
}
 
fn main() {
    let attachment = EmailAttachment {
        filename: "document.txt".to_string(),
        mime_type: "text/plain".to_string(),
        data: b"Hello, this is an attachment.".to_vec(),
    };
    
    println!("{}", attachment.to_mime());
}

Benchmark Comparison

use base64::{Engine as _, engine::general_purpose};
 
fn main() {
    let data = vec![0u8; 1_000_000];  // 1 MB
    
    // Encoding speed
    let start = std::time::Instant::now();
    let encoded = general_purpose::STANDARD.encode(&data);
    let encode_time = start.elapsed();
    
    // Decoding speed
    let start = std::time::Instant::now();
    let decoded = general_purpose::STANDARD.decode(&encoded).unwrap();
    let decode_time = start.elapsed();
    
    println!("Original size: {} bytes", data.len());
    println!("Encoded size: {} bytes", encoded.len());
    println!("Overhead: {:.1}%", (encoded.len() as f64 / data.len() as f64 - 1.0) * 100.0);
    println!("Encode time: {:?}", encode_time);
    println!("Decode time: {:?}", decode_time);
}

Validating Base64 Input

use base64::{Engine as _, engine::general_purpose};
 
fn is_valid_base64(input: &str) -> bool {
    general_purpose::STANDARD.decode(input).is_ok()
}
 
fn main() {
    let test_cases = [
        ("aGVsbG8=", true),
        ("SGVsbG8gV29ybGQ=", true),
        ("invalid!!!", false),
        ("not base64", false),
    ];
    
    for (input, expected) in test_cases {
        let result = is_valid_base64(input);
        println!("{}: {} (expected {})", input, result, expected);
    }
}

Image Data Encoding

use base64::{Engine as _, engine::general_purpose};
 
fn encode_image_to_data_url(image_data: &[u8], format: &str) -> String {
    let encoded = general_purpose::STANDARD.encode(image_data);
    format!("data:image/{};base64,{}", format, encoded)
}
 
fn main() {
    // Fake PNG signature + data
    let png_data = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR";
    
    let data_url = encode_image_to_data_url(png_data, "png");
    println!("Data URL (truncated): {}...", &data_url[..50]);
}

API Key Encoding

use base64::{Engine as _, engine::general_purpose};
 
struct ApiKey {
    prefix: String,
    key: Vec<u8>,
}
 
impl ApiKey {
    fn new(prefix: &str, key: Vec<u8>) -> Self {
        Self {
            prefix: prefix.to_string(),
            key,
        }
    }
    
    fn encode(&self) -> String {
        format!("{}.{}", self.prefix, general_purpose::URL_SAFE_NO_PAD.encode(&self.key))
    }
    
    fn decode(s: &str) -> Option<Self> {
        let parts: Vec<&str> = s.splitn(2, '.').collect();
        if parts.len() != 2 {
            return None;
        }
        
        let key = general_purpose::URL_SAFE_NO_PAD.decode(parts[1]).ok()?;
        Some(Self {
            prefix: parts[0].to_string(),
            key,
        })
    }
}
 
fn main() {
    let api_key = ApiKey::new("sk", vec![1, 2, 3, 4, 5]);
    let encoded = api_key.encode();
    println!("API Key: {}", encoded);
    
    let decoded = ApiKey::decode(&encoded).unwrap();
    assert_eq!(api_key.key, decoded.key);
}

Summary

Base64 Key Imports:

use base64::{Engine as _, engine::general_purpose};

Encoding Engines:

Engine Description
STANDARD Standard base64 with +/ and padding
STANDARD_NO_PAD Standard without padding
URL_SAFE URL-safe with -_ and padding
URL_SAFE_NO_PAD URL-safe without padding

Core Methods:

// Encoding
let encoded = general_purpose::STANDARD.encode(data);
 
// Decoding
let decoded = general_purpose::STANDARD.decode(&encoded)?;

Character Sets:

Type Characters
Standard A-Za-z0-9+/
URL-safe A-Za-z0-9-_
Padding =

Common Use Cases:

Use Case Recommended Engine
General purpose STANDARD
URLs/Filenames URL_SAFE_NO_PAD
JWT tokens URL_SAFE_NO_PAD
Email/MIME STANDARD

Key Points:

  • Base64 adds ~33% size overhead
  • Use URL-safe encoding for URLs and filenames
  • JWT uses URL-safe without padding
  • Decode handles both padded and unpadded input
  • Streaming available for large data
  • Use Engine trait for custom configurations
  • Error handling returns DecodeError