Você tem que lidar com o formato Base64? Então esse site é perfeito para você! Use o nosso ferramenta online super útil para codificar ou decodificar os seus dados.

Decodificação Base64 em Rust: um guia completo

Uma string pousa no seu programa Rust: letras e dígitos, de vez em quando um sinal de mais ou uma barra, e um = ou dois suspeitos pendurados no final. Essa é Base64, e este guia é sobre recuperar os bytes originais sem surpresas. A página inicial deste site percorre o formato em toda a profundidade, então aqui basta repetir a forma da troca: quatro caracteres do alfabeto valem por três bytes de entrada, e uma cauda de um ou dois caracteres = marca onde os dados de verdade terminaram. Decodificar roda essa troca ao contrário, e tudo o que vem a seguir é sobre fazê-lo de forma deliberada.

O único detalhe que separa o Rust da maioria das linguagens: a biblioteca padrão não tem Base64 nenhum. Não existe base64_decode() escondido em std, e nenhum use std::... que invoque um. O ecossistema se acertou numa única crate chamada simplesmente base64, e ela virou peça fundamental: a versão 0.23.1 saiu em 4 de agosto de 2026, a crate publicou 45 versões desde o primeiro lançamento em dezembro de 2015, e o contador de downloads está perto de 1,5 bilhão. Quase com certeza você já decodifica Base64 através desta crate, diretamente ou puxada por algo como jsonwebtoken, pem ou serde_with, que todos dependem dela.

A única crate e o círculo ao redor

Se o próprio Rust ainda não está na máquina, o seu sistema operacional o fornece: rustc e cargo no Debian e no Ubuntu, um pacote ou instalador no macOS e no Windows, ou o instalador oficial que configura o rustup:

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

Depois a crate, dentro de qualquer projeto cargo. Esta única linha é a instalação inteira, e ela puxa exatamente zero dependências:

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

Três features opcionais moldam a compilação. std vem ligada por padrão e habilita os tipos de streaming do std::io, as implementações padrão de Error e a alocação em heap. alloc fornece as APIs que alocam para builds embutidos no_std sem a biblioteca padrão completa. simd-unsafe vem ligada por padrão e controla os motores vetorizados que você vai conhecer adiante. A versão mínima suportada do Rust é 1.71.0, então qualquer coisa recente roda. Em torno da crate central orbita um pequeno círculo de especialistas, cada um para uma borda que o núcleo deixa deliberadamente para você:

Crate Versão (2026) O que traz Recorra a ela quando
base64ct 1.8 Decodificação em tempo constante do projeto RustCrypto; as APIs de heap ficam atrás de uma feature alloc Os bytes que você decodifica podem vazar informação pelo tempo, como material de chave
data-encoding 2.11 Base64 ao lado de base32, hex e companhia, com variantes MIME permissivas e encode/decode em nível de slice Um componente precisa fazer parse de entrada suja, quebrada em linhas ou de vários protocolos
base64-turbo 0.3 Um codec mais novo que passa de 100 GiB/s no pico, com kernels AVX512, AVX2 e NEON além de um fallback escalar seguro O throughput é o objetivo inteiro e o motor padrão deixa ciclos para trás

Nenhum deles substitui o trabalho diário. Para a vasta maioria dos programas Rust, base64 sozinho é a resposta correta e completa, e o resto deste artigo usa essa única crate para a decodificação em si, recorrendo aos especialistas do círculo apenas quando o trabalho é mais amplo que base64.

Três linhas até os seus bytes

Noventa por cento da vida de decodificação cabe em três linhas. O teste de fumaça canônico usa a famosa string 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"));
}

A saída é Man, e três coisas daquela pequena cerimônia valem a pena guardar. Primeira, decode() sempre te entrega um Vec<u8>, nunca uma string. Isso é um recurso, não um acidente: o Base64 pode carregar uma frase, um JPEG ou um certificado, e nenhum deles deve ser tratado de forma diferente antes de você saber o que tem em mãos. Segunda, o salto dos bytes para o texto é um passo separado e deliberado através do String::from_utf8(), e é nesse passo que mora a decisão do charset. Terceira, o módulo prelude te entrega duas coisas de uma vez sem fazer alarde: o motor BASE64_STANDARD e o trait Engine cujos métodos você está chamando. Se você prefere imports explícitos, use base64::engine::general_purpose::STANDARD; junto com use base64::Engine; é a mesma porta com a placa de nome.

E porque você vai acabar decodificando algo que codificou você mesmo, aqui está a ida e volta que prova que as duas direções concordam. A codificação tem o seu próprio guia completo no site irmão; ela aparece aqui apenas para fabricar dados de teste:

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!
}

Guarde TWFu no bolso de trás como teste de fumaça para qualquer caminho de decodificação que você escrever: se ele transforma TWFu em Man, a máquina é honesta.

Quatro jeitos de dizer não

Esta é a seção que te salva às 2 da manhã, porque quando uma string de produção explode você quer saber exatamente do que a crate está reclamando. A boa notícia: ela reclama alto e com precisão. DecodeError tem exatamente quatro variantes, e aqui está como cada uma soa diante de uma família de infratores típicos, todos alimentados pelo motor padrão estrito:

Entrada O que há de errado Erro exato
"SGVs bG8s" um espaço se esgueirou Invalid symbol 32, offset 4.
"SGVs\nbG8s" uma quebra de linha se esgueirou Invalid symbol 10, offset 4.
"SG=VsbG8="" padding no meio da string Invalid symbol 61, offset 2.
"SGVsbG8sIHdvcmxkIQ==xx" lixo pendurado após o padding Invalid symbol 61, offset 18.
"S" um símbolo não forma um byte Invalid input length: 1
"SGV" três símbolos sem o padding que deveria seguir Invalid padding
"SGVs$bG8="" um $ não está no alfabeto Invalid symbol 36, offset 4.

Repare como a mensagem Invalid symbol te diz tanto o valor do byte culpado quanto o offset dele, para você saltar direto para a cena do crime. A variante InvalidLength é a chata: desde a versão 0.22.0 ela dispara especificamente quando a quantidade de símbolos válidos é impossível, o que quer dizer um comprimento que é um a mais que um múltiplo de quatro, enquanto outros comprimentos ruins aparecem como erros de padding. Aqui está o match completo, para os dias em que você quer tratar cada falha de um jeito diferente:

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"),
  }
}

O rigor deliberado remonta ao próprio padrão. A seção 12 do RFC 4648 avisa que caracteres fora do alfabeto podem ser abusados como um canal dissimulado para contrabandear informação fora da banda, ou para fisgar bugs em parsers descuidados, e recomenda que os decodificadores os rejeitem. A especificação MIME é a exceção famosa, dizendo explicitamente aos decodificadores para ignorar caracteres errantes, e essa é a forma de entrada que a seção de e-mail abaixo mostra como domar.

Três posturas sobre o padding

Toda string Base64 por aí faz uma promessa silenciosa sobre padding, e a versão 0.23 deixa você escolher qual promessa cobrar através do enum DecodePaddingMode. Existem três modos, e a diferença de comportamento vale a pena memorizar. Aqui está o placar para Zm8, que é a palavra fo sem o seu =:

Modo "Zm8", sem padding "Zm8=", com padding Use-o quando
RequireCanonical, o padrão Err(Invalid padding) Ok([102, 111]) Você produz e consome os dados você mesmo
Indifferent Ok([102, 111]) Ok([102, 111]) Recebendo dados de fontes mistas
RequireNone Ok([102, 111]) Err(Invalid padding) Rodando um protocolo sem padding
use base64::engine::general_purpose::{GeneralPurpose, GeneralPurposeConfig, STANDARD_PAD_INDIFFERENT};
use base64::engine::DecodePaddingMode;
use base64::prelude::*;
let strict = BASE64_STANDARD;  // RequireCanonical é o padrão
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)

Os valores padrão seguem o padrão: a seção 3.2 do RFC 4648 diz que as implementações devem incluir os caracteres de padding apropriados no final dos dados codificados, a menos que a especificação ao redor diga o contrário, e é por isso que um motor STANDARD de fábrica os exige. E a escolha importa para a segurança, não só para a pedantia. Aceitar tanto a grafia com padding quanto a sem padding dos mesmos dados torna o Base64 maleável: o mesmo payload lógico pode ser escrito de dois jeitos, e qualquer código que assuma uma grafia canônica única de um valor pode ser surpreendido. O artigo de 2022 "Maleabilidade do Base64 na Prática" (Chatzigiannis e Chalkias, ePrint 2022/361) documenta consequências reais, e a documentação da própria crate faz link para ele. A regra prática: escolha um modo por protocolo, e seja estrito em cada fronteira onde você não controla o produtor.

Os bits escondidos do último símbolo

Aqui está uma corrupção que sobrevive a toda verificação de caracteres. Cada símbolo Base64 carrega 6 bits, e 3 bytes de entrada (24 bits) viram exatamente 4 símbolos. Quando a entrada tem só 1 ou 2 bytes, o símbolo final tem bits sem uso, e o RFC é claro: codificadores conformes devem zerar esses bits sobrando. Um codificador bugado ou malicioso pode, em vez disso, deixar lixo ali, e o resultado ainda passa no teste do alfabeto, no teste do comprimento e no teste do padding, enquanto carrega em silêncio uma cauda corrompida. O motor estrito tem as suas costas, com um erro singularmente detalhado que até te mostra os bits suspeitos:

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

Esse 0b00010011 no erro é o valor decodificado do símbolo culpado incluindo os bits altos ilegais, e a versão 0.23.0 tornou esse detalhe visível justamente porque é tão difícil depurar de outro jeito. Se você sabe que os seus produtores são descuidados, with_decode_allow_trailing_bits(true) engole o lixo em vez de rejeitá-lo. Os navegadores fizeram a aposta oposta: o algoritmo forgiving-base64 do WHATWG, o que fica por trás do atob() do JavaScript, é explicitamente tolerante com bits finais, enquanto o padrão do Rust é o perito forense. Saiba de qual lado da mesa você está sentado.

Base64url: o alfabeto que viaja

O Base64 padrão gasta os seus dois últimos slots do alfabeto no + e na /, exatamente onde as URLs não os querem: numa query string, o mais significa espaço, a barra inicia um novo segmento de caminho, e um = pendurado parece um separador. Então a seção 5 do RFC 4648 define o alfabeto seguro para URL e nome de arquivo, que troca os dois causadores de transtorno por - e _ e, como o comprimento geralmente é recuperável, geralmente dispensa o padding também. O RFC até avisa que essa codificação não deve ser considerada a mesma que o Base64 padrão, e os nomes dos motores concordam:

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]
// o motor padrão recusa a mesma entrada,
// porque um hífen nem está no alfabeto dele
println!("{:?}", base64::prelude::BASE64_STANDARD.decode("----"));
// Err(Invalid symbol 45, offset 0.)

Agora o motivo pelo qual a maioria dos desenvolvedores encontra o base64url de alguma forma: JSON Web Tokens. Um JWT é três partes base64url unidas por pontos, e dar uma espiada por dentro de um é uma questão de cinco linhas:

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}

Dois avisos sinceros. Decodificar um JWT é espiar, não confiar: a terceira parte é a assinatura, e ela não significa nada até ser conferida contra uma chave, o que é trabalho da crate jsonwebtoken (versão 11 em 2026). A versão 11 tem uma aresta afiada: ela precisa de exatamente uma das features rust_crypto ou aws_lc_rs habilitada no Cargo.toml ou dá panic na primeira vez que você assina ou verifica um token, e o builder Validation dela trata a claim exp como obrigatória por padrão, então tokens cunhados para outras bibliotecas podem precisar de uma validação ajustada. E a decodificação é onde a escolha do alfabeto morde: alimente uma string segura para URL no BASE64_STANDARD, ou o contrário, e você recebe uma rejeição, porque -, _ e padding ausente são todos inválidos para o outro motor. Alinhe o motor ao protocolo, sempre.

Bytes não são palavras

Todo decodificador neste artigo para nos bytes de propósito, e no Rust isso é mais fácil do que na maioria das linguagens, porque não existe um passo escondido de charset que possa dar errado. Base64 é um formato de bytes, ponto final. A pergunta "que texto era aquele?" é sua para responder, e a resposta padrão para a web moderna é UTF-8, o que você controla em uma linha da biblioteca padrão:

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

O caminho feliz de multibyte cobre tudo o que você vai encontrar na rede:

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

E quando o payload não é texto nem de perto, o mesmo código só ganha um final diferente. Aqui está o magic number de um arquivo PNG, os quatro bytes 89 50 4E 47 mais o par CRLF que segue:

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();     // bytes para fora, não texto para fora

A regra prática é curta: assuma UTF-8, verifique com std::str::from_utf8() e trate tudo que falhar como um payload de bytes para fs::write, um blob de banco de dados ou qualquer destino de onde ele veio. A única vez que você recorre a um charset de verdade é em dados legados que nunca migraram. A crate encoding_rs (versão 0.8) dá nome às codificações antigas e converte:

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

Não existe um modo "decodificar como Latin-1" para configurar errado dentro da crate base64, porque ela nunca adivinha por você. Essa é a disciplina que você mantém: a crate te entrega bytes, e você decide o que eles significam.

Entrada que sobreviveu ao e-mail

Base64 que sobreviveu a um sistema de e-mail carrega quebras de linha: o MIME quebra a cada 76 caracteres por linha (blocos PEM a cada 64), e a especificação MIME diz explicitamente aos decodificadores conformes para ignorar caracteres fora do alfabeto, quebras de linha incluídas. Nosso motor é o oposto de um decodificador MIME-conformante: ele rejeita a primeira quebra de linha que aparece, e o leitor de streaming reporta a recusa como um erro de E/A envolvendo o mesmo DecodeError preciso:

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. })

As duas posições remontam ao mesmo RFC, que deixa a escolha para a especificação ao redor, e a crate base64 escolheu ser a estrita. Não é a primeira vez que ela mudou de ideia: a versão 0.5.0 saiu com wrapping de linha MIME embutido e tratamento de espaços em branco, e a versão 0.10.0 removeu os dois, sob o argumento de que quebrar linhas era opinativo demais para uma biblioteca geral e complicava a história no_std. Então a receita para entrada quebrada do mundo real é a mesma que a documentação da própria crate sugere: remova primeiro os caracteres fora do alfabeto, depois decodifique. Para uma string em memória é um filtro só:

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
}

Se você prefere um decodificador que olha direto através das quebras, a constante BASE64_MIME_PERMISSIVE da crate data-encoding é exatamente isso: ela decodifica "SGVsbG8s\r\nd29ybGQh\r\n" em Hello,world! sem você tocar em uma linha. Para streams onde você não pode segurar a entrada inteira, o FAQ da crate aponta para a crate iter_read para filtrar o stream de bytes, ou para escrever um wrapper Read minúsculo que joga fora os bytes indesejados à medida que chegam. Um aviso antes de você montar um "decodificador tolerante" à mão: ignorar em silêncio caracteres fora do alfabeto é exatamente o comportamento que a seção 12 do RFC 4648 aponta como canal dissimulado, então remova só o espaço em branco que você espera, e rejeite tudo o mais.

Quando a mensagem inteira está em mãos, um parser de e-mail faz o base64 por você. A crate mail-parser (versão 0.11) decodifica toda parte Content-Transfer-Encoding: base64 enquanto faz o parse, então anexos voltam como bytes crus, já sem quebras e 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

Esse anexo GIF decodifica para 42 bytes começando com os quatro bytes 47 49 46 38, as letras ASCII GIF8. Você nunca escreveu uma linha de código Base64, e esse é o ponto de usar um parser: os detalhes da codificação são problema da biblioteca.

O payload grande

Strings são fáceis; arquivos é onde o Base64 se paga, e a crate responde com a mesma filosofia de streaming do resto do io do Rust. O read::DecoderReader envolve qualquer leitor e entrega bytes decodificados de forma transparente à medida que você lê, então um arquivo codificado de vários gigabytes nunca precisa caber na memória. Para o exemplo abaixo, salve a string dGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw== em um arquivo de texto simples chamado 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
}

A mesma ideia encolhe para uma linha com io::copy: monte um DecoderReader em volta de um arquivo e copie para qualquer writer, e a decodificação acontece no caminho. Existe também um truque gostoso da documentação oficial para validar um payload em espaço constante, usando um buffer de tamanho estático e sem nenhuma alocação dos dados 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,   // leu até o final sem erro
      Ok(_) => continue,
      Err(_) => return false, // algo não era base64
    }
  }
}
fn main() {
  println!("{}", is_valid_base64("SGVsbG8sIHdvcmxkIQ=="));  // true
  println!("{}", is_valid_base64("dt=="));                  // false
}

Para payloads grandes que ainda cabem num buffer que você gerencia, a API de slice é a opção sem surpresas: base64::decoded_len_estimate(len) dá o tamanho máximo decodificado conservador para len símbolos, e decode_slice() escreve direto no seu buffer pré-alocado, retornando exatamente quantos bytes ele escreveu:

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!

Se o seu buffer for pequeno demais, você recebe um DecodeSliceError::OutputSliceTooSmall limpo em vez de um panic, e existe uma variante decode_slice_unchecked() que dá panic por design, para os lugares onde "pequeno demais" é um erro de programador em que você prefere travar a arrastar por aí. Desde a 0.22.0 a verificação de slice é conservadora a seu favor: ela só falha quando a saída realmente não cabe, então um buffer do tamanho exato funciona.

Para onde vão os bytes decodificados

Base64 aparece em projetos Rust com muito mais frequência do que "uma string aleatória" sugere:

  • Respostas de API e webhooks que embutem binário ou JSON aninhado como texto Base64 dentro dos payloads, o padrão clássico de upload de arquivo como JSON.
  • Inspecção de JWT, onde você decodifica o header e o payload para espiar as claims e entrega a assinatura ao jsonwebtoken para a parte que realmente significa algo.
  • Dados com cara de e-mail: anexos MIME e qualquer coisa que tenha passado por um sistema de e-mail, e é por isso que a seção de espaços em branco existe.
  • Blocos PEM em certificados e chaves, as seções -----BEGIN CERTIFICATE----- que toda pilha TLS mastiga; a crate pem faz o parse para você e, cabalmente, é construída sobre a crate base64 internamente.
  • Data URIs escondidos dentro do HTML e do CSS que você está raspando ou renderizando, o tipo data:image/png;base64,....
  • Headers de autenticação básica HTTP, onde Basic TWFuOnBhc3M= é só Man:pass vestindo uma fantasia.
  • Bancos de dados e arquivos de configuração, onde alguém quis binário dentro de uma coluna de texto ou de uma variável de ambiente.
  • Passe de mão entre linguagens: um serviço Python empacota um blob, o Rust desempacota, e os dois lados falam o mesmo alfabeto por definição.

Para o caso de JSON existe um atalho que vale a pena conhecer: a crate serde_with (versão 3) pode anotar um campo de struct para que o serde trate o Base64 nas duas direções, codificando campos Vec<u8> em texto na saída e decodificando de volta na entrada, com uma variante segura para URL a um parâmetro de distância:

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);

Data URIs pedem uma operação de string em duas etapas seguida de uma decodificação comum: encontre a vírgula, guarde o que vem depois, e verifique que os metadados terminam na palavra 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

E a única regra que governa tudo isso, digna de repetição porque ainda pega gente no flagra: Base64 é fita adesiva, não é cadeado. Não é criptografia e não é compressão - é o oposto de compressão - e qualquer pessoa com este artigo consegue reverter tudo o que ele faz. Decodifique à vontade, confie seletivamente.

Uma década de passos cuidadosos

A própria história da crate se lê como um aperto lento de parafusos. Ela apareceu pela primeira vez no crates.io em dezembro de 2015, e a versão 0.5.0 adicionou com orgulho o suporte MIME com finais de linha configuráveis e wrapping. Então a versão 0.10.0, em 2018, removeu o wrapping e o tratamento de espaços em branco, a biblioteca decidindo que uma crate de propósito geral deve decodificar e deixar a poesia para a camada de aplicação; a mesma liberação adicionou o codificador de streaming e a detecção de símbolos finais inválidos. A versão 0.20.0, em 2022, introduziu a abstração de motor e inverteu o padrão de padding para que o motor de fábrica exija padding canônico; a 0.21.0 descontinuou as velhas funções soltas como base64::decode() em favor dos métodos de motor, com a nota do compilador "Use Engine::decode" (elas ainda funcionam, e é por isso que muito código legado compila feliz). Em 2024, a versão 0.22.0 afiou a semântica dos erros, refinou o que InvalidLength significa e acelerou a decodificação em 5 a 10 por cento. E em julho de 2026, a versão 0.23.0 chegou com os motores SIMD, símbolos de padding customizados, a mensagem mais clara de InvalidLastSymbol e o aumento do MSRV para 1.71, com o patch 0.23.1 em 4 de agosto consertando a suíte de testes para arquiteturas sem SIMD. Uma década de passos pequenos e cuidadosos, e a crate que começou como "É base64. O que mais alguém poderia querer?" agora distribui kernels vetorizados.

O formato é mais velho que a web, e é por isso que o rigor parece pessoal. Em 1987, o protocolo Privacy-Enhanced Mail (RFC 989) precisava carregar dados binários por canais de e-mail de 7 bits, e padronizou essa codificação com linhas de 64 caracteres; todo bloco -----BEGIN CERTIFICATE----- que a sua pilha TLS já confiou é um descendente direto daquela decisão. Em 1996 a especificação MIME (RFC 2045) adotou o esquema, batizou de "base64" em homenagem ao alfabeto de 64 caracteres e fixou o comprimento de linha de 76 caracteres que ainda quebra seus anexos de e-mail hoje. Em 2006, o RFC 4648 virou o padrão que todo mundo cita: as tabelas de alfabeto, a variante base64url na seção 5, e as regras de rigor que esta crate implementa com um prazer visível.

Fatos divertidos

Porque um guia completo deve terminar num sorriso:

  • A palavra "base64" codifica para YmFzZTY0. Um formato se descrevendo é o equivalente técnico de um espelho que fala em Morse.
  • A string vazia decodifica para zero bytes, mas "AA==" decodifica para um byte: um NUL. No Base64, "nada" e "um zero" são criaturas diferentes.
  • Todo PNG codificado em Base64 que você já viu começa com iVBORw0K. Esse é o magic number do PNG disfarçado, e é um dos prefixos mais reconhecíveis da internet.
  • Os IDs de vídeo do YouTube são base64url sem padding: um valor de 8 bytes dá doze caracteres Base64, e jogar fora o padding final deixa o conhecido ID de onze caracteres que você pode colar em qualquer lugar de uma URL. Um dos usos mais visíveis do modo sem padding em toda a internet.
  • O Bash conta na base 64 há anos: o literal aritmético $((64#...)) aceita os dígitos na ordem 0-9, a-z, A-Z, e por fim @ e _ para os valores 62 e 63, então o seu shell carrega um alfabeto de 64 caracteres à vista de todos.
  • Os antigos hashes de senha do crypt(3) usavam uma variante de Base64 cujo alfabeto começa com ./, e ela tem uma propriedade gostosa: ordenar as strings codificadas dá a mesma ordem que ordenar os bytes originais. Arquivos de genealogia usavam o mesmo alfabeto para multimídia embutida (GEDCOM 5.5; a revisão 5.5.1 a descartou), e a crate base64 a distribui como alphabet::CRYPT.
  • A matemática MIME, como a velha regra prática ainda calcula: um payload de e-mail quebrado em linhas custa cerca de 1,37 vezes o tamanho original, mais uma ordem de algumas centenas de bytes de headers. A infraestrutura de e-mail dos anos 90 cobrava mesmo aquele pedágio em cada anexo.
  • A crate inteira é #![forbid(unsafe_code)], exceto pela feature simd-unsafe ligada por padrão, da qual você sai em vez de entrar. Uma palavra, "unsafe", e ela é o nome de uma flag de feature.
  • Base64 é maleável de um jeito que assusta pesquisadores de segurança: os mesmos bytes podem ser escritos com ou sem padding, e com lixo nos bits finais, e decodificadores tolerantes não vão perceber. Um artigo de 2022 demonstrou consequências no mundo real, e é por isso que o padrão estrito desta crate parece um corpo de guarda.
  • Base64 não é criptografia. Se fosse, você não conseguiria ler a saída de nenhum exemplo deste artigo. É um assento junto à janela, não um cofre.

Para fechar

Escolha o seu motor pela companhia que você mantém: BASE64_STANDARD para tudo o que você produz e controla, STANDARD_PAD_INDIFFERENT para a fronteira de fontes mistas, URL_SAFE_NO_PAD para tokens e URLs, e um GeneralPurpose feito à mão quando o protocolo impõe as próprias regras. Deixe as quatro variantes de DecodeError fazerem o seu reclamo preciso, filtre os seus bytes pelo std::str::from_utf8() antes de chamá-los de texto, faça streaming das coisas grandes com DecoderReader, remova só o espaço em branco que você espera, e recorra ao base64ct quando o tempo for a ameaça. Decodifique tudo, confie só no que se verifica. E se um dia você precisar ir na direção oposta, empacotando bytes numa string para a estrada em vez de desempacotá-la, o artigo irmão cobre a codificação em Rust, das contas de tamanho ao final em streaming.

Última atualização: 2026-09-08

Artigo relacionado: Codificação Base64 em Rust: um guia completo