How do I encode and decode Base64 data in Rust?

Walkthrough

The base64 crate provides Base64 encoding and decoding for Rust. Base64 is a binary-to-text encoding scheme that represents binary data as ASCII characters. It's commonly used for encoding binary data in JSON, URLs, email attachments, and other text-based protocols. The crate supports multiple Base64 variants (standard, URL-safe, etc.) and configurable character sets.

Key concepts:

  1. Standard Base64 — uses + and / with = padding
  2. URL-safe Base64 — uses - and _ instead of + and /
  3. Encoding — convert bytes to Base64 string
  4. Decoding — convert Base64 string back to bytes
  5. Configuration — different padding and character set options

Base64 encodes 3 bytes into 4 characters, expanding data by ~33%.

Code Example

# Cargo.toml
[dependencies]
base64 = "0.22"
use base64::{Engine as _, engine::general_purpose};
 
fn main() {
    // Simple encoding and decoding
    let data = b"Hello, World!";
    let encoded = general_purpose::STANDARD.encode(data);
    println!("Encoded: {}", encoded);
    
    let decoded = general_purpose::STANDARD.decode(&encoded).unwrap();
    println!("Decoded: {}", String::from_utf8_lossy(&decoded));
}

Basic Encoding and Decoding

use base64::{Engine as _, engine::general_purpose};
 
fn main() {
    // Standard Base64 encoding
    let original = "Hello, Rust!";
    let encoded = general_purpose::STANDARD.encode(original.as_bytes());
    println!("Original: {}", original);
    println!("Encoded: {}", encoded);
    
    // Standard Base64 decoding
    let decoded = general_purpose::STANDARD.decode(&encoded).unwrap();
    let decoded_str = String::from_utf8(decoded).unwrap();
    println!("Decoded: {}", decoded_str);
    
    // Encoding binary data
    let binary_data: Vec<u8> = vec![0, 1, 2, 255, 254, 253];
    let encoded = general_purpose::STANDARD.encode(&binary_data);
    println!("\nBinary encoded: {}", encoded);
    
    let decoded = general_purpose::STANDARD.decode(&encoded).unwrap();
    println!("Binary decoded: {:?}", decoded);
}

URL-Safe Base64

use base64::{Engine as _, engine::general_purpose};
 
fn main() {
    let data = b"Data with + and / characters";
    
    // Standard encoding (uses + 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);
    
    // Decode URL-safe
    let decoded = general_purpose::URL_SAFE.decode(&url_safe).unwrap();
    println!("Decoded: {:?}", String::from_utf8_lossy(&decoded));
    
    // The difference in characters
    let test_data = b"\xff\xff";  // Encodes to //+ in standard
    println!("\nStandard: {}", general_purpose::STANDARD.encode(test_data));
    println!("URL-safe: {}", general_purpose::URL_SAFE.encode(test_data));
}

Encoding Strings and Files

use base64::{Engine as _, engine::general_purpose};
use std::fs;
use std::path::Path;
 
fn main() {
    // Encode a string
    let text = "This is some text to encode";
    let encoded = general_purpose::STANDARD.encode(text.as_bytes());
    println!("Encoded text: {}", encoded);
    
    // Decode back to string
    let decoded_bytes = general_purpose::STANDARD.decode(&encoded).unwrap();
    let decoded_text = String::from_utf8(decoded_bytes).unwrap();
    println!("Decoded text: {}", decoded_text);
    
    // Encode JSON-like data
    let json_data = r#"{"user":"alice","id":42}"#;
    let encoded = general_purpose::STANDARD.encode(json_data.as_bytes());
    println!("\nEncoded JSON: {}", encoded);
    
    // Simulate file encoding
    let file_content = b"\x89PNG\r\n\x1a\n";  // PNG header
    let encoded = general_purpose::STANDARD.encode(file_content);
    println!("\nEncoded PNG header: {}", encoded);
    
    // Decode and verify
    let decoded = general_purpose::STANDARD.decode(&encoded).unwrap();
    assert_eq!(file_content.to_vec(), decoded);
    println!("Round-trip successful!");
}

Working with Different Data Types

use base64::{Engine as _, engine::general_purpose};
 
fn main() {
    // Encode numbers (as bytes)
    let number: u32 = 12345678;
    let bytes = number.to_be_bytes();
    let encoded = general_purpose::STANDARD.encode(bytes);
    println!("Encoded number: {}", encoded);
    
    let decoded_bytes = general_purpose::STANDARD.decode(&encoded).unwrap();
    let decoded_number = u32::from_be_bytes(
        decoded_bytes.try_into().unwrap()
    );
    println!("Decoded number: {}", decoded_number);
    
    // Encode a struct
    #[derive(Debug)]
    struct Point {
        x: f64,
        y: f64,
    }
    
    let point = Point { x: 10.5, y: 20.3 };
    let bytes: [u8; 16] = unsafe {
        // In real code, prefer bytemuck or zerocopy crates
        std::mem::transmute([point.x.to_be_bytes(), point.y.to_be_bytes()])
    };
    let encoded = general_purpose::STANDARD.encode(bytes);
    println!("\nEncoded point: {}", encoded);
    
    // Encode a vector of bytes
    let data = vec![0x00, 0xFF, 0xAB, 0xCD, 0xEF];
    let encoded = general_purpose::STANDARD.encode(&data);
    println!("\nEncoded vec: {}", encoded);
    
    // Encode empty data
    let empty: &[u8] = &[];
    let encoded = general_purpose::STANDARD.encode(empty);
    println!("Empty encoded: '{}'", encoded);
}

Custom Configuration

use base64::{Engine as _, engine::general_purpose::{self, GeneralPurpose}};
use base64::engine::{GeneralPurposeConfig, PaddingDirection};
 
fn main() {
    // Using pre-defined configurations
    let data = b"Hello";
    
    // Standard with padding (default)
    println!("STANDARD: {}", general_purpose::STANDARD.encode(data));
    
    // Standard without padding
    println!("STANDARD_NO_PAD: {}", general_purpose::STANDARD_NO_PAD.encode(data));
    
    // URL-safe with padding
    println!("URL_SAFE: {}", general_purpose::URL_SAFE.encode(data));
    
    // URL-safe without padding
    println!("URL_SAFE_NO_PAD: {}", general_purpose::URL_SAFE_NO_PAD.encode(data));
    
    // Custom configuration
    let custom_config = GeneralPurposeConfig::new()
        .with_encode_padding(true)
        .with_decode_padding_mode(base64::engine::DecodePaddingMode::Indifferent);
    
    let custom_engine = GeneralPurpose::new(
        &base64::alphabet::STANDARD,
        custom_config,
    );
    
    let encoded = custom_engine.encode(data);
    println!("\nCustom config: {}", encoded);
}

Error Handling

use base64::{Engine as _, engine::general_purpose};
use base64::DecodeError;
 
fn decode_safely(encoded: &str) -> Result<Vec<u8>, String> {
    general_purpose::STANDARD.decode(encoded)
        .map_err(|e| format!("Decode error: {}", e))
}
 
fn main() {
    // Valid encoding
    match decode_safely("SGVsbG8gV29ybGQ=") {
        Ok(data) => println!("Decoded: {:?}", String::from_utf8_lossy(&data)),
        Err(e) => println!("Error: {}", e),
    }
    
    // Invalid Base64 characters
    match decode_safely("Invalid!@#$") {
        Ok(data) => println!("Decoded: {:?}", data),
        Err(e) => println!("Error: {}", e),
    }
    
    // Invalid padding
    match decode_safely("SGVsbG8") {
        Ok(data) => println!("Decoded: {:?}", String::from_utf8_lossy(&data)),
        Err(e) => println!("Error: {}", e),
    }
    
    // Wrong padding length
    match decode_safely("SGVsbG8=") {
        Ok(data) => println!("Decoded: {:?}", String::from_utf8_lossy(&data)),
        Err(e) => println!("Error: {}", e),
    }
    
    // Empty string
    match decode_safely("") {
        Ok(data) => println!("Empty decoded: {:?}", data),
        Err(e) => println!("Error: {}", e),
    }
}

Base64 in JSON and APIs

use base64::{Engine as _, engine::general_purpose};
use serde::{Deserialize, Serialize};
 
#[derive(Serialize, Deserialize, Debug)]
struct Image {
    filename: String,
    #[serde(
        serialize_with = "serialize_base64",
        deserialize_with = "deserialize_base64"
    )]
    data: Vec<u8>,
}
 
fn serialize_base64<S>(data: &Vec<u8>, serializer: S) -> Result<S::Ok, S::Error>
where
    S: serde::Serializer,
{
    let encoded = general_purpose::STANDARD.encode(data);
    serializer.serialize_str(&encoded)
}
 
fn deserialize_base64<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let encoded = String::deserialize(deserializer)?;
    general_purpose::STANDARD.decode(&encoded)
        .map_err(serde::de::Error::custom)
}
 
fn main() {
    // Create an image struct
    let image = Image {
        filename: "photo.png".to_string(),
        data: vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A],  // PNG header
    };
    
    // Serialize to JSON
    let json = serde_json::to_string(&image).unwrap();
    println!("JSON: {}", json);
    
    // Deserialize from JSON
    let decoded: Image = serde_json::from_str(&json).unwrap();
    println!("Decoded: {:?}", decoded);
}

Base64 for Data URIs

use base64::{Engine as _, engine::general_purpose};
 
fn create_data_uri(mime_type: &str, data: &[u8]) -> String {
    let encoded = general_purpose::STANDARD.encode(data);
    format!("data:{};base64,{}", mime_type, encoded)
}
 
fn main() {
    // Create a data URI for a small image
    let png_data = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR"; // PNG header
    let data_uri = create_data_uri("image/png", png_data);
    println!("Data URI: {}", data_uri);
    
    // Create a data URI for text
    let text = b"Hello, World!";
    let data_uri = create_data_uri("text/plain", text);
    println!("Text URI: {}", data_uri);
    
    // Create a data URI for CSS
    let css = b"body { color: red; }";
    let data_uri = create_data_uri("text/css", css);
    println!("CSS URI: {}", data_uri);
    
    // Create inline HTML with embedded image
    let html = format!(
        r#"<img src="{}" alt="Embedded image">"#,
        create_data_uri("image/gif", b"GIF89a")
    );
    println!("HTML: {}", html);
}

Chunked Encoding for Large Files

use base64::{Engine as _, engine::general_purpose};
 
fn encode_chunked(data: &[u8], chunk_size: usize) -> String {
    data.chunks(chunk_size)
        .map(|chunk| general_purpose::STANDARD.encode(chunk))
        .collect::<Vec<_>>()
        .join("\n")
}
 
fn main() {
    // Simulate a large file
    let large_data: Vec<u8> = (0..=255).cycle().take(1000).collect();
    
    // Encode in chunks (for display purposes)
    let encoded = encode_chunked(&large_data, 300);
    println!("Encoded in chunks:");
    for (i, line) in encoded.lines().enumerate() {
        println!("  Chunk {}: {}... ({} chars)", i + 1, &line[..40.min(line.len())], line.len());
    }
    
    // Encode the whole thing at once
    let full_encoded = general_purpose::STANDARD.encode(&large_data);
    println!("\nFull encoded length: {} chars", full_encoded.len());
    
    // Decode and verify
    let decoded = general_purpose::STANDARD.decode(&full_encoded).unwrap();
    assert_eq!(large_data, decoded);
    println!("Round-trip verified!");
}

Base64 for Authentication Tokens

use base64::{Engine as _, engine::general_purpose};
 
fn create_basic_auth(username: &str, password: &str) -> String {
    let credentials = format!("{}:{}", username, password);
    let encoded = general_purpose::STANDARD.encode(credentials.as_bytes());
    format!("Basic {}", encoded)
}
 
fn parse_basic_auth(header: &str) -> Option<(String, String)> {
    let prefix = "Basic ";
    if !header.starts_with(prefix) {
        return None;
    }
    
    let encoded = &header[prefix.len()..];
    let decoded = general_purpose::STANDARD.decode(encoded).ok()?;
    let credentials = String::from_utf8(decoded).ok()?;
    
    let mut parts = credentials.splitn(2, ':');
    let username = parts.next()?.to_string();
    let password = parts.next()?.to_string();
    
    Some((username, password))
}
 
fn main() {
    // Create Basic Auth header
    let auth_header = create_basic_auth("alice", "secret123");
    println!("Authorization: {}", auth_header);
    
    // Parse Basic Auth header
    match parse_basic_auth(&auth_header) {
        Some((user, pass)) => println!("Decoded: user={}, password={}", user, pass),
        None => println!("Failed to parse"),
    }
    
    // Encode API token
    let api_token = "sk-1234567890abcdef";
    let encoded_token = general_purpose::URL_SAFE_NO_PAD.encode(api_token.as_bytes());
    println!("\nEncoded token: {}", encoded_token);
    
    let decoded_token = general_purpose::URL_SAFE_NO_PAD.decode(&encoded_token).unwrap();
    println!("Decoded token: {}", String::from_utf8_lossy(&decoded_token));
}

Comparing Encoding Options

use base64::{Engine as _, engine::general_purpose};
 
fn main() {
    let test_cases: Vec<&[u8]> = vec![
        b"",
        b"a",
        b"ab",
        b"abc",
        b"abcd",
        b"Hello, World!",
        b"\x00\xff\xab\xcd",
    ];
    
    println!("Comparing Base64 variants:\n");
    println!("{:-12} | {:-24} | {:-24} | {::-24}", 
             "Input", "STANDARD", "STANDARD_NO_PAD", "URL_SAFE_NO_PAD");
    println!("{}", "-".repeat(90));
    
    for data in test_cases {
        let preview = if data.is_empty() {
            "(empty)".to_string()
        } else if data.len() > 8 {
            format!("{:02x?}...", &data[..8])
        } else {
            format!("{:02x?}", data)
        };
        
        let standard = general_purpose::STANDARD.encode(data);
        let no_pad = general_purpose::STANDARD_NO_PAD.encode(data);
        let url_safe = general_purpose::URL_SAFE_NO_PAD.encode(data);
        
        println!("{:12} | {:24} | {:24} | {:24}", 
                 preview, standard, no_pad, url_safe);
    }
}

Streaming with Read/Write Traits

use base64::{Engine as _, engine::general_purpose};
use std::io::{self, Read, Write};
 
struct Base64Encoder {
    buffer: Vec<u8>,
}
 
impl Base64Encoder {
    fn new() -> Self {
        Base64Encoder { buffer: Vec::new() }
    }
    
    fn write(&mut self, data: &[u8]) {
        self.buffer.extend_from_slice(data);
    }
    
    fn finish(self) -> String {
        general_purpose::STANDARD.encode(&self.buffer)
    }
}
 
struct Base64Decoder {
    buffer: String,
}
 
impl Base64Decoder {
    fn new() -> Self {
        Base64Decoder { buffer: String::new() }
    }
    
    fn write(&mut self, data: &str) {
        self.buffer.push_str(data);
    }
    
    fn finish(self) -> Result<Vec<u8>, String> {
        general_purpose::STANDARD.decode(&self.buffer)
            .map_err(|e| format!("Decode error: {}", e))
    }
}
 
fn main() {
    // Simulated streaming encode
    let mut encoder = Base64Encoder::new();
    encoder.write(b"Hello, ");
    encoder.write(b"World!");
    let encoded = encoder.finish();
    println!("Encoded: {}", encoded);
    
    // Simulated streaming decode
    let mut decoder = Base64Decoder::new();
    decoder.write(&encoded);
    let decoded = decoder.finish().unwrap();
    println!("Decoded: {}", String::from_utf8_lossy(&decoded));
}

Summary

  • Use base64 crate for Base64 encoding/decoding
  • general_purpose::STANDARD.encode(data) for standard Base64 with padding
  • general_purpose::STANDARD_NO_PAD.encode(data) without padding
  • general_purpose::URL_SAFE.encode(data) for URL-safe Base64 (uses - and _)
  • general_purpose::URL_SAFE_NO_PAD.encode(data) for URL-safe without padding
  • engine.decode(encoded) returns Result<Vec<u8>, DecodeError>
  • Standard Base64 uses + and /, URL-safe uses - and _
  • Padding character = is added to make length a multiple of 4
  • Base64 expands data by approximately 33%
  • Use URL-safe variant when embedding in URLs or filenames
  • Handle decode errors gracefully with decode().map_err()
  • For JSON, create custom serialize/deserialize functions with serde
  • Use Base64 for data URIs, authentication tokens, and binary data in text formats
  • The Engine trait allows custom configurations and character sets