Decodifica Base64 in Rust: una guida completa
Una stringa atterra nel tuo programma Rust: lettere e cifre, ogni tanto un più e una barra, e uno o due = sospetti appesi alla fine. È Base64, e questa guida è su come riottenere i byte originali senza sorprese. La home page di questo sito passa in rassegna il formato in tutta la sua profondità, qui serve solo ribadire la forma dello scambio: quattro caratteri dell'alfabeto stanno per tre byte in ingresso, e una coda di uno o due caratteri = segna dove finivano i dati veri. La decodifica esegue quello scambio al contrario, e tutto quello che segue riguarda il farlo con intenzione.
Il dettaglio che distingue Rust dalla maggior parte degli altri linguaggi: la libreria standard non ha il Base64 in assoluto. Non c'è un base64_decode() nascosto in std, e nessun use std::... che lo convochi. L'ecosistema si è assestato su una singola crate semplicemente chiamata base64, ed è diventata portante: la versione 0.23.1 è uscita il 4 agosto 2026, la crate ha pubblicato 45 versioni dalla prima release di dicembre 2015, e il suo contatore di download è vicino a 1,5 miliardi. Quasi certamente stai già decodificando Base64 attraverso questa crate, direttamente o trascinata in campo da qualcosa come jsonwebtoken, pem o serde_with, che ne dipendono tutti.
L'unica crate e il suo giro
Se Rust non c'è ancora sulla macchina, il tuo sistema operativo te lo fornisce: rustc e cargo su Debian e Ubuntu, un pacchetto o un installer su macOS e Windows, o l'installer ufficiale che configura rustup:
# Debian / Ubuntu
sudo apt install rustc cargo
# o l'installer ufficiale, che configura rustup e cargo
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Poi la crate, dentro qualsiasi progetto cargo. Questa singola riga è l'intera installazione, e non tira in nessuna dipendenza:
cargo new my-app
cd my-app
cargo add base64
Tre feature opzionali modellano la build. std è attiva di default e abilita i tipi streaming di std::io, le implementazioni standard di Error e l'allocazione sull'heap. alloc fornisce le API con allocazione per le build embedded no_std senza una libreria standard completa. simd-unsafe è attiva di default e decide l'accesso ai motori vettorizzati che incontrerai più avanti. La versione minima di Rust supportata è 1.71.0, quindi qualsiasi versione recente la esegue. Intorno alla crate centrale orbita un piccolo giro di specialisti, ognuno per un caso limite che il nucleo lascia deliberatamente a te:
| Crate | Versione (2026) | Cosa porta | Usala quando |
|---|---|---|---|
base64ct |
1.8 | Decodifica in tempo costante dal progetto RustCrypto; le API in heap stanno dietro una feature alloc |
I byte che decodifichi potrebbero rivelare informazioni per via dei tempi di esecuzione, come il materiale delle chiavi |
data-encoding |
2.11 | Base64 accanto a base32, hex e soci, con varianti MIME tolleranti e codifica/decodifica a livello di slice | Un componente deve analizzare input sporchi, con a capo o multi-protocollo |
base64-turbo |
0.3 | Un codec più recente che arriva a punte oltre 100 GiB/s, con kernel AVX512, AVX2 e NEON più un fallback scalare sicuro | Il throughput è l'unico obiettivo e il motore standard lascia cicli sul tavolo |
Nessuno di questi sostituisce il lavoro di tutti i giorni. Per la vastissima maggioranza dei programmi Rust, base64 da solo è la risposta corretta e completa, e il resto di questo articolo usa quella singola crate per la decodifica in sé, chiamando gli specialisti del giro solo quando il lavoro è più largo del base64.
Tre righe per i tuoi byte
Il novanta per cento della vita di decodifica sta in tre righe. Lo smoke test canonico usa la famosa stringa 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"));
}
L'output è Man, e tre cose in quella piccola cerimonia valgono la pena di tenerle. Prima, decode() ti consegna sempre un Vec<u8>, mai una stringa. È una qualità, non un caso: il Base64 può portare una frase, una JPEG o un certificato, e nessuno dei tre va trattato diversamente prima di sapere cosa hai in mano. Secondo, il salto dai byte al testo è un passo separato e intenzionale attraverso String::from_utf8(), ed è in quel passo che vive la decisione sul charset. Terzo, il modulo prelude ti consegna in silenzio due cose insieme: il motore BASE64_STANDARD e il trait Engine i cui metodi stai chiamando. Se preferisci import espliciti, use base64::engine::general_purpose::STANDARD; insieme a use base64::Engine; è la stessa porta, con il cartellino.
E perché prima o poi decoderai qualcosa che hai codificato tu, ecco il giro di andata e ritorno che dimostra che le due direzioni concordano. La codifica ha la sua guida completa sul sito gemello; qui compare solo per produrre dati di test:
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!
}
Tieni TWFu nel taschino come smoke test per ogni percorso di decodifica che scrivi: se trasforma TWFu in Man, la macchina è onesta.
Quattro modi per dire no
Questa è la sezione che ti salva alle 2 di notte, perché quando una stringa di produzione esplode vuoi sapere precisamente di cosa si lamenta la crate. La buona notizia: si lamenta a voce alta e con precisione. DecodeError ha esattamente quattro varianti, ed ecco come suona ciascuna su una famiglia di trasgressori tipici, tutti fatti passare per il motore standard rigoroso:
| Input | Cosa non va | Errore esatto |
|---|---|---|
"SGVs bG8s" |
un spazio si è infilato | Invalid symbol 32, offset 4. |
"SGVs\nbG8s" |
un cambio di riga si è infilato | Invalid symbol 10, offset 4. |
"SG=VsbG8="" |
padding in mezzo alla stringa | Invalid symbol 61, offset 2. |
"SGVsbG8sIHdvcmxkIQ==xx" |
spazzatura dopo il padding | Invalid symbol 61, offset 18. |
"S" |
un simbolo non può formare un byte | Invalid input length: 1 |
"SGV" |
tre simboli senza il padding che deve seguirli | Invalid padding |
"SGVs$bG8="" |
un $ non è nell'alfabeto |
Invalid symbol 36, offset 4. |
Nota come il messaggio Invalid symbol ti dica sia il valore del byte colpevole sia il suo offset, così puoi saltare direttamente sulla scena del crimine. La variante InvalidLength è la schizzinosa: dalla versione 0.22.0 scatta specificamente quando il numero di simboli validi è impossibile, cioè una lunghezza che è uno in più di un multiplo di quattro, mentre le altre lunghezze sbagliate emergono come errori di padding. Ecco il match completo, per i giorni in cui vuoi gestire ogni fallimento a modo suo:
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 severità deliberata risale allo standard in sé. La sezione 12 della RFC 4648 avvisa che i caratteri fuori dall'alfabeto possono essere usati come canale coperto per contrabbandare informazioni fuori banda, o per tirare fuori bug dai parser trascurati, e raccomanda che i decoder li rifiutino. La specifica MIME è la famosa eccezione, dice esplicitamente ai decoder di ignorare i caratteri vaganti, ed è quella forma di input che la sezione sulla posta qui sotto ti mostra come domare.
Tre posizioni sul padding
Ogni stringa Base64 nel mondo fa una promessa silenziosa sul padding, e la versione 0.23 ti lascia scegliere quale promessa far rispettare attraverso l'enum DecodePaddingMode. Ci sono tre modalità, e la differenza di comportamento merita di essere memorizzata. Ecco il tabellone dei risultati per Zm8, che è la parola fo senza il suo =:
| Modalità | "Zm8", senza padding |
"Zm8=", con padding |
Usala quando |
|---|---|---|---|
RequireCanonical, il default |
Err(Invalid padding) |
Ok([102, 111]) |
Produci e consumi i dati tu stesso |
Indifferent |
Ok([102, 111]) |
Ok([102, 111]) |
Ricevi dati da fonti miste |
RequireNone |
Ok([102, 111]) |
Err(Invalid padding) |
Usi un protocollo senza padding |
use base64::engine::general_purpose::{GeneralPurpose, GeneralPurposeConfig, STANDARD_PAD_INDIFFERENT};
use base64::engine::DecodePaddingMode;
use base64::prelude::*;
let strict = BASE64_STANDARD; // RequireCanonical è il default
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)
I valori di default seguono lo standard: la sezione 3.2 della RFC 4648 dice che le implementazioni devono includere i caratteri di pad appropriati alla fine dei dati codificati, a meno che la specifica circostante non dica il contrario, ed è per questo che un motore STANDARD stock li pretende. E la scelta conta per la sicurezza, non solo per pignoleria. Accettare sia la forma con padding sia quella senza dello stesso dato rende il Base64 malleabile: lo stesso payload logico può essere scritto in due modi, e qualsiasi codice che dia per scontata una sola forma canonica di un valore può essere colto di sorpresa. Il paper del 2022 "La malleabilità del Base64 in pratica" (Chatzigiannis e Chalkias, ePrint 2022/361) documenta conseguenze reali, e la documentazione della crate stessa ci rimanda con un link. La regola pratica: scegli una modalità per protocollo, e sii rigoroso a ogni confine dove non controlli tu chi produce i dati.
I bit nascosti dell'ultimo simbolo
Ecco una corruzione che sopravvive a ogni controllo dei caratteri. Ogni simbolo Base64 porta 6 bit, e 3 byte in ingresso (24 bit) diventano esattamente 4 simboli. Quando l'input è di soli 1 o 2 byte, l'ultimo simbolo ha bit inutilizzati, e la RFC è chiara: gli encoder conformi devono impostare a zero quei bit di scorta. Un encoder con bug o malevolo può invece lasciare spazzatura lì, e il risultato passa comunque la prova dell'alfabeto, la prova della lunghezza e la prova del padding, continuando in silenzio a portare una coda corrotta. Il motore rigoroso ti ha le spalle coperte, con un errore dal dettaglio unico che ti mostra persino i bit sospetti:
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
Quel 0b00010011 nell'errore è il valore decodificato del simbolo colpevole con i bit alti illegali inclusi, e la versione 0.23.0 ha reso quel dettaglio visibile proprio perché altrimenti è così difficile da debuggare. Se sai che i tuoi produttori sono scarsi, with_decode_allow_trailing_bits(true) inghiotte la spazzatura invece di rifiutarla. I browser hanno fatto la scommessa opposta: l'algoritmo forgiving-base64 del WHATWG, quello dietro atob() di JavaScript, è esplicitamente tollerante sui bit di riempimento, mentre il default di Rust è l'esaminatore forense. Saprai tu da che parte del tavolo stai.
Base64url: l'alfabeto che viaggia
Il Base64 standard spende gli ultimi due posti dell'alfabeto per + e /, che è esattamente dove gli URL non li vogliono: in una query string, il più significa spazio, la barra avvia un nuovo segmento di path, e un = appeso si legge come un separatore. Così la sezione 5 della RFC 4648 definisce l'alfabeto sicuro per URL e nomi di file, che scambia i due guai per - e _ e, poiché la lunghezza è di solito recuperabile, di solito butta via anche il padding. La RFC avvisa persino che questa codifica non va considerata la stessa del Base64 standard, e i nomi dei motori sono d'accordo:
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]
// il motore standard rifiuta lo stesso input,
// perché un trattino non è affatto nel suo alfabeto
println!("{:?}", base64::prelude::BASE64_STANDARD.decode("----"));
// Err(Invalid symbol 45, offset 0.)
Ecco il motivo per cui la maggior parte degli sviluppatori incontra il base64url: i JSON Web Token. Un JWT è tre parti base64url unite da punti, e dare un'occhiata dentro è una faccenda di cinque righe:
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}
Due avvertimenti onesti. Decodificare un JWT è dare un'occhiata, non fidarsi: la terza parte è la firma, e non significa nulla finché non viene controllata contro una chiave, che è il lavoro della crate jsonwebtoken (versione 11 nel 2026). La versione 11 ha un lato tagliente: serve esattamente una delle feature rust_crypto o aws_lc_rs abilitata in Cargo.toml, altrimenti va in panico la prima volta che firmi o verifichi un token, e il suo builder Validation tratta il claim exp come obbligatorio di default, quindi i token coniati per altre librerie possono avere bisogno di una validazione aggiustata. E la decodifica è dove la scelta dell'alfabeto morde: passa una stringa URL-safe al BASE64_STANDARD, o viceversa, e ottieni un rifiuto, perché -, _ e il padding mancante sono tutti invalidi per l'altro motore. Abbina il motore al protocollo, ogni volta.
I byte non sono parole
Ogni decoder di questo articolo si ferma ai byte per scelta, e in Rust è più facile che nella maggior parte degli altri linguaggi, perché non c'è un passo nascosto sul charset che possa sbagliarsi. Il Base64 è un formato di byte, punto. La domanda "che testo era?" tocca a te, e la risposta di default per il web moderno è UTF-8, che verifichi in una riga di libreria standard:
use base64::prelude::*;
let packed = "Y2Fmw6k="; // la parola cafe con l'accento, compattata
let bytes = BASE64_STANDARD.decode(packed).unwrap();
match std::str::from_utf8(&bytes) {
Ok(text) => println!("{text}"),
Err(_) => eprintln!("not utf-8: {bytes:02x?}"),
}
Il percorso fortunato dei caratteri multibyte copre tutto quello che incontrerai in rete:
| Testo originale | Base64 | Si decodifica |
|---|---|---|
café |
Y2Fmw6k= |
sì |
日本語 |
5pel5pys6Kqe |
sì |
naïve résumé |
bmHDr3ZlIHLDqXN1bcOp |
sì |
😀 |
8J+YgA== |
sì |
π ≈ 3.14159 |
z4Ag4omIIDMuMTQxNTk= |
sì |
E quando il payload non è testo in assoluto, lo stesso codice ha solo una fine diversa. Ecco il magic number di un file PNG, i quattro byte 89 50 4E 47 più la coppia CRLF che 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(); // byte fuori, non testo fuori
La regola pratica è corta: assumi UTF-8, verifica con std::str::from_utf8(), e tratta tutto quello che fallisce come payload di byte per fs::write, un blob del database o qualunque sink da cui sia venuto. L'unica volta che prendi un charset vero è per dati legacy che non sono mai stati migrati. La crate encoding_rs (versione 0.8) chiama per nome le vecchie codifiche e converte:
use base64::prelude::*;
use encoding_rs::Encoding;
let packed = "Y2Fm6Q=="; // cafe con l'accento, compattato da byte Latin-1
let bytes = BASE64_STANDARD.decode(packed).unwrap();
let (text, _, _) = Encoding::for_label(b"windows-1252").unwrap().decode(&bytes);
println!("{text}"); // cafe con l'accento, come UTF-8
Non c'è una modalità "decodifica come Latin-1" da configurare male dentro la crate base64, perché non indovina mai per te. Questa è la disciplina che tieni: la crate ti dà i byte, e decidi tu cosa significano.
L'input che ha sopravvissuto alla posta
Il Base64 che ha sopravvissuto a un sistema di posta porta con sé i cambi di riga: il MIME fa a capo a 76 caratteri per riga (i blocchi PEM a 64), e la specifica MIME dice esplicitamente ai decoder conformi di ignorare i caratteri fuori dall'alfabeto, i cambi di riga inclusi. Il nostro motore è il contrario di MIME-conforme: rifiuta il primo cambio di riga, e il lettore in streaming riporta il rifiuto come errore I/O che avvolge lo stesso preciso DecodeError:
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. })
Entrambe le posizioni risalgono alla stessa RFC, che lascia la scelta alla specifica circostante, e la crate base64 ha scelto di essere quella rigorosa. Non è la prima volta che cambia idea: la versione 0.5.0 è uscita con avvolgimento MIME delle righe integrato e gestione degli spazi bianchi, e la versione 0.10.0 li ha rimossi entrambi, con il motivo che l'avvolgimento era troppo opinato per una libreria generale e complicava la storia no_std. Quindi la ricetta per l'input avvolto del mondo reale è la stessa che suggerisce la documentazione della crate: togli prima i caratteri non alfabetici, poi decodifica. Per una stringa in memoria basta un 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
}
Se preferisci un decoder che guardi dritto attraverso i cambi di riga, la costante BASE64_MIME_PERMISSIVE della crate data-encoding è esattamente quella: decodifica "SGVsbG8s\r\nd29ybGQh\r\n" in Hello,world! senza che tu tocchi una riga. Per i flussi dove non puoi tenere l'intero input in mano, la FAQ della crate punta alla crate iter_read per filtrare il flusso di byte, o a scrivere un piccolo wrapper Read che scarta i byte indesiderati man mano che arrivano. Un'avvertenza prima di costruire un "decoder tollerante" a mano: ignorare in silenzio i caratteri non alfabetici è esattamente il comportamento che la sezione 12 della RFC 4648 segnala come canale coperto, quindi togli solo gli spazi bianchi che ti aspetti, e rifiuta tutto il resto.
Quando il messaggio intero è in mano, il parser di posta fa il base64 per te. La crate mail-parser (versione 0.11) decodifica ogni parte Content-Transfer-Encoding: base64 mentre analizza, quindi gli allegati tornano come byte grezzi, già scolti dall'avvolgimento e decodificati:
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
Quell'allegato GIF si decodifica in 42 byte che iniziano con i quattro byte 47 49 46 38, le lettere ASCII GIF8. Non hai mai scritto una riga di codice Base64, e questo è il punto di usare un parser: i dettagli della codifica sono il problema della libreria.
Il payload grande
Le stringhe sono facili; i file sono dove il Base64 si guadagna lo stipendio, e la crate risponde con la stessa filosofia di streaming del resto dell'io di Rust. Il read::DecoderReader avvolge qualsiasi reader e consegna trasparentemente byte decodificati man mano che leggi, così un file codificato di diversi gigabyte non deve mai stare in memoria. Per l'esempio qui sotto, salva la stringa dGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw== in un semplice file di testo chiamato 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 stessa idea si rimpicciolisce a una riga con io::copy: costruisci un DecoderReader attorno a un file e copialo in qualsiasi writer, e la decodifica avviene lungo il percorso. C'è anche un bel trucco della documentazione ufficiale per validare un payload in spazio costante, usando un buffer a dimensione statica e nessuna allocazione dei dati decodificati:
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, // letto fino alla fine senza errori
Ok(_) => continue,
Err(_) => return false, // qualcosa non era base64
}
}
}
fn main() {
println!("{}", is_valid_base64("SGVsbG8sIHdvcmxkIQ==")); // true
println!("{}", is_valid_base64("dt==")); // false
}
Per i payload grandi che però stanno ancora in un buffer che gestisci tu, l'API delle slice è l'opzione zero sorprese: base64::decoded_len_estimate(len) dà una dimensione massima decodificata conservativa per len simboli, e decode_slice() scrive dritto nel tuo buffer pre-allocato, restituendo esattamente quanti byte ha scritto:
use base64::prelude::*;
let packed = "SGVsbG8sIHdvcmxkIQ==";
let cap = base64::decoded_len_estimate(packed.len()); // 15, massimo conservativo
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 il tuo buffer è troppo piccolo ottieni un pulito DecodeSliceError::OutputSliceTooSmall invece di un panico, e c'è una variante decode_slice_unchecked() che va in panico di proposito, per i posti dove "troppo piccolo" è un errore di programmazione su cui preferisci crashare piuttosto che girarci intorno. Dalla 0.22.0 il controllo della slice è conservativo a tuo favore: fallisce solo quando l'output non sta davvero, quindi un buffer della misura esatta funziona.
Dove vanno i byte decodificati
Il Base64 appare nei progetti Rust molto più spesso di quanto suggerisca "una stringa casuale":
- Risposte API e webhook che incorporano binario o JSON annidato come testo Base64 dentro i loro payload, il classico pattern upload-di-file-come-JSON.
- Ispezione dei JWT, dove decodifichi l'header e il payload per dare un'occhiata ai claim e passi la firma a
jsonwebtokenper la parte che conta davvero. - Dati a forma di email: allegati MIME e qualsiasi cosa passata da un sistema di posta, ed è per questo che esiste la sezione sugli spazi bianchi.
- Blocchi PEM nei certificati e nelle chiavi, le sezioni
-----BEGIN CERTIFICATE-----che ogni stack TLS mastica; la cratepemli analizza per te e, come conviene, si costruisce sopra la cratebase64internamente. - Data URI nascoste dentro l'HTML e il CSS che stai scrapando o renderizzando, la specie
data:image/png;base64,.... - Header di autenticazione Basic HTTP, dove
Basic TWFuOnBhc3M=è soloMan:passin incognito. - Database e file di configurazione, dove qualcuno voleva binario dentro una colonna di testo o una variabile d'ambiente.
- Passaggi di consegne tra linguaggi: un servizio Python compatta un blob, Rust lo scomprime, e entrambi i lati parlano lo stesso alfabeto per definizione.
Per il caso JSON c'è una scorciatoia che vale la pena conoscere: la crate serde_with (versione 3) può annotare un campo di una struct in modo che serde gestisca il Base64 in entrambe le direzioni, codificando i campi Vec<u8> in testo in uscita e decodificandoli di nuovo in entrata, con una variante URL-safe a un parametro di distanza:
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);
Le Data URI prendono un'operazione di stringa in due passi seguita da una decodifica ordinaria: trova la virgola, tieni quello che c'è dopo, e controlla che i metadati finiscano con la parola 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 la regola unica che governa tutto, da ripetere perché ancora becca la gente con le mani nel sacco: il Base64 è nastro imballaggio, non un lucchetto. Non è cifratura e non è compressione - è l'opposto della compressione - e chiunque con questo articolo può invertire tutto ciò che fa. Decodifica liberamente, fidati selettivamente.
Un decennio di passi prudenti
La storia della crate in sé si legge come un lento stringersi delle viti. È apparsa per la prima volta su crates.io nel dicembre 2015, e la versione 0.5.0 ha aggiunto con fierezza il supporto MIME con a capo configurabili e avvolgimento. Poi la versione 0.10.0 nel 2018 ha rimosso l'avvolgimento e la gestione degli spazi bianchi, la libreria che decideva che una crate generale doveva decodificare e lasciare la poesia al livello di applicazione; la stessa release ha aggiunto l'encoder in streaming e il rilevamento dei simboli finali invalidi. La versione 0.20.0 nel 2022 ha introdotto l'astrazione dei motori e ha ribaltato il default del padding, in modo che il motore stock richieda il padding canonico; la 0.21.0 ha deprecato le vecchie funzioni libere come base64::decode() a favore dei metodi dei motori, con la nota del compilatore "Use Engine::decode" (funzionano ancora, ed è per questo che tanto codice legacy compila contento). Nel 2024, la versione 0.22.0 ha affinato la semantica degli errori, ha rifinito cosa significa InvalidLength, e ha accelerato la decodifica del 5 al 10 per cento. E nel luglio 2026, la versione 0.23.0 è arrivata con i motori SIMD, i simboli di padding personalizzati, il messaggio InvalidLastSymbol più chiaro e l'innalzamento dell'MSRV a 1.71, con la patch 0.23.1 del 4 agosto che sistemava la suite di test per le architetture non-SIMD. Un decennio di passi piccoli e prudenti, e la crate che è partita come "È base64. Cosa potrebbe volere di più qualcuno?" oggi consegna kernel vettorizzati.
Il formato è più vecchio del web, ed è per questo che la severità sembra personale. Nel 1987, il protocollo Privacy-Enhanced Mail (RFC 989) doveva portare dati binari sui canali di posta a 7 bit, e standardizzò questa codifica con righe da 64 caratteri; ogni blocco -----BEGIN CERTIFICATE----- a cui il tuo stack TLS si è mai fidato è un discendente diretto di quella decisione. Nel 1996 la specifica MIME (RFC 2045) adottò lo schema, lo chiamò "base64" dal suo alfabeto di 64 caratteri, e fissò la lunghezza di riga di 76 caratteri che ancora oggi fa a capo ai tuoi allegati email. Nel 2006, la RFC 4648 è diventata lo standard che tutti citano: le tabelle degli alfabeti, la variante base64url nella sezione 5, e le regole di severità che questa crate implementa con così tanto visibile piacere.
Fatti divertenti
Perché una guida completa dovrebbe finire con un sorriso:
- La parola "base64" si codifica in
YmFzZTY0. Un formato che si descrive da solo è l'equivalente tecnico di uno specchio che parla in Morse. - La stringa vuota si decodifica in zero byte, ma
"AA=="si decodifica in un byte: un NUL. Nel Base64, "il nulla" e "uno zero" sono creature diverse. - Ogni PNG codificato in Base64 che hai mai visto inizia con
iVBORw0K. È il magic number del PNG in incognito, ed è uno dei prefissi più riconoscibili di tutto internet. - Gli ID dei video di YouTube sono base64url senza padding: un valore di 8 byte dà dodici caratteri Base64, e togliendo il padding finale resta il familiare ID di undici caratteri che puoi incollare ovunque in un URL. Uno degli usi più visibili della modalità senza padding su tutto internet.
- Bash conta in base 64 da anni: il letterale aritmetico
$((64#...))prende le sue cifre nell'ordine0-9,a-z,A-Z, e infine@e_per i valori 62 e 63, così la tua shell porta un alfabeto di 64 caratteri in bella vista. - I vecchi hash delle password di
crypt(3)usavano una variante Base64 il cui alfabeto inizia con./, e ha una proprietà deliziosa: ordinare le stringhe codificate dà lo stesso ordine di ordinare i byte originali. I file di genealogia usavano lo stesso alfabeto per il multimedia incorporato (GEDCOM 5.5; la revisione 5.5.1 l'ha tolta), e la cratebase64lo include comealphabet::CRYPT. - La matematica MIME, come ancora la calcola la vecchia regola pratica: un payload email avvolto costa circa 1,37 volte la sua dimensione originale, più dell'ordine di qualche centinaio di byte di header. L'infrastruttura di posta degli anni '90 davvero pretendeva quel pedaggio su ogni allegato.
- L'intera crate è
#![forbid(unsafe_code)], tranne la featuresimd-unsafeattiva di default, dalla quale si esce piuttosto che entrarci. Una parola, "unsafe", ed è il nome di una feature flag. - Il Base64 è malleabile in un modo che fa spavento agli ricercatori della sicurezza: gli stessi byte possono essere scritti con o senza padding, e con spazzatura nei bit di riempimento, e i decoder tolleranti non se ne accorgeranno. Un paper del 2022 ha dimostrato conseguenze nel mondo reale, ed è per questo che il default rigoroso di questa crate fa la guardia del corpo.
- Il Base64 non è cifratura. Se lo fosse, non potresti leggere l'output di nessun esempio di questo articolo. È un sedile vicino al finestrino, non una cassaforte.
In chiusura
Scegli il motore in base alla compagnia che frequenti: BASE64_STANDARD per tutto quello che produci e controlli, STANDARD_PAD_INDIFFERENT per il confine a fonti miste, URL_SAFE_NO_PAD per token e URL, e un GeneralPurpose fatto a mano quando il protocollo pretende regole sue. Lascia che le quattro varianti di DecodeError facciano il loro preciso lamentarsi, passa i tuoi byte attraverso std::str::from_utf8() prima di chiamarli testo, fai streaming alle cose grandi con DecoderReader, togli solo gli spazi bianchi che ti aspetti, e prendi base64ct quando il timing è una minaccia. Decodifica tutto, fidati solo di quello che si verifica. E se un giorno devi andare nella direzione opposta, compattando byte in una stringa per il viaggio invece di scompattarla, l'articolo gemello copre la codifica in Rust, dalla matematica delle dimensioni al tocco finale in streaming.
Ultimo aggiornamento: 2026-09-08
Articolo correlato: Codifica Base64 in Rust: una guida completa