Codificação Base64 em Rust: um guia completo
Você está prestes a enviar alguns bytes por uma porta que só aceita texto, e o preço de entrada é uma string de letras, dígitos, sinais de mais e barras que fica mais ou menos um terço maior do que o que você começou. Bem-vindo ao Base64, o pedágio da internet. A página inicial deste site explica o formato em toda a profundidade, então aqui basta repetir a forma: o Base64 escreve três bytes de entrada como quatro caracteres tirados de um alfabeto de 64 símbolos, e uma cauda de um ou dois caracteres = diz ao leitor onde os dados de verdade terminaram. Essa troca de quatro por três é a economia inteira do formato, e este guia é sobre fazê-lo bem no Rust.
A primeira coisa a saber é que a biblioteca padrão do Rust não faz isso por você. Não existe base64_encode() escondido em std, e nenhum use std::... que mude sua ideia. 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 dezembro de 2015, e o contador de downloads está perto de 1,5 bilhão. Todo exemplo de codificação abaixo usa essa única crate, mais duas companhias pequenas para quebra de linha e armadura PEM.
A toolchain e a crate
Primeiro a toolchain, um comando por mundo:
# 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:
cargo new my-app
cd my-app
cargo add base64
Esta única linha é a instalação inteira, e ela puxa exatamente zero dependências. A crate vem com três features opcionais que você deveria conhecer: std (ligada por padrão; te dá streaming do std::io, as impls padrão de Error e alocação em heap), alloc (as APIs que alocam para builds embutidos no_std) e simd-unsafe (ligada por padrão; os motores SIMD, que aparecem algumas seções adiante). A versão mínima suportada do Rust é 1.71.0, então qualquer coisa recente roda. Em volta dela ficam as companhias para os trabalhos que o núcleo deliberadamente não faz:
- line-wrap (versão 0.2) insere as quebras de linha de 76 ou 64 caracteres que o MIME e o PEM exigem; a crate
base64em si recusa quebrar linhas, de propósito, como você vai ver. - pem (versão 4) monta e faz o parse de blocos
-----BEGIN ...-----para certificados e chaves; ela depende dobase64internamente e adiciona a armadura e o wrapping. - base64ct (versão 1.8) é o decodificador em tempo constante do projeto RustCrypto, para quando o lado da leitura de uma ida e volta é a metade sensível.
- base64-turbo (versão 0.3) é um codec mais novo de alto throughput que passa de 100 GiB/s no pico em hardware moderno.
Uma codificação, quatro caracteres
A cerimônia menor possível tem este ar, e ela já prova a ida e volta inteira:
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!
}
Duas coisas valem a pena notar. 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, e é por isso que um simples use base64::prelude::*; é tudo o que este exemplo precisa. E encode() aceita qualquer coisa que possa ser lida como bytes, graças à bound AsRef<[u8]>: um &str, um literal &[u8], um Vec<u8>, você dá o nome. A metade de decodificação do exemplo está ali só para manter um olho no codificador, porque a direção oposta tem o seu próprio guia completo no site irmão. Se você quer o próprio teste de fumaça da crate, a documentação dela codifica asdf e recebe YXNkZg== de volta; mesmo alfabeto, mesmas contas.
O preço exato de cada byte
Cada codificador Base64 existente cobra o mesmo imposto, e uma vez que você enxerga a matemática, dá para orçá-lo. Cada caractere de saída carrega 6 bits, cada byte de entrada carrega 8, e a menor pilha que é as duas coisas tem 24 bits: exatamente 3 bytes entrando, exatamente 4 caracteres saindo. Essa proporção é o espetáculo inteiro, então um arquivo de 3 kilobytes vira 4 kilobytes e um upload de 10 megabytes vira 13,3. O padding é o erro de arredondamento tornado visível: quando a entrada não é um múltiplo de 3 bytes, o grupo final tem capacidade sobrando, e o codificador a enche com = para o comprimento da saída continuar um múltiplo de 4. Aqui está a tabela da verdade do RFC 4648, que o motor padrão reproduz exatamente:
| Entrada | Comprimento mod 3 | Codificado | Comprimento da saída |
|---|---|---|---|
"" (vazio) |
0 | "" (vazio) |
0 |
f |
1 | Zg== |
4 |
fo |
2 | Zm8= |
4 |
foo |
0 | Zm9v |
4 |
foobar |
0 | Zm9vYmFy |
8 |
use base64::prelude::*;
let words: [&[u8]; 4] = [b"", b"f", b"fo", b"foo"];
for input in words {
println!("{:?} -> {:?}", String::from_utf8_lossy(input), BASE64_STANDARD.encode(input));
}
// "" -> ""
// "f" -> "Zg=="
// "fo" -> "Zm8="
// "foo" -> "Zm9v"
Leia aquela primeira linha duas vezes, porque é a que todo mundo erra na cabeça: a entrada vazia codifica para a string vazia, não para AA==. A string AA== é a codificação de exatamente um byte, um NUL, o que é um payload genuinamente diferente. E quando você precisa dimensionar um buffer antes de codificar, a crate te entrega a matemática como const fn, então você até dimensiona arrays em tempo de compilação:
let padded = base64::encoded_len(15, true).unwrap();
let slim = base64::encoded_len(15, false).unwrap();
println!("{padded} / {slim}"); // 20 / 20
println!("{:?}", base64::encoded_len(13, true)); // Some(20)
println!("{:?}", base64::encoded_len(13, false)); // Some(18)
println!("{:?}", base64::encoded_len(14, false)); // Some(19)
println!("{:?}", base64::encoded_len(100, false)); // Some(134)
Observe as linhas de 13 e 14 bytes, porque são as que derrubam a conta de lenço de bolso: 13 bytes precisam de 18 caracteres sem padding mas 20 com, enquanto 14 bytes precisam de 19 e 20. A função retorna um Option, que é None só quando a conta do comprimento transbordaria, então um unwrap() é seguro para qualquer entrada que possa realmente existir em memória. Para o mundo com cara de e-mail o imposto tem um adicional: o MIME quebra linhas a cada 76 caracteres, e a velha regra prática é que o Base64 quebrado em linhas custa cerca de 1,37 vezes o tamanho original, mais o overhead de headers. O próprio FAQ da crate tem uma opinião menos educada sobre o padding em si: os bytes = "não afetam a decodificação além de dar a oportunidade de dizer 'aquele padding está incorreto'", e "exabytes de armazenamento e transferência com certeza foram desperdiçados em bytes = sem sentido".
Padding: uma decisão sobre o leitor
No base64 0.23 as funções soltas de encode foram descontinuadas - o jeito atual é chamar um método de um Engine, e um motor é uma política: qual alfabeto escrever e qual padding adicionar. Os presets moram em base64::engine::general_purpose, com os quatro populares reexportados no prelude:
| Motor | Alfabeto | Adiciona padding | Melhor para |
|---|---|---|---|
STANDARD / BASE64_STANDARD |
+ / |
sim | tudo, o padrão |
STANDARD_NO_PAD / BASE64_STANDARD_NO_PAD |
+ / |
não | payloads enxutos que você também consome |
URL_SAFE / BASE64_URL_SAFE |
- _ |
sim | conteúdo de URL que ainda quer padding |
URL_SAFE_NO_PAD / BASE64_URL_SAFE_NO_PAD |
- _ |
não | JWTs, URLs, IDs de objeto |
Codificar sem padding não é um hack que a crate tolera; é uma posição de primeira classe, com as constantes de configuração NO_PAD e PAD pré-configuradas ao lado dos irmãos *_INDIFFERENT adicionados na 0.23.0. Se os presets não servirem, você monta o seu próprio motor a partir de um Alphabet e de um GeneralPurposeConfig com um único controle, e como motores são baratos de construir, você guarda o resultado num const em vez de reconstruí-lo a cada requisição:
use base64::engine::general_purpose::{GeneralPurpose, GeneralPurposeConfig};
use base64::prelude::*;
const SLIM: GeneralPurpose = GeneralPurpose::new(
&base64::alphabet::STANDARD,
GeneralPurposeConfig::new().with_encode_padding(false),
);
fn main() {
println!("{}", SLIM.encode("fo")); // Zm8
println!("{}", BASE64_STANDARD.encode("fo")); // Zm8=
}
Agora a decisão vira uma decisão sobre os decodificadores dos outros, o que nunca é puramente estético. As regras de rigor no lado da decodificação vêm do DecodePaddingMode, e a tabela abaixo responde "o outro lado consegue ler o que eu escrevi?":
| Você codifica com | Um decodificador STANDARD estrito |
Um decodificador STANDARD_NO_PAD |
Um decodificador INDIFFERENT |
|---|---|---|---|
STANDARD (com padding) |
lê | recusa o = |
lê |
STANDARD_NO_PAD |
recusa: padding ausente | lê | lê |
URL_SAFE_NO_PAD |
recusa: alfabeto errado | recusa: alfabeto errado | lê só com o alfabeto de URL |
As regras práticas caem prontas daquela tabela. Se você controla as duas pontas, escolha um motor e use em todos os lugares, e prefira sem padding para economizar bytes. Se você consome dados do mundo exterior, o seu decodificador tem voto sobre qual motor você deveria emitir: um decodificador STANDARD de fábrica precisa do seu padding, enquanto um decodificador STANDARD_PAD_INDIFFERENT aceita os dois. E a escolha tem também um sabor de segurança. Permitir tanto a grafia com padding quanto a sem padding do mesmo payload torna o Base64 maleável; o artigo de 2022 "Maleabilidade do Base64 na Prática" (Chatzigiannis e Chalkias, ePrint 2022/361), para o qual a documentação da própria crate faz link, mostra por quê. Um protocolo em que os mesmos dados podem ser escritos de dois jeitos diferentes tem o hábito de surpreender o código que trata a string codificada como uma identidade, então quando o seu formato define uma grafia canônica única, exija-a na fronteira.
Base64url para tokens e links
As duas últimas letras do alfabeto do Base64 padrão são + e /, e numa URL esses são dois dos caracteres mais caros da linguagem: o mais vira %2B, a barra vira %2F, e o padding vira %3D. A seção 5 do RFC 4648 resolve isso com o alfabeto seguro para URL e nome de arquivo, que troca os dois causadores de transtorno por - e _ e geralmente pula o padding também. Os motores tornam a distinção impossível de perder:
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]
Três bytes da entrada mais chata possível viram uma string de quatro caracteres que você pode colar numa URL, num nome de arquivo, num cookie ou numa chave de banco de dados sem um único escape percentual. Esse é o alfabeto em que os JSON Web Tokens vivem: um JWT é três partes base64url unidas por pontos, e cunhar um com a crate jsonwebtoken (versão 11 em 2026) tem este ar:
use serde::Serialize;
use jsonwebtoken::{EncodingKey, Header, encode};
#[derive(Debug, Serialize)]
struct Claims {
sub: String,
company: String,
exp: u64,
}
let key = b"secret";
let my_claims = Claims {
sub: "b@b.com".to_owned(),
company: "ACME".to_owned(),
exp: 19_000_000_000, // bem no futuro
};
let token = encode(&Header::default(), &my_claims, &EncodingKey::from_secret(key)).unwrap();
println!("{token}");
// eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJiQGIuY29t...
A versão 11 tem um requisito de configuração que morde iniciantes: a crate precisa de exatamente uma das features rust_crypto ou aws_lc_rs habilitada no Cargo.toml, e se nenhuma estiver ligada, dá panic na primeira vez que você assina ou verifica um token. Note a claim exp na struct: a validação da crate a trata como obrigatória por padrão, então tokens de verdade carregam uma de qualquer jeito, e o alfabeto base64url no token é inteiramente assunto da biblioteca. Se você só está inspecionando tokens em vez de cunhá-los, o artigo irmão mostra a espiada de cinco linhas. E um lembrete da regra de ouro, que se aplica com força especial a tokens: as três partes de um JWT são todas legíveis sem chave. Base64 é um assento junto à janela, não um cadeado.
Texto entrando, bytes saindo
Codificadores não leem mentes, então "codificar esta string" sempre significa "codificar os bytes UTF-8 desta string" no Rust, porque é isso que o str::as_bytes() entrega. A boa notícia é que a web moderna é quase inteiramente UTF-8, então o caminho honesto é curto e feliz:
use base64::prelude::*;
let text = "café";
let packed = BASE64_STANDARD.encode(text.as_bytes());
println!("{packed}"); // Y2Fmw6k=
Os casos de multibyte se comportam todos:
| Texto original | Base64 | Ida e volta |
|---|---|---|
café |
Y2Fmw6k= |
limpo |
日本語 |
5pel5pys6Kqe |
limpo |
😀 |
8J+YgA== |
limpo |
π ≈ 3.14159 |
z4Ag4omIIDMuMTQxNTk= |
limpo |
A única decisão de verdade é em quais bytes você parte. Se os dados chegam como bytes e não como texto, um arquivo lido do disco ou um buffer de uma chamada de rede, pule a string por completo e codifique o Vec<u8> diretamente; essa também é a única resposta correta para payloads não-UTF-8 como um PNG ou um protobuf. Solte qualquer imagem ao lado do seu código e aponte a leitura para ela:
use base64::prelude::*;
let file_bytes = std::fs::read("sprite.png").unwrap();
let size = file_bytes.len();
let packed = BASE64_STANDARD.encode(file_bytes);
println!("{size} bytes -> {} base64 chars", packed.len());
// todo PNG codificado começa com iVBORw0K
assert!(packed.starts_with("iVBORw0K"));
Essa última asserção é um teste de sanidade grátis e um dos prefixos mais reconhecíveis da internet. E se um dia você codificar o mesmo texto lógico por dois charsets diferentes, ou codificar bytes que leu como um charset diferente, a ida e volta vai voltar como mojibake com cara de paz. O codificador nunca mente; ele só codifica os bytes que você der, o que é ao mesmo tempo a sua maior força e a sua única armadilha.
Quando um formato quer linhas
A crate base64 deliberadamente não insere quebras de linha, e não é a primeira vez que ela toma essa decisão. A versão 0.5.0 saiu com wrapping de linha MIME embutido com finais de linha configuráveis, e a versão 0.10.0 o removeu, a biblioteca decidindo que quebrar linhas era opinativo demais para uma crate geral e complicava a história no_std. Se um formato exige linhas, a crate line-wrap existe exatamente para isso. A sua função única, line_wrap(), recebe o seu buffer pré-alocado, o comprimento da entrada, o limite de coluna e o final de linha, e retorna a quantidade de bytes de final de linha que ela inseriu:
use base64::prelude::*;
let data = BASE64_STANDARD.encode(vec![b'a'; 300]); // 400 caracteres
let mut buf = vec![0u8; data.len() + 16];
buf[..data.len()].copy_from_slice(data.as_bytes());
let endings = line_wrap::line_wrap(&mut buf, data.len(), 76, &line_wrap::crlf());
buf.truncate(data.len() + endings);
let wrapped = String::from_utf8(buf).unwrap();
println!("{} chars in, {} bytes out, {} line endings", data.len(), wrapped.len(), endings);
// 400 chars in, 410 bytes out, 10 line endings (cinco pares CRLF)
Pré-dimensione o buffer com espaço para os finais, chame a função e trunque para o total reportado; os cinco pares CRLF são o preço da regra de 76 colunas do MIME. Para PEM, troque o limite e o final, 64 colunas e line_wrap::lf(), e você tem o texto do corpo da armadura. Então a crate pem adiciona os banners em uma chamada:
let pem_block = pem::encode(&pem::Pem::new("CERTIFICATE", b"0123456789abcdef"));
println!("{pem_block}");
// -----BEGIN CERTIFICATE-----
// MDEyMzQ1Njc4OWFiY2RlZg==
// -----END CERTIFICATE-----
let back = pem::parse(pem_block).unwrap();
println!("{}: {} bytes", back.tag(), back.contents().len());
// CERTIFICATE: 16 bytes
// e finais de linha estilo Unix, se o consumidor for chato
let lf_block = pem::encode_config(
&pem::Pem::new("KEY", b"0123456789abcdef"),
pem::EncodeConfig::new().set_line_ending(pem::LineEnding::LF),
);
Por padrão, o pem::encode usa CRLF, a convenção histórica do PEM; o builder set_line_ending muda para LF para as ferramentas que esperam isso. Repare no que a crate pem não faz: ela nunca chama uma função de base64 que você consiga ver, porque a codificação é assunto interno dela. Quando um formato quer linhas, a arquitetura é uma crate por trabalho.
Streaming em espaço constante
Para dados grandes demais para caber numa variável só, a crate responde com a mesma filosofia de streaming do resto do io do Rust: o write::EncoderWriter envolve qualquer writer e codifica em base64 tudo o que você escreve nele, em espaço constante. O rito completo para um buffer tem este ar, e a estrela da seção é a chamada finish():
use std::io::Write;
use base64::prelude::*;
use base64::write::EncoderWriter;
fn main() {
let mut encoder = EncoderWriter::new(Vec::new(), &BASE64_STANDARD);
encoder.write_all(b"the quick brown fox jumps over the lazy dog").unwrap();
let packed = encoder.finish().unwrap();
println!("{}", String::from_utf8(packed).unwrap());
// dGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw==
}
Por que o finish() é a estrela? Porque é a única chamada que esvazia o grupo parcial final e adiciona o padding, e o codificador tem um método irmão que não faz isso. A própria documentação da crate diz em claras letras: finish() "codifica os bytes de entrada restantes e adiciona padding se for apropriado. É chamado automaticamente quando desalocado (veja a implementação de Drop), mas qualquer erro que ocorra ao invocar o writer subjacente será suprimido. Se você quer tratar esse tipo de erro, chame o finish() você mesmo." A implementação de Drop se comporta como o BufWriter: esvazia, mas ignora erros durante o drop. Então o último grupo parcial não se perde, mas "provavelmente funcionou" não é uma estratégia de lançamento, porque o erro de escrita que ele teria te contado sumiu.
O mesmo stream funciona um nível abaixo através do io::copy quando você quer o pipeline inteiro em uma chamada, e existe um wrapper de bônus para os momentos de "só preciso disso dentro de uma string de formatação":
use std::io;
use base64::prelude::*;
use base64::write::EncoderWriter;
let file = b"the quick brown fox jumps over the lazy dog".to_vec();
let mut cursor = io::Cursor::new(file);
let mut encoder = EncoderWriter::new(Vec::new(), &BASE64_STANDARD);
io::copy(&mut cursor, &mut encoder).unwrap();
let packed = encoder.finish().unwrap();
println!("{}", String::from_utf8(packed).unwrap());
use base64::display::Base64Display;
use base64::prelude::*;
let value = Base64Display::new(b"\0\x01\x02\x03", &BASE64_STANDARD);
println!("base64: {value}"); // base64: AAECAw==
Esse wrapper Base64Display é uma joia: ele formata bytes como Base64 dentro de qualquer string de formatação sem uma única alocação em heap, o que torna linhas de log e saída de depuração de repente agradáveis.
Alocação, e a ausência dela
O método conveniente aloca, e para a maior parte da sua vida esse é o trade certo. Mas o trait Engine expõe três sabores de encode, e a tabela abaixo é a matriz de decisão inteira:
| Método | Saída | Aloca |
|---|---|---|
encode() |
um String novo |
sempre |
encode_string() |
adiciona ao seu String |
só se precisar crescer |
encode_slice() |
escreve no seu &[u8] |
nunca |
use base64::prelude::*;
let input = b"Hello, world!";
let mut buf = vec![0u8; base64::encoded_len(input.len(), true).unwrap()];
let written = BASE64_STANDARD.encode_slice(input, &mut buf).unwrap();
buf.truncate(written);
println!("{}", std::str::from_utf8(&buf).unwrap()); // SGVsbG8sIHdvcmxkIQ==
// ou mantenha o buffer inteiro na pilha
let mut stack = [0u8; 24];
let n = BASE64_STANDARD.encode_slice(b"abc 123", &mut stack).unwrap();
println!("{}", String::from_utf8(stack[..n].to_vec()).unwrap()); // YWJjIDEyMw==
// e se você dimensionou errado, recebe um erro, não um estouro de buffer
let mut tiny = [0u8; 5];
println!("{:?}", BASE64_STANDARD.encode_slice(input, &mut tiny));
// Err(OutputSliceTooSmall)
Dimensione o buffer com encoded_len(), escreva com encode_slice(), e se você errar o tamanho recebe um EncodeSliceError::OutputSliceTooSmall limpo em vez de comportamento indefinido, o que numa linguagem de sistemas é a diferença entre uma tarde chata e uma longa. Para trabalho embutido as mesmas funções existem atrás da feature alloc, então você mantém a API e joga fora o heap.
Velocidade: os motores SIMD
A versão 0.23.0, a que saiu em julho de 2026, trouxe a feature de manchete: motores acelerados por SIMD para os alfabetos padrão e seguro para URL. São três, e eles se dividem por quanto confiam no seu hardware:
| Motor | Detecta em runtime | Funciona em no_std |
|---|---|---|
Simd |
sim, escolhe AVX2 ou NEON, cai para o motor escalar | não, precisa do std para detectar |
Avx2 |
não, assume que a CPU tem AVX2 | sim, em alvos x86_64 |
Neon |
não, assume que a CPU tem NEON | sim, em alvos aarch64 |
use base64::engine::general_purpose::GeneralPurposeConfig;
use base64::engine::{Avx2, Simd};
use base64::Engine;
let turbo = Simd::standard(GeneralPurposeConfig::new());
println!("{}", turbo.encode("simd works!"));
// c2ltZCB3b3JrcyE=
if let Some(fixed) = Avx2::standard(GeneralPurposeConfig::new()) {
println!("{}", fixed.encode("hello avx2")); // aGVsbG8gYXZ4Mg==
}
O construtor Simd faz a detecção da CPU uma vez e retorna o melhor kernel que encontra, ou o motor escalar se nenhum se aplicar, então construa uma vez num const ou na inicialização e reutilize; em hardware capaz ele é várias vezes mais rápido que o caminho escalar tanto na codificação quanto na decodificação. Uma nota de rodapé honesta: o caminho SIMD é o único lugar da crate que toca em unsafe, e é por isso que a feature se chama simd-unsafe. Desligue a feature e a crate inteira volta a ser #![forbid(unsafe_code)], com o motor escalar ainda fazendo trabalho honesto. Se o throughput bruto é o objetivo inteiro, a crate base64-turbo empurra o envelope ainda mais, passando de 100 GiB/s no pico com kernels AVX512, AVX2 e NEON atrás de detecção em runtime, e um fallback escalar 100% seguro em tudo o mais. A crate base64 tem licença dupla MIT/Apache-2.0, então tudo isso é grátis, inclusive a velocidade.
Quatro alfabetos a mais
O alfabeto do RFC é o padrão, mas a crate base64 traz quatro a mais, cada um um pequeno monumento a algum protocolo real que precisou do seu próprio giro:
| Alfabeto | O giro | Quem usa | abc 123 codifica para |
|---|---|---|---|
alphabet::CRYPT |
./ vêm primeiro, depois dígitos e letras, sem padding |
hashes de senha clássicos do Unix crypt(3) | MK7X612mAk |
alphabet::BCRYPT |
./ primeiro, depois letras, depois dígitos |
hashes de senha bcrypt | WUHhGBCwKu |
alphabet::IMAP_MUTF7 |
uma vírgula faz as vezes da barra, sem padding | nomes de caixa postal em UTF-7 modificado do IMAP | YWJjIDEyMw |
alphabet::BIN_HEX |
um alfabeto pesado em pontuação que pula letras confusáveis | BinHex 4, o antigo embrulho de arquivo do Macintosh | B@*M)$%b-` |
use base64::engine::general_purpose::{GeneralPurpose, NO_PAD};
use base64::Engine;
let crypt = GeneralPurpose::new(&base64::alphabet::CRYPT, NO_PAD);
println!("{}", crypt.encode(b"abc 123")); // MK7X612mAk
let bcrypt = GeneralPurpose::new(&base64::alphabet::BCRYPT, NO_PAD);
println!("{}", bcrypt.encode(b"abc 123")); // WUHhGBCwKu
let imap = GeneralPurpose::new(&base64::alphabet::IMAP_MUTF7, NO_PAD);
println!("{}", imap.encode(b"abc 123")); // YWJjIDEyMw
Entrada a mesma, três saídas diferentes, todas Base64 válido no próprio dialeto. O alfabeto crypt é o que tem um superpoder de verdade: como os símbolos dele são ordenados para casar com os padrões de bits, ordenar as strings codificadas dá a mesma ordem que ordenar os bytes originais, e é por isso que o GEDCOM 5.5 (1996) o usou para campos de multimídia - a revisão 5.5.1 descartou a feature - e a crate ainda distribui o alfabeto para você. E se o dialeto que você precisa não está na crate, dá para defini-lo com uma string de 64 caracteres, porque o Alphabet::new() monta as tabelas de encode e decode por você:
use base64::alphabet::Alphabet;
use base64::engine::general_purpose::{GeneralPurpose, PAD};
use base64::Engine;
// um base64 de mundo bizarro: +/ na frente em vez do final
let alphabet = Alphabet::new(
"+/ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
).expect("a valid 64 char alphabet");
let bizarro = GeneralPurpose::new(&alphabet, PAD);
println!("{}", bizarro.encode(b"hello 99")); // YETqZE6eMRi=
// enquanto o motor padrão diz:
println!("{}", base64::prelude::BASE64_STANDARD.encode(b"hello 99"));
// aGVsbG8gOTk=
Um aviso sobre o caminho do alfabeto customizado: no momento em que você inventa um dialeto, vira a única pessoa no planeta que consegue ler os seus dados, então faça só quando um protocolo exigir, e escreva um comentário dizendo qual.
Onde os codificadores trabalham
A codificação Base64 aparece em projetos Rust num elenco previsível de situações:
- Uploads de arquivo em APIs JSON, onde o arquivo é um campo de bytes vestindo fantasia de texto, o uso mais comum por uma margem ampla.
- Data URIs em HTML e CSS, o tipo
data:image/png;base64,..., maravilhosos para ícones minúsculos, questionáveis para imagens hero. - JWTs e OAuth, onde o base64url é o dialeto e a crate
jsonwebtokené a ferramenta. - Blocos PEM para certificados e chaves, as seções
-----BEGIN CERTIFICATE-----que quebram o Base64 a cada 64 caracteres por linha. - Binário em XML e arquivos de configuração, o padrão
<data encoding="base64">que você ainda encontra em bookmarks exportados e dumps de configurações. - Arquivos LDAP e LDIF, que usam Base64 para manter valores binários de atributos em uma linha só.
- Payloads de QR code e passes de área de transferência, onde o texto sobrevive à viagem e o binário não.
- Headers de autenticação básica HTTP, onde
Basic TWFuOnBhc3M=é um par de credenciais, e um lembrete de que isso é um problema de embalagem, não de esconderijo.
E a regra de ouro que governa tudo isso: 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 em uma linha. Codifique à vontade, mas nunca codifique uma senha, uma chave de API ou um segredo e chame de protegido. Se precisa ser escondido, use criptografia de verdade, e se for grande, considere se um upload multipart simplesmente teria custado menos que o imposto.
Uma década de passos pequenos
O formato é mais velho que a web. 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 exatamente 64 caracteres. Todo bloco -----BEGIN CERTIFICATE----- da internet é descendente daquela decisão, e é por isso que arquivos PEM ainda quebram a cada 64 hoje. Em 1996 a especificação MIME (RFC 2045) adotou o esquema, batizou de "base64" em homenagem ao alfabeto de 64 caracteres e moveu a quebra para 76 caracteres. Antes de tudo isso, as caixas Unix saíam de fábrica com uuencode e os Macs com BinHex, cada um com o seu alfabeto, e ambos ainda aparecem em sistemas antigos como fósseis com cabeçalhos de arquivo. Em 2006, o RFC 4648 virou o padrão que todo mundo cita, com as tabelas de alfabeto, a variante base64url e as regras canônicas de codificação que todo motor deste artigo implementa. A seção 3.5 dele exige que os codificadores zerem os bits finais sem uso, e a crate faz isso; se o seu payload um dia disparar a verificação InvalidLastSymbol de um decodificador estrito, a corrupção aconteceu a montante.
A própria história da crate rima. Ela apareceu no crates.io em dezembro de 2015, e a versão 0.5.0 adicionou com orgulho o wrapping de linha MIME com finais de linha configuráveis. 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 codificar e deixar a poesia para a camada de aplicação; a mesma liberação adicionou o EncoderWriter de streaming. A versão 0.20.0, em 2022, introduziu a abstração de motor e tornou o padding canônico o padrão, e a 0.21.0 descontinuou as velhas funções soltas em favor dos métodos de motor, com a nota do compilador "Use Engine::encode" (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 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, uma mensagem de erro mais clara 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.
Coisas que valem um sorriso
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 codifica para a string vazia. Nada é a única entrada que não custa nada, o que é uma espécie de isenção fiscal.
AA==não é a codificação de nada; é a codificação de um byte NUL. No Base64, "nada" e "um zero" são criaturas diferentes, e os decodificadores as distinguem.- Todo PNG codificado em Base64 começa com
iVBORw0K. Esse é o magic number do PNG com a fita adesiva por cima, um dos prefixos mais reconhecíveis da internet. - Numa URL, os caracteres do Base64 padrão precisam de fantasias de escape: o mais vira
%2B, a barra vira%2F, e o padding vira%3D. O Base64url existe para que os caracteres possam usar as próprias caras. - Os IDs de vídeo do YouTube são base64url sem padding: oito bytes de ID viram a string de onze caracteres que você pode colar em qualquer lugar. Um dos usos mais visíveis do modo sem padding em toda a internet.
- O alfabeto antigo de senha do crypt(3) ordena corretamente: strings codificadas ordenadas ficam na mesma ordem que o texto puro ordenado. O GEDCOM 5.5 (1996) usou aquele alfabeto para os campos de multimídia, a revisão 5.5.1 descartou a feature, e a crate ainda o distribui para você.
- O BinHex, o antigo embrulho do Macintosh, montou o alfabeto para excluir caracteres visualmente confusáveis como
7,O,geo. Um codificador desenhado para olhos humanos, num mundo antes do corretor ortográfico. - O próprio FAQ da crate é direto sobre o padding: exabytes de armazenamento e transferência com certeza foram desperdiçados em bytes
=sem sentido. O pedágio cobra desde 1987. - 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.
A versão curta
Escolha o seu motor pelo caminho que os dados vão percorrer: BASE64_STANDARD para tudo o que você também decodifica você mesmo, os motores _NO_PAD quando você controla as duas pontas e quer os bytes de volta, URL_SAFE_NO_PAD para tokens e URLs, e um Alphabet customizado só quando um protocolo insiste. Dimensione os buffers com encoded_len(), faça streaming das coisas grandes através do EncoderWriter e feche sempre com finish(), quebre linhas com line-wrap e pem só quando um formato exigir, deixe os motores SIMD fazerem o trabalho pesado quando puder, e lembre que a troca de quatro por três é o preço de atravessar a porta que só aceita texto. Codifique tudo, proteja só o que precisa de um cadeado de verdade. E quando você precisar ir na direção oposta, desempacotando uma string de volta nos bytes que começaram a viagem, o artigo irmão cobre a decodificação em Rust, completo com o placar inteiro de mensagens de erro exatas.
Última atualização: 2026-09-08
Artigo relacionado: Decodificação Base64 em Rust: um guia completo