¿Tiene que ocuparse del formato Base64? Entonces esta página es perfecta para Ud. Utilice nuestra práctica herramienta en línea para codificar o decodificar sus datos.

Decodificación Base64 en Rust: una guía completa

Una cadena aterriza en tu programa de Rust: letras y dígitos, el ocasional signo más o barra, y un par de = sospechosos colgando del final. Eso es Base64, y esta guía va de recuperar los bytes originales sin sorpresas. La página de inicio de este sitio recorre el formato a fondo, así que solo toca redecir la forma del intercambio: cuatro caracteres del alfabeto representan tres bytes de entrada, y una cola de uno o dos caracteres = marca dónde acabó el dato real. Decodificar es correr ese intercambio en reversa, y todo lo que sigue va de hacerlo con deliberación.

El único giro que separa a Rust de la mayoría de los lenguajes: la biblioteca estándar no trae ningún Base64 en absoluto. No hay ningún base64_decode() escondido en std, ni ningún use std::... que lo invoque. El ecosistema se decantó por una única crate llamada simplemente base64, y se convirtió en carga estructural: la versión 0.23.1 salió el 4 de agosto de 2026, la crate ha publicado 45 versiones desde su primera salida en diciembre de 2015, y su contador de descargas ronda los 1.500 millones. Ya casi con seguridad estás decodificando Base64 a través de esta crate, directamente o arrastrada por algo como jsonwebtoken, pem o serde_with, que dependen todas de ella.

La única crate y su círculo

Si Rust en sí aún no está en el equipo, tu sistema operativo lo trae: rustc y cargo en Debian y Ubuntu, un paquete o instalador en macOS y Windows, o el instalador oficial que configura rustup:

# Debian / Ubuntu
sudo apt install rustc cargo
# o el instalador oficial, que configura rustup y cargo
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Después la crate, dentro de cualquier proyecto de cargo. Esta única línea es toda la instalación, y arrastra exactamente cero dependencias:

cargo new my-app
cd my-app
cargo add base64

Tres características opcionales dan forma a la compilación. std va activada por defecto y habilita los tipos de streaming de std::io, las implementaciones estándar de Error y la asignación en el montón. alloc aporta las API con asignación para builds incrustados no_std sin biblioteca estándar completa. simd-unsafe va activada por defecto y controla los motores vectorizados que conocerás más adelante. La versión mínima soportada de Rust es 1.71.0, así que cualquier Rust reciente lo ejecutará. En órbita de la crate central gira un pequeño círculo de especialistas, cada uno para un extremo que el núcleo deja deliberadamente en tus manos:

Crate Versión (2026) Qué aporta Cuándo acudir a ella
base64ct 1.8 Decodificación en tiempo constante del proyecto RustCrypto; las API de montón van detrás de la característica alloc Los bytes que decodificas pueden filtrar información por timing, como material de claves
data-encoding 2.11 Base64 junto a base32, hex y compañía, con variantes MIME permisivas y codificación/decodificación a nivel de slice Un componente debe analizar entrada sucia, envuelta o de varios protocolos
base64-turbo 0.3 Un codec más nuevo que supera los 100 GiB/s en picos, con núcleos AVX512, AVX2 y NEON más un respaldo escalar seguro Todo va del caudal y el motor estándar deja ciclos en la mesa

Ninguno de estos reemplaza el trabajo del día a día. Para la inmensa mayoría de los programas de Rust, base64 por sí sola es la respuesta correcta y completa, y el resto de este artículo usa esa única crate para la decodificación en sí, acudiendo a los especialistas del círculo solo cuando el trabajo es más amplio que base64.

Tres líneas hasta tus bytes

El noventa por ciento de la vida decodificadora cabe en tres líneas. La prueba de humo canónica usa la célebre cadena TWFu:

use base64::prelude::*;
fn main() {
  let packed = "TWFu";
  let bytes = BASE64_STANDARD.decode(packed).expect("valid base64");
  println!("{}", String::from_utf8(bytes).expect("valid utf-8"));
}

La salida es Man, y tres cosas de esa pequeña ceremonia merecen quedarse. Primero, decode() siempre te entrega un Vec<u8>, nunca una cadena. Eso es una característica, no un accidente: Base64 puede transportar una frase, un JPEG o un certificado, y ninguno debe tratarse de forma distinta antes de saber qué tienes en las manos. Segundo, el salto de bytes a texto es un paso separado y deliberado a través de String::from_utf8(), y ese paso es donde vive la decisión de juego de caracteres. Tercero, el módulo prelude te entrega dos cosas a la vez sin hacer ruido: el motor BASE64_STANDARD y el trait Engine cuyos métodos estás llamando. Si prefieres importaciones explícitas, use base64::engine::general_purpose::STANDARD; junto con use base64::Engine; es la misma puerta con la placa del nombre.

Y porque algún día decodificarás algo que tú mismo codificaste, aquí está el viaje de ida y vuelta que demuestra que las dos direcciones coinciden. La codificación tiene su propia guía completa en el sitio hermano; aquí solo aparece para fabricar datos de prueba:

use base64::prelude::*;
fn main() {
  let packed = BASE64_STANDARD.encode("Hello, world!");
  println!("{packed}");                             // SGVsbG8sIHdvcmxkIQ==
  let back = BASE64_STANDARD.decode(packed).unwrap();
  println!("{}", String::from_utf8(back).unwrap()); // Hello, world!
}

Guárdate TWFu en el bolsillo como prueba de humo para cualquier ruta de decodificación que escribas: si convierte TWFu en Man, la máquina es honesta.

Cuatro formas de decir que no

Esta es la sección que te salva a las 2 de la madrugada, porque cuando una cadena de producción explota quieres saber con exactitud de qué se queja la crate. La buena noticia: se queja fuerte y con precisión. DecodeError tiene exactamente cuatro variantes, y así suena cada una ante una familia de transgresores típicos, todos alimentados a través del estricto motor estándar:

Entrada Qué falla Error exacto
"SGVs bG8s" coló un espacio Invalid symbol 32, offset 4.
"SGVs\nbG8s" coló un salto de línea Invalid symbol 10, offset 4.
"SG=VsbG8="" padding en medio de la cadena Invalid symbol 61, offset 2.
"SGVsbG8sIHdvcmxkIQ==xx" basura detrás del padding Invalid symbol 61, offset 18.
"S" un solo símbolo no puede formar un byte Invalid input length: 1
"SGV" tres símbolos sin el padding que debe seguirles Invalid padding
"SGVs$bG8="" una $ no está en el alfabeto Invalid symbol 36, offset 4.

Fíjate en cómo el mensaje Invalid symbol te dice tanto el valor del byte culpable como su desplazamiento, para que saltes directo a la escena del crimen. La variante InvalidLength es la quisquillosa: desde la versión 0.22.0 se dispara específicamente cuando el número de símbolos válidos es imposible, es decir, una longitud que es uno más que un múltiplo de cuatro, mientras que otras longitudes malas se manifiestan como errores de padding. Aquí está el match completo, para los días en que quieras tratar cada fallo de forma distinta:

use base64::DecodeError;
use base64::prelude::*;
fn triage(dirty: &str) {
  match BASE64_STANDARD.decode(dirty) {
    Ok(_) => println!("{dirty:?} sailed through"),
    Err(DecodeError::InvalidByte(offset, byte)) =>
      println!("{dirty:?}: symbol {byte} at {offset} is not in the alphabet"),
    Err(DecodeError::InvalidLength(symbols)) =>
      println!("{dirty:?}: {symbols} valid symbols is impossible"),
    Err(DecodeError::InvalidLastSymbol { offset, .. }) =>
      println!("{dirty:?}: trailing bits at {offset} suggest truncation"),
    Err(DecodeError::InvalidPadding) =>
      println!("{dirty:?}: padding is wrong or missing"),
  }
}

La estrictividad deliberada remonta al propio estándar. La sección 12 del RFC 4648 advierte que los caracteres fuera del alfabeto pueden malusarse como canal encubierto para sacar información fuera de banda, o para picar bugs en analizadores descuidados, y recomienda que los decodificadores los rechacen. La especificación MIME es la famosa excepción: dice explícitamente a los decodificadores que ignoren los caracteres errantes, y esa es la forma de entrada que la sección de correo de abajo te enseña a domar.

Tres posturas ante el padding

Cada cadena Base64 del mundo real hace una promesa silenciosa sobre el padding, y la versión 0.23 te deja elegir qué promesa aplicar a través del enum DecodePaddingMode. Hay tres modos, y la diferencia de comportamiento merece memorizarse. Aquí está la tabla de resultados para Zm8, que es la palabra fo sin su =:

Modo "Zm8", sin padding "Zm8=", con padding Cuándo usarlo
RequireCanonical, el defecto Err(Invalid padding) Ok([102, 111]) Tú produces y consumes los datos
Indifferent Ok([102, 111]) Ok([102, 111]) Recibes datos de fuentes mezcladas
RequireNone Ok([102, 111]) Err(Invalid padding) Aplicas un protocolo sin padding
use base64::engine::general_purpose::{GeneralPurpose, GeneralPurposeConfig, STANDARD_PAD_INDIFFERENT};
use base64::engine::DecodePaddingMode;
use base64::prelude::*;
let strict = BASE64_STANDARD;  // RequireCanonical es el defecto
let flexible = STANDARD_PAD_INDIFFERENT;
let bare = GeneralPurpose::new(
  &base64::alphabet::STANDARD,
  GeneralPurposeConfig::new().with_decode_padding_mode(DecodePaddingMode::RequireNone),
);
println!("{:?}", strict.decode("Zm8"));    // Err(Invalid padding)
println!("{:?}", flexible.decode("Zm8"));  // Ok([102, 111])
println!("{:?}", flexible.decode("Zm8=")); // Ok([102, 111])
println!("{:?}", bare.decode("Zm8="));     // Err(Invalid padding)

Los valores por defecto siguen al estándar: la sección 3.2 del RFC 4648 dice que las implementaciones deben incluir los caracteres de relleno apropiados al final de los datos codificados a menos que la especificación que los rodea diga lo contrario, y por eso un motor STANDARD de fábrica los exige. Y la elección importa por seguridad, no por mera pedantería. Aceptar tanto la grafía con padding como la sin padding de los mismos datos hace que Base64 sea maleable: la misma carga lógica puede escribirse de dos formas, y cualquier código que asuma una grafía canónica de un valor puede llevarse una sorpresa. El paper de 2022 "La maleabilidad de Base64 en la práctica" (Chatzigiannis y Chalkias, ePrint 2022/361) documenta consecuencias reales, y la propia documentación de la crate enlaza con él. La regla práctica: elige un modo por protocolo, y sé estricto en cada frontera donde no controlas al productor.

Los bits ocultos del último símbolo

Aquí hay una corrupción que sobrevive a todas las comprobaciones de caracteres. Cada símbolo Base64 lleva 6 bits, y 3 bytes de entrada (24 bits) se convierten en exactamente 4 símbolos. Cuando la entrada son solo 1 o 2 bytes, el símbolo final tiene bits sin usar, y el RFC es claro: los codificadores conformes deben poner esos bits sobrantes a cero. Un codificador con bug o malicioso puede en cambio dejar basura ahí, y el resultado sigue pasando la prueba del alfabeto, la de longitud y la de padding, mientras transporta en silencio una cola corrupta. El motor estricto te cubre, con un error de detalle único que hasta te muestra los bits sospechosos:

use base64::engine::general_purpose::{GeneralPurpose, GeneralPurposeConfig};
use base64::prelude::*;
println!("{:?}", BASE64_STANDARD.decode("MT=="));
// Err(Invalid last symbol 0x54 ('T') at offset 1, decoded as 0b00010011.)
let lenient = GeneralPurpose::new(
  &base64::alphabet::STANDARD,
  GeneralPurposeConfig::new().with_decode_allow_trailing_bits(true),
);
println!("{}", String::from_utf8_lossy(&lenient.decode("MT==").unwrap()));
// 1

Ese 0b00010011 del error es el valor decodificado del símbolo ofensor incluyendo los bits altos ilegales, y la versión 0.23.0 hizo visible ese detalle precisamente porque de otra forma es tan difícil de depurar. Si sabes que tus productores son descuidados, with_decode_allow_trailing_bits(true) se traga la basura en vez de rechazarla. Los navegadores apostaron por lo contrario: el algoritmo forgiving-base64 de WHATWG, el que está detrás de atob() de JavaScript, es explícitamente permisivo con los bits finales, mientras que el defecto de Rust es el perito forense. Sabrás en qué lado de la mesa estás sentado.

Base64url: el alfabeto que viaja

El Base64 estándar gasta sus dos últimas plazas del alfabeto en + y /, que es exactamente donde las URLs no los quieren: en una cadena de consulta, más significa espacio, la barra empieza un nuevo segmento de ruta, y un = colgando se lee como un separador. Así que la sección 5 del RFC 4648 define el alfabeto seguro para URLs y nombres de archivo, que cambia a los dos problemáticos por - y _ y, como la longitud suele poder recuperarse, suele soltarse también el padding. El RFC hasta advierte que esta codificación no debe considerarse la misma que el Base64 estándar, y los nombres de los motores están de acuerdo:

use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine;
let packed = URL_SAFE_NO_PAD.encode(b"\xfb\xef\xbe");
println!("{packed}");                            // ----
let back = URL_SAFE_NO_PAD.decode(packed).unwrap();
println!("{back:02x?}");                         // [fb, ef, be]
// el motor estándar se niega ante la misma entrada,
// porque un guion no está en su alfabeto en absoluto
println!("{:?}", base64::prelude::BASE64_STANDARD.decode("----"));
// Err(Invalid symbol 45, offset 0.)

Ahora la razón por la que la mayoría de los developers se topa con base64url: los JSON Web Tokens. Un JWT son tres partes base64url unidas por puntos, y asomarse dentro de uno es asunto de cinco líneas:

use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine;
let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
let parts: Vec<&str> = jwt.split('.').collect();
let header = String::from_utf8(URL_SAFE_NO_PAD.decode(parts[0]).unwrap()).unwrap();
let payload = String::from_utf8(URL_SAFE_NO_PAD.decode(parts[1]).unwrap()).unwrap();
println!("{header}");
println!("{payload}");
// {"alg":"HS256","typ":"JWT"}
// {"sub":"1234567890","name":"John Doe","iat":1516239022}

Dos honestas salvedades. Decodificar un JWT es asomarse, no confiar: la tercera parte es la firma, y no significa nada hasta que se comprueba contra una clave, y ese es el trabajo de la crate jsonwebtoken (versión 11 en 2026). La versión 11 tiene un filo afilado: necesita exactamente una de las características rust_crypto o aws_lc_rs activada en Cargo.toml, o hace pánico la primera vez que firmas o verificas un token, y su constructor Validation trata la reclamación exp como obligatoria por defecto, así que los tokens acuñados para otras bibliotecas pueden necesitar una validación ajustada. Y al decodificar es donde la elección de alfabeto muerde: si le pasas una cadena URL-safe a BASE64_STANDARD, o al revés, te llevas un rechazo, porque -, _ y el padding que falta son todos inválidos para el otro motor. Ajusta el motor al protocolo, siempre.

Los bytes no son palabras

Cada decodificador de este artículo se detiene en los bytes a propósito, y en Rust eso es más fácil que en la mayoría de los lenguajes, porque no hay un paso oculto de juego de caracteres que pueda salir mal. Base64 es un formato de bytes, sin más. La pregunta "¿qué texto era ese?" es cosa tuya de responder, y la respuesta por defecto para la web moderna es UTF-8, que controlas en una línea de la biblioteca estándar:

use base64::prelude::*;
let packed = "Y2Fmw6k=";   // la palabra cafe con acento, empaquetada
let bytes = BASE64_STANDARD.decode(packed).unwrap();
match std::str::from_utf8(&bytes) {
  Ok(text) => println!("{text}"),
  Err(_) => eprintln!("not utf-8: {bytes:02x?}"),
}

El camino feliz multibyte cubre todo lo que te encontrarás en el cable:

Texto original Base64 Decodifica de vuelta
café Y2Fmw6k=
日本語 5pel5pys6Kqe
naïve résumé bmHDr3ZlIHLDqXN1bcOp
😀 8J+YgA==
π ≈ 3.14159 z4Ag4omIIDMuMTQxNTk=

Y cuando la carga no es texto en absoluto, el mismo código solo cambia de final. Aquí está el número mágico de un archivo PNG, los cuatro bytes 89 50 4E 47 más el par CRLF que sigue:

use base64::prelude::*;
let packed = "iVBORw0KGgo=";
let bytes = BASE64_STANDARD.decode(packed).unwrap();
println!("{bytes:02x?}");                          // [89, 50, 4e, 47, 0d, 0a, 1a, 0a]
assert!(std::str::from_utf8(&bytes).is_err());
std::fs::write("sprite.png", &bytes).unwrap();     // salen bytes, no sale texto

La regla práctica es corta: asume UTF-8, verifica con std::str::from_utf8(), y trata todo lo que falle como una carga de bytes para fs::write, un blob de base de datos o el sumidero de donde saliera. La única vez que recurres a un juego de caracteres de verdad es con datos legados que nunca migraron. La crate encoding_rs (versión 0.8) nombra a los viejos juegos de caracteres y convierte:

use base64::prelude::*;
use encoding_rs::Encoding;
let packed = "Y2Fm6Q==";   // cafe con acento, empaquetado desde bytes Latin-1
let bytes = BASE64_STANDARD.decode(packed).unwrap();
let (text, _, _) = Encoding::for_label(b"windows-1252").unwrap().decode(&bytes);
println!("{text}");   // cafe con acento, como UTF-8

No hay ningún modo "decodificar como Latin-1" que puedas configurar a mal dentro de la crate base64, porque nunca adivina por ti. Esa es la disciplina que mantienes: la crate te da bytes, y tú decides qué significan.

Entrada que sobrevivió al correo

El Base64 que ha sobrevivido a un sistema de correo trae saltos de línea: MIME envuelve a los 76 caracteres por línea (los bloques PEM a los 64), y la especificación MIME dice explícitamente a los decodificadores conformes que ignoren los caracteres fuera del alfabeto, saltos de línea incluidos. Nuestro motor es lo opuesto a un decodificador MIME conforme: rechaza el primer salto de línea que ve, y el lector de streaming reporta el rechazo como un error de E/S que envuelve el mismo DecodeError de precisión:

use std::io::Read;
use base64::prelude::*;
use base64::read::DecoderReader;
let wrapped_in = "SGVs\nbG8s";
let mut reader = DecoderReader::new(wrapped_in.as_bytes(), &BASE64_STANDARD);
let mut out = Vec::new();
println!("{:?}", reader.read_to_end(&mut out).map(|_| out));
// Err(Custom { kind: InvalidData, error: Invalid symbol 10, offset 4. })

Ambas posturas remontan al mismo RFC, que deja la elección a la especificación que rodea, y la crate base64 eligió ser la estricta. No es la primera vez que cambia de opinión: la versión 0.5.0 salió con envoltura MIME de líneas y manejo de espacios en blanco integrados, y la versión 0.10.0 retiró ambos, con el argumento de que el envoltura era demasiado de opinión para una biblioteca general y complicaba la historia de no_std. Así que la receta para la entrada envuelta del mundo real es la misma que sugiere la propia documentación de la crate: quita primero los caracteres no alfabéticos, y decodifica después. Para una cadena en memoria es un solo filtro:

use base64::prelude::*;
fn strip_non_b64(input: &[u8]) -> Vec<u8> {
  input.iter().copied().filter(|b| !b" \n\r\t\x0b\x0c".contains(b)).collect()
}
fn main() {
  let wrapped = "SGVs\nbG8s\r\nIHN0\nYW5kYXJk";
  let clean = strip_non_b64(wrapped.as_bytes());
  let bytes = BASE64_STANDARD.decode(clean).unwrap();
  println!("{}", String::from_utf8_lossy(&bytes));   // Hello, standard
}

Si prefieres un decodificador que mire directamente a través de los saltos, la constante BASE64_MIME_PERMISSIVE de la crate data-encoding es exactamente eso: decodifica "SGVsbG8s\r\nd29ybGQh\r\n" a Hello,world! sin que toques ni una línea. Para streams donde no puedes retener toda la entrada, el FAQ de la crate señala a la crate iter_read para filtrar el stream de bytes, o a escribir un pequeño envoltorio Read que descarta los bytes no deseados a medida que llegan. Una advertencia antes de construir un "decodificador permisivo" a mano: ignorar en silencio los caracteres no alfabéticos es exactamente el comportamiento que la sección 12 del RFC 4648 señala como canal encubierto, así que quita solo los espacios en blanco que esperas, y rechaza todo lo demás.

Cuando tienes el mensaje entero en la mano, un analizador de correo hace el base64 por ti. La crate mail-parser (versión 0.11) decodifica cada parte Content-Transfer-Encoding: base64 mientras analiza, así que los adjuntos vuelven como bytes crudos, ya desenvueltos y decodificados:

use mail_parser::MessageParser;
let email = br#"From: art@vandelay.com
To: jane@example.com
Subject: gift
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="festivus"

--festivus
Content-Type: text/plain; charset="us-ascii"
Content-Transfer-Encoding: base64

SGVsbG8gZnJvbSBlbWFpbA==
--festivus
Content-Type: image/gif
Content-Transfer-Encoding: Base64
Content-Disposition: attachment; filename="tiny.gif"

R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7
--festivus--
"#;
let message = MessageParser::default().parse(email).unwrap();
for part in message.attachments() {
  let name = part
    .headers()
    .iter()
    .find(|h| h.name().eq_ignore_ascii_case("content-disposition"))
    .and_then(|h| h.value().clone().unwrap_content_type()
      .attribute("filename").map(|n| n.to_string()));
  let bytes = part.contents().to_vec();
  println!("{name:?}: {} bytes", bytes.len());
}
// Some("tiny.gif"): 42 bytes

Ese adjunto GIF decodifica a 42 bytes que empiezan con los cuatro bytes 47 49 46 38, las letras ASCII GIF8. Nunca escribiste una línea de código Base64, y ese es justo el punto de usar un analizador: los detalles de la codificación son problema de la biblioteca.

La carga grande

Las cadenas son fáciles; los archivos son donde Base64 demuestra de qué es capaz, y la crate responde con la misma filosofía de streaming que el resto del io de Rust. El read::DecoderReader envuelve cualquier lector y te entrega bytes decodificados transparentemente a medida que lees, de modo que un archivo codificado de varios gigabytes nunca tiene que caber en memoria. Para el ejemplo de abajo, guarda la cadena dGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw== en un archivo de texto plano llamado fox.b64:

use std::io::Read;
use base64::prelude::*;
use base64::read::DecoderReader;
fn main() {
  let packed = std::fs::read("fox.b64").unwrap();
  let mut decoder = DecoderReader::new(&packed[..], &BASE64_STANDARD);
  let mut plain = Vec::new();
  decoder.read_to_end(&mut plain).unwrap();
  println!("{}", String::from_utf8_lossy(&plain));
  // the quick brown fox jumps over the lazy dog
}

La misma idea se encoge a una línea con io::copy: construye un DecoderReader alrededor de un archivo y cópialo a cualquier escritor, y la decodificación sucede por el camino. También hay un truco precioso de la documentación oficial para validar una carga en espacio constante, usando un buffer de tamaño estático y sin ninguna asignación de los datos decodificados:

use std::io::Cursor;
use std::io::Read;
use base64::prelude::*;
use base64::read::DecoderReader;
fn is_valid_base64(input: &str) -> bool {
  let mut cursor = Cursor::new(input.as_bytes());
  let mut decoder = DecoderReader::new(&mut cursor, &BASE64_STANDARD);
  let mut buf = [0u8; 128];
  loop {
    match decoder.read(&mut buf) {
      Ok(0) => return true,   // leído hasta el final sin error
      Ok(_) => continue,
      Err(_) => return false, // algo no era base64
    }
  }
}
fn main() {
  println!("{}", is_valid_base64("SGVsbG8sIHdvcmxkIQ=="));  // true
  println!("{}", is_valid_base64("dt=="));                  // false
}

Para cargas que son grandes pero todavía caben en un buffer que gestionas tú, la API de slices es la opción sin sorpresas: base64::decoded_len_estimate(len) da el tamaño decodificado máximo conservador para len símbolos, y decode_slice() escribe directo en tu buffer preasignado, devolviendo exactamente cuántos bytes escribió:

use base64::prelude::*;
let packed = "SGVsbG8sIHdvcmxkIQ==";
let cap = base64::decoded_len_estimate(packed.len());  // 15, máximo conservador
let mut buf = vec![0u8; cap];
let written = BASE64_STANDARD.decode_slice(packed, &mut buf).unwrap();
buf.truncate(written);
println!("{}", std::str::from_utf8(&buf).unwrap());    // Hello, world!

Si tu buffer es demasiado pequeño te llevas un DecodeSliceError::OutputSliceTooSmall limpio en vez de un pánico, y existe una variante decode_slice_unchecked() que hace pánico por diseño, para los sitios donde "demasiado pequeño" es un error de programador ante el que prefieres estrellarte a pasarlo de mano en mano. Desde la 0.22.0 la comprobación de slice es conservadora a tu favor: solo falla cuando la salida realmente no cabe, así que un buffer del tamaño exacto funciona.

Adónde van los bytes decodificados

Base64 aparece en los proyectos de Rust mucho más a menudo de lo que sugiere "una cadena aleatoria":

  • Respuestas de APIs y webhooks que incrustan binarios o JSON anidado como texto Base64 dentro de sus cargas, el patrón clásico de subida de archivos como JSON.
  • Inspección de JWTs, donde decodificas la cabecera y la carga para asomarte a las reclamaciones y pasas la firma a jsonwebtoken para la parte que de verdad significa algo.
  • Datos con forma de correo: adjuntos MIME y cualquier cosa que haya pasado por un sistema de correo, y por eso existe la sección de espacios en blanco.
  • Bloques PEM en certificados y claves, las secciones -----BEGIN CERTIFICATE----- que mastica cada stack TLS; la crate pem los analiza por ti y, con toda la razón, se construye sobre la crate base64 internamente.
  • Data URIs escondidas dentro del HTML y CSS que estas raspando o renderizando, del tipo data:image/png;base64,....
  • Cabeceras de autenticación básica HTTP, donde Basic TWFuOnBhc3M= no es más que Man:pass con un disfraz.
  • Bases de datos y archivos de configuración, donde alguien quería un binario dentro de una columna de texto o una variable de entorno.
  • Pasos entre lenguajes: un servicio de Python empaqueta un blob, Rust lo abre, y ambos lados hablan el mismo alfabeto por definición.

Para el caso de JSON hay un atajo que merece la pena conocer: la crate serde_with (versión 3) puede anotar un campo de un struct para que serde gestione el Base64 en las dos direcciones, codificando los campos Vec<u8> a texto en la salida y decodificándolos de vuelta en la entrada, con una variante URL-safe a un parámetro de distancia:

use serde::{Deserialize, Serialize};
use serde_with::serde_as;
#[serde_as]
#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct Config {
  #[serde_as(as = "serde_with::base64::Base64")]
  blob: Vec<u8>,
}
let cfg = Config { blob: b"stored in a database".to_vec() };
let json = serde_json::to_string(&cfg).unwrap();
// {"blob":"c3RvcmVkIGluIGEgZGF0YWJhc2U="}
let back: Config = serde_json::from_str(&json).unwrap();
assert_eq!(back, cfg);

Los Data URIs llevan una operación de cadena en dos pasos seguida de una decodificación ordinaria: encuentra la coma, conserva lo que va después, y comprueba que los metadatos terminan en la palabra base64:

use base64::prelude::*;
let uri = "data:image/png;base64,iVBORw0KGgo=";
let comma = uri.find(',').unwrap();
let meta = &uri[..comma];
let payload = &uri[comma + 1..];
let is_b64 = meta.rsplit(';').next().unwrap() == "base64";
let bytes = BASE64_STANDARD.decode(payload).unwrap();
println!("{is_b64}: {} bytes from {meta}", bytes.len());
// true: 8 bytes from data:image/png;base64

Y la única regla que lo gobierna todo, que vale la pena repetir porque todavía coge a la gente en plena faena: Base64 es cinta de empaquetar, no un candado. No es cifrado y no es compresión - es lo contrario de la compresión - y cualquiera con este artículo puede revertir todo lo que hace. Decodifica a gusto, confía con selectividad.

Una década de pasos cuidados

La propia historia de la crate se lee como un apriete lento de tornillos. Apareció por primera vez en crates.io en diciembre de 2015, y la versión 0.5.0 añadió con orgullo el soporte MIME con finales de línea y envoltura configurables. Después la versión 0.10.0 en 2018 retiró el envoltura y el manejo de espacios en blanco, la biblioteca decidió que una crate de propósito general debía decodificar y dejar la poesía a la capa de aplicación; la misma versión añadió el codificador de streaming y la detección de símbolos finales inválidos. La versión 0.20.0 en 2022 introdujo la abstracción de motores e invirtió el defecto de padding para que el motor de fábrica exija padding canónico; la 0.21.0 dejó obsoletas las viejas funciones sueltas como base64::decode() en favor de los métodos de motor, con la nota del compilador "Use Engine::decode" (siguen funcionando, y por eso bastante código legado compila feliz). En 2024, la versión 0.22.0 afinó la semántica de los errores, precisó lo que significa InvalidLength y aceleró la decodificación un 5 a 10 por ciento. Y en julio de 2026, la versión 0.23.0 llegó con los motores SIMD, los símbolos de padding personalizados, el mensaje más claro de InvalidLastSymbol y el aumento del MSRV a 1.71, con el parche 0.23.1 del 4 de agosto arreglando la suite de pruebas para arquitecturas sin SIMD. Una década de pasos pequeños y cuidados, y la crate que empezó como "Es base64. ¿Qué más se puede querer?" ahora trae núcleos vectorizados.

El formato es más viejo que la web, y por eso la estrictividad se siente personal. En 1987, el protocolo Privacy-Enhanced Mail (RFC 989) necesitaba llevar datos binarios por canales de correo de 7 bits, y estandarizó esta codificación con líneas de 64 caracteres; cada bloque -----BEGIN CERTIFICATE----- en el que tu stack TLS se ha fiado nunca es un descendiente directo de esa decisión. En 1996 la especificación MIME (RFC 2045) adoptó el esquema, lo bautizó como "base64" por su alfabeto de 64 caracteres, y fijó la longitud de línea de 76 caracteres que todavía envuelve tus adjuntos de correo hoy. En 2006, el RFC 4648 se convirtió en el estándar que todos citan: las tablas de alfabetos, la variante base64url en la sección 5, y las reglas de estrictividad que esta crate implementa con una fruición tan visible.

Curiosidades

Porque una guía completa debería terminar con una sonrisa:

  • La palabra "base64" codifica a YmFzZTY0. Un formato que se describe a sí mismo es el equivalente técnico de un espejo que habla en Morse.
  • La cadena vacía decodifica a cero bytes, pero "AA==" decodifica a un byte: un NUL. En Base64, "nada" y "un cero" son criaturas distintas.
  • Cada PNG codificado en Base64 que hayas visto jamás empieza por iVBORw0K. Ese es el número mágico del PNG con un disfraz, y es uno de los prefijos más reconocibles de internet.
  • Los IDs de video de YouTube son base64url sin padding: un valor de 8 bytes da doce caracteres Base64, y soltar el padding final deja el familiar ID de once caracteres que puedes pegar en cualquier URL. Uno de los usos más visibles del modo sin padding de todo internet.
  • Bash lleva contando en base 64 desde hace años: el literal aritmético $((64#...)) toma sus dígitos en el orden 0-9, a-z, A-Z, y por fin @ y _ para los valores 62 y 63, así que tu shell arrastra un alfabeto de 64 caracteres a plena vista.
  • Los viejos hashes de contraseñas de crypt(3) usaban una variante de Base64 cuyo alfabeto empieza por ./, y tiene una propiedad preciosa: ordenar las cadenas codificadas da el mismo orden que ordenar los bytes originales. Los archivos de genealogía usaban el mismo alfabeto para multimedia incrustada (GEDCOM 5.5; la revisión 5.5.1 lo retiró), y la crate base64 lo trae como alphabet::CRYPT.
  • La matemática MIME, como la calcula todavía la vieja regla práctica: una carga de correo envuelta cuesta unas 1.37 veces su tamaño original, más por el orden de unos cientos de bytes de cabeceras. La infraestructura de correo de los 90 de verdad cobraba ese peaje en cada adjunto.
  • La crate entera es #![forbid(unsafe_code)], salvo la característica simd-unsafe activada por defecto, de la que sales a propósito en vez de entrar. Una palabra, "unsafe", y es el nombre de un flag de característica.
  • Base64 es maleable de una forma que pone los pelos de punta a los investigadores de seguridad: los mismos bytes pueden escribirse con o sin padding, y con basura en los bits finales, y los decodificadores permisivos no se enteran. Un paper de 2022 demostró consecuencias en el mundo real, y por eso el defecto estricto de esta crate se siente como un guardaespaldas.
  • Base64 no es cifrado. Si lo fuera, no podrías leer la salida de ningún ejemplo de este artículo. Es un asiento junto a la ventanilla, no una bóveda.

Para rematar

Elige tu motor según la compañía que llevas: BASE64_STANDARD para todo lo que produces y controlas, STANDARD_PAD_INDIFFERENT para la frontera de fuentes mezcladas, URL_SAFE_NO_PAD para tokens y URLs, y un GeneralPurpose hecho a mano cuando el protocolo exige sus propias reglas. Deja que las cuatro variantes de DecodeError hagan sus quejas de precisión, controla tus bytes con std::str::from_utf8() antes de llamarlos texto, haz streaming de las cosas grandes con DecoderReader, quita solo los espacios en blanco que esperas, y acude a base64ct cuando el tiempo sea una amenaza. Decodifica todo, confía solo en lo que se verifica. Y si un día necesitas ir en la otra dirección, empaquetando bytes en una cadena para el viaje en vez de abrirla, el artículo hermano cubre la codificación en Rust, desde la matemática de tamaños hasta el remate de streaming.

Última actualización: 2026-09-08

Artículo relacionado: Codificación Base64 en Rust: una guía completa