Base64-decodering in Rust: een complete gids
Een string belandt in je Rust-programma: letters en cijfers, af en toe een plus of een slash, en een of twee verdachte =-tekens die aan het einde ophangen. Dat is Base64, en deze gids gaat over het terugkrijgen van de oorspronkelijke bytes zonder verrassingen. De startpagina van deze site behandelt het formaat in volle diepte, dus hoeft hier alleen de vorm van de ruil herhaald te worden: vier alfabettekens staan voor drie invoerbytes, en een staart van één of twee =-tekens markeert waar de echte data ophield. Decoderen draait die ruil om, en alles hieronder gaat over dat doelbewust te doen.
De ene wending die Rust onderscheidt van de meeste talen: de standaardbibliotheek heeft helemaal geen Base64. Er zit geen base64_decode() verborgen in std, en er is geen use std::... die er een oproept. Het ecosysteem zette in op één enkele crate, simpelweg base64 genoemd, en die werd dragend: versie 0.23.1 verscheen op 4 augustus 2026, de crate publiceerde 45 versies sinds de eerste release in december 2015, en de downloadteller zit rond de 1,5 miljard. Je decodeert Base64 vrijwel zeker al via deze crate, rechtstreeks of meegenomen door iets als jsonwebtoken, pem of serde_with, die het allemaal als afhankelijkheid hebben.
De ene crate en zijn kring
Als Rust zelf nog niet op de machine staat, levert je besturingssysteem hem: rustc en cargo op Debian en Ubuntu, een pakket of installer op macOS en Windows, of de officiële installer die rustup installeert:
# Debian / Ubuntu
sudo apt install rustc cargo
# of de officiële installer, die rustup en cargo installeert
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Daarna de crate, in elk cargo-project. Deze ene regel is de volledige installatie, en hij trekt exact nul afhankelijkheden met zich mee:
cargo new my-app
cd my-app
cargo add base64
Drie optionele features bepalen de compilatie. std staat standaard aan en schakelt de std::io-streamingtypes, de standaard Error-implementaties en heap-toewijzing in. alloc levert de allocerende API's voor ingebedde no_std-builds zonder volledige standaardbibliotheek. simd-unsafe staat standaard aan en bepaalt de gevectoriseerde engines die je straks tegenkomt. De minimum ondersteunde Rust-versie is 1.71.0, dus alles recente draait het. Rond de kern-crate draait een kleine kring van specialisten, elk voor een randgeval dat de kern bewust aan jou overlaat:
| Crate | Versie (2026) | Wat het meebrengt | Grijp ernaar wanneer |
|---|---|---|---|
base64ct |
1.8 | Constant-time decodering van het RustCrypto-project; de heap-API's zitten achter een alloc-feature |
De bytes die je decodeert kunnen via timing informatie lekken, zoals sleuteldata |
data-encoding |
2.11 | Base64 naast base32, hex en meer, met tolerante MIME-varianten en encode/decode op slice-niveau | Eén component moet vuile, omgewikkelde of multi-protocolinvoer parsen |
base64-turbo |
0.3 | Een jongere codec die piekt boven de 100 GiB/s, met AVX512-, AVX2- en NEON-kernels plus een veilige scalaire fallback | Datadoorvoer is het hele punt en de standaard-engine benut niet al zijn cycli |
Geen van deze vervangt het dagelijkse werk. Voor het overgrote deel van de Rust-programma's is base64 alleen al het juiste en complete antwoord, en gebruikt de rest van dit artikel die ene crate voor de decodering zelf; naar de specialisten uit de kring wordt alleen gegrepen wanneer het werk breder is dan base64.
Drie regels naar je bytes
Negentig procent van het decoderen past in drie regels. De canonieke smoke test gebruikt de beroemde TWFu-string:
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"));
}
De uitvoer is Man, en drie dingen uit dat kleine ceremonieeltje zijn het waard om te bewaren. Ten eerste geeft decode() je altijd een Vec<u8>, nooit een string. Dat is een eigenschap, geen toeval: Base64 kan een zin, een JPEG of een certificaat dragen, en geen van die mag anders behandeld worden voordat je weet wat je hebt. Ten tweede is de sprong van bytes naar tekst een afzonderlijke, doelbewuste stap via String::from_utf8(), en op die stap woont het charset-besluit. Ten derde reikt de prelude-module je stilzwijgend twee dingen tegelijk aan: de BASE64_STANDARD-engine en de Engine-trait wiens methodes je aanroept. Als je expliciete importen prefereert, is use base64::engine::general_purpose::STANDARD; samen met use base64::Engine; dezelfde deur, maar met een naamplaatje erop.
En omdat je op den duur iets gaat decoderen dat je zelf hebt gecodeerd, hier de heen-en-weertrip die bewijst dat de twee richtingen het eens zijn. Encoderen krijgt zijn eigen volledige gids op de zustersite; het verschijnt hier alleen om testdata te produceren:
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!
}
Houd TWFu in je achterzak als smoke test voor elke decode-route die je schrijft: als hij TWFu tot Man maakt, dan is de machine eerlijk.
Vier manieren om nee te zeggen
Dit is de sectie die je redt om 2 uur 's nachts, want als een productiestring explodeert, wil je precies weten waarover de crate klaagt. Het goede nieuws: zij klaagt hard en precies. DecodeError heeft exact vier varianten, en zo klinkt elk van die op een stel typische overtreders, allemaal gevoed door de strenge standaard-engine:
| Invoer | Wat er mis is | Exacte fout |
|---|---|---|
"SGVs bG8s" |
er is een spatie binnengeslopen | Invalid symbol 32, offset 4. |
"SGVs\nbG8s" |
er is een regeleinde binnengeslopen | Invalid symbol 10, offset 4. |
"SG=VsbG8="" |
padding in het midden van de string | Invalid symbol 61, offset 2. |
"SGVsbG8sIHdvcmxkIQ==xx" |
afval achter de padding aan | Invalid symbol 61, offset 18. |
"S" |
één symbool kan geen byte vormen | Invalid input length: 1 |
"SGV" |
drie symbolen zonder de padding die erachter moet zitten | Invalid padding |
"SGVs$bG8="" |
een $ zit niet in het alfabet |
Invalid symbol 36, offset 4. |
Let erop hoe het Invalid symbol-bericht je zowel de waarde van het problematische byte als de offset vertelt, zodat je direct naar het toneel van de daad kunt springen. De InvalidLength-variant is de kieskeurige: sinds versie 0.22.0 gaat hij alleen af als het aantal geldige symbolen onmogelijk is, en dat betekent een lengte die één groter is dan een veelvoud van vier, terwijl andere slechte lengtes opduiken als padding-fouten. Hier is de volledige match, voor de dagen dat je elke fout anders wilt afhandelen:
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"),
}
}
De bewuste strengheid gaat terug tot de standaard zelf. Sectie 12 van RFC 4648 waarschuwt dat tekens buiten het alfabet om misbruikt kunnen worden als een sluipkanaal om informatie uit de band te smokkelen, of om fouten in slordige parsers naar boven te halen, en het adviseert decoders ze te weigeren. De MIME-specificatie is de beroemde uitzondering; die vertelt decoders expliciet verdwaalde tekens te negeren, en dat is de invoervorm die de e-mailsectie hieronder leert temmen.
Drie standpunten over padding
Elke Base64-string in het wild doet een stille belofte over padding, en versie 0.23 laat je kiezen welke belofte je wilt afdwingen via de DecodePaddingMode-enum. Er zijn drie modi, en het gedragverschil is de moeite waard om te onthouden. Hier is de scorelijst voor Zm8, dat is het woord fo zonder zijn =:
| Modus | "Zm8", zonder padding |
"Zm8=", met padding |
Gebruik het wanneer |
|---|---|---|---|
RequireCanonical, de standaard |
Err(Invalid padding) |
Ok([102, 111]) |
Je produceert en consumeert de data zelf |
Indifferent |
Ok([102, 111]) |
Ok([102, 111]) |
Je ontvangt data uit gemengde bronnen |
RequireNone |
Ok([102, 111]) |
Err(Invalid padding) |
Je draait een protocol zonder padding |
use base64::engine::general_purpose::{GeneralPurpose, GeneralPurposeConfig, STANDARD_PAD_INDIFFERENT};
use base64::engine::DecodePaddingMode;
use base64::prelude::*;
let strict = BASE64_STANDARD; // RequireCanonical is standaard
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)
De standaardwaarden volgen de standaard: sectie 3.2 van RFC 4648 zegt dat implementaties geschikte padtekens aan het einde van gecodeerde data moeten opnemen, tenzij de omringende specificatie anders zegt, en dat is waarom een standaard STANDARD-engine ze eist. En de keuze doet er voor beveiliging toe, niet alleen om pedanterij. Als je dezelfde data accepteert in zowel met- als zonder-padding-spelling, wordt Base64 vervormbaar: dezelfde logische payload kan op twee manieren geschreven worden, en code die uitgaat van één canonieke spelling van een waarde kan voor verrassingen komen te staan. Het paper uit 2022 "Base64-vervormbaarheid in de praktijk" (Chatzigiannis en Chalkias, ePrint 2022/361) documenteert echte gevolgen, en de documentatie van de crate zelf linkt ernaar. De praktische regel: kies één modus per protocol, en wees streng aan elke grens waar je de producent niet onder controle hebt.
De verborgen bits van het laatste symbool
Hier is een beschadiging die elke tekencontrole overleeft. Elk Base64-symbool draagt 6 bits, en 3 invoerbytes (24 bits) worden exact 4 symbolen. Als de invoer maar 1 of 2 bytes is, dan heeft het laatste symbool ongebruikte bits, en de RFC is duidelijk: conforme encoders moeten die reserve-bits op nul zetten. Een gefoutteerde of kwaadaardige encoder kan er in plaats van afval achterlaten, en het resultaat slaagt dan nog steeds voor de alfabettest, de lengtetest en de paddingtest, terwijl het stilletjes een beschadigde staart meedraagt. De strenge engine staat je bij, met een uitzonderlijk gedetailleerde fout die je zelfs de verdachte bits toont:
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
Die 0b00010011 in de fout is de gedecodeerde waarde van het problematische symbool, inclusief de illegale hoge bits, en versie 0.23.0 maakte dat detail zichtbaar juist omdat het anders zo lastig te debuggen is. Als je weet dat je producers slordig zijn, slurpt with_decode_allow_trailing_bits(true) het afval op in plaats van het af te wijzen. Browsers zetten de tegenovergestelde inzet: het forgiving-base64-algoritme van WHATWG, het algoritme achter atob() van JavaScript, is expliciet tolerant over eindbits, terwijl de Rust-standaard de forensische onderzoeker is. Weet aan welke kant van de tafel je zit.
Base64url: het alfabet dat reist
Standaard Base64 besteedt zijn laatste twee alfabetslots uit aan + en /, precies waar URLs ze niet willen: in een query string betekent plus een spatie, een slash begint een nieuw padsegment, en een ophangend = leest als een scheidingsteken. Daarom definieert sectie 5 van RFC 4648 het URL- en bestandsnaam-veilige alfabet, dat de twee rotkoeien vervangt door - en _ en, omdat de lengte meestal te herleiden is, laat het meestal ook de padding weg. De RFC waarschuwt zelfs dat deze codering niet als hetzelfde als standaard Base64 beschouwd moet worden, en de namen van de engines zijn het daarmee eens:
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]
// de standaard-engine wijst dezelfde invoer af,
// want een koppelteken zit helemaal niet in het alfabet
println!("{:?}", base64::prelude::BASE64_STANDARD.decode("----"));
// Err(Invalid symbol 45, offset 0.)
Nu de reden waarom de meeste ontwikkelaars base64url überhaupt tegenkomen: JSON Web Tokens. Een JWT is drie base64url-delen, samengevoegd met punten, en een blik erin werpen is een kwestie van vijf regels:
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}
Twee eerlijke kanttekeningen. Een JWT decoderen is een blik werpen, geen vertrouwen: het derde deel is de handtekening, en die betekent niets totdat ze is gecontroleerd tegen een sleutel, en dat is het werk van de jsonwebtoken-crate (versie 11 in 2026). Versie 11 heeft één scherpe kant: zij heeft exact één van de features rust_crypto of aws_lc_rs nodig, ingeschakeld in Cargo.toml, anders paniceert zij de eerste keer dat je een token ondertekent of verifieert, en de Validation-builder behandelt de exp-claim als verplicht bij standaard, dus tokens die voor andere bibliotheken geslagen zijn, hebben soms een aangepaste verificatie nodig. En decoderen is waar de alfabetkeuze bijt: geef een URL-veilige string aan BASE64_STANDARD, of andersom, en je krijgt een weigering, want -, _ en ontbrekende padding zijn allemaal ongeldig voor de andere engine. Pas de engine altijd aan het protocol aan.
Bytes zijn geen woorden
Elke decoder in dit artikel stopt opzettelijk bij de bytes, en in Rust is dat makkelijker dan in de meeste talen, want er is geen verborgen charset-stap die mis kan gaan. Base64 is een byteformaat, punt. De vraag "wat was dat voor tekst?" is aan jou om te beantwoorden, en het standaardantwoord voor het moderne web is UTF-8, en dat controleer je in één regel van de standaardbibliotheek:
use base64::prelude::*;
let packed = "Y2Fmw6k="; // het woord cafe met een accent, ingepakt
let bytes = BASE64_STANDARD.decode(packed).unwrap();
match std::str::from_utf8(&bytes) {
Ok(text) => println!("{text}"),
Err(_) => eprintln!("not utf-8: {bytes:02x?}"),
}
De multibyte-route zonder hobbels dekt alles wat je op het net tegenkomt:
| Originele tekst | Base64 | Decodeert terug |
|---|---|---|
café |
Y2Fmw6k= |
ja |
日本語 |
5pel5pys6Kqe |
ja |
naïve résumé |
bmHDr3ZlIHLDqXN1bcOp |
ja |
😀 |
8J+YgA== |
ja |
π ≈ 3.14159 |
z4Ag4omIIDMuMTQxNTk= |
ja |
En als de payload helemaal geen tekst is, krijgt dezelfde code gewoon een andere afloop. Hier is het magische getal van een PNG-bestand, de vier bytes 89 50 4E 47 plus het CRLF-ertje dat volgt:
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 eruit, geen tekst eruit
De vuistregel is kort: ga uit van UTF-8, verifieer met std::str::from_utf8(), en behandel alles dat faalt als een byte-payload voor fs::write, een database-blob of welke bestemming dan ook waar het vandaan kwam. De ene keer dat je naar een echt charset grijpt, is bij bestaande data die nooit gemigreerd is. De encoding_rs-crate (versie 0.8) noemt de oude coderingen en zet ze om:
use base64::prelude::*;
use encoding_rs::Encoding;
let packed = "Y2Fm6Q=="; // cafe met een accent, ingepakt uit Latin-1-bytes
let bytes = BASE64_STANDARD.decode(packed).unwrap();
let (text, _, _) = Encoding::for_label(b"windows-1252").unwrap().decode(&bytes);
println!("{text}"); // cafe met een accent, als UTF-8
Er is geen "decodeer als Latin-1"-modus om mis te configureren in de base64-crate, want die raadt nooit voor je. Dat is de discipline die je aanhoudt: de crate geeft je bytes, en jij bepaalt wat ze betekenen.
Invoer die e-mail heeft overleefd
Base64 die een mailsysteem heeft overleefd, draagt regeleindes mee: MIME breekt af op 76 tekens per regel (PEM-blokken op 64), en de MIME-specificatie vertelt conforme decoders expliciet tekens buiten het alfabet te negeren, regeleindes inbegrepen. Onze engine is het tegenovergestelde van MIME-conform: hij wijst het allererste regeleinde al af, en de streaming-lezer meldt de weigering als een I/O-fout die dezelfde precieze DecodeError verpakt:
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. })
Beide standpunten gaan terug tot dezelfde RFC, die de keuze aan de omringende specificatie overlaat, en de base64-crate koos voor het strenge pad. Het is niet de eerste keer dat het van gedachten veranderde: versie 0.5.0 bracht ingebouwde MIME-regelomwikkeling en witruimte-behandeling, en versie 0.10.0 verwijderde beide, met als reden dat omwikkeling te meningsmakerisch was voor een algemene bibliotheek en het no_std-verhaal vermorzelde. Dus het recept voor omwikkelde invoer uit de echte wereld is hetzelfde dat de eigen documentatie van de crate suggereert: haal eerst de niet-alfabettekens weg, en decodeer daarna. Voor een string in het geheugen is dat één filter:
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
}
Als je liever een decoder wilt die rechtstreeks door de regeleindes heen kijkt, is de data-encoding-crate met zijn BASE64_MIME_PERMISSIVE-constante precies dat: hij decodeert "SGVsbG8s\r\nd29ybGQh\r\n" naar Hello,world! zonder dat je een regel hoeft aan te passen. Voor streams waar je de hele invoer niet kunt vasthouden, wijst de FAQ van de crate naar de iter_read-crate om de bytestream te filteren, of naar het schrijven van een kleine Read-wrapper die de ongewenste bytes wegwerpt zodra ze binnenkomen. Eén waarschuwing voordat je een "tolerante decoder" in eigen huis bouwt: stilzwijgend niet-alfabettekens negeren is precies het gedrag dat sectie 12 van RFC 4648 als sluipkanaal markeert, dus haal alleen de witruimte weg die je verwacht, en wijs alles andere af.
Als je de hele boodschap bij de hand hebt, doet een e-mailparser de base64 voor je. De mail-parser-crate (versie 0.11) decodeert elk Content-Transfer-Encoding: base64-gedeelte tijdens het parsen, zodat bijlagen terugkomen als ruwe bytes, al ontomwikkeld en gedecodeerd:
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
Die GIF-bijlage decodeert naar 42 bytes, beginnend met de vier bytes 47 49 46 38, de ASCII-letters GIF8. Je hebt nooit één regel Base64-code geschreven, en dat is het punt van het gebruik van een parser: de coderingsdetails zijn het probleem van de bibliotheek.
De grote payload
Strings zijn makkelijk; bestanden zijn waar Base64 zich betaalt, en de crate antwoordt met dezelfde streaming-filosofie als de rest van de io van Rust. De read::DecoderReader verpakt elke lezer en reikt transparant gedecodeerde bytes aan terwijl je leest, zodat een gecodeerd bestand van meerdere gigabyte nooit in het geheugen hoeft te passen. Sla voor het onderstaande voorbeeld de string dGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw== op in een gewoon tekstbestand met de naam 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
}
Hetzelfde idee krimpt in met io::copy tot één regel: bouw een DecoderReader rond een bestand en kopieer het naar elke writer, en het decoderen gebeurt onderweg. Er zit ook een mooi trucje uit de officiële documentatie voor het valideren van een payload in constante ruimte, met een statisch gedimensioneerde buffer en helemaal geen toewijzing van de gedecodeerde data:
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, // tot het einde gelezen zonder fout
Ok(_) => continue,
Err(_) => return false, // iets was geen base64
}
}
}
fn main() {
println!("{}", is_valid_base64("SGVsbG8sIHdvcmxkIQ==")); // true
println!("{}", is_valid_base64("dt==")); // false
}
Voor payloads die groot zijn maar nog wel in een buffer passen die jij beheert, is de slice-API de optie zonder verrassingen: base64::decoded_len_estimate(len) geeft een conservatief maximum van de gedecodeerde grootte voor len symbolen, en decode_slice() schrijft rechtstreeks in je vooraf toegewezen buffer en geeft exact terug hoeveel bytes het schreef:
use base64::prelude::*;
let packed = "SGVsbG8sIHdvcmxkIQ==";
let cap = base64::decoded_len_estimate(packed.len()); // 15, conservatief maximum
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!
Als je buffer te klein is, krijg je een nette DecodeSliceError::OutputSliceTooSmall in plaats van een panic, en er is een decode_slice_unchecked()-variant die per ontwerp paniceert, voor de plekken waar "te klein" een programmeurfout is waar je liever op crasht dan mee rondzweeft. Sinds 0.22.0 is de slice-check conservatief in jouw voordeel: hij faalt alleen als de uitvoer echt niet past, dus een exact gedimensioneerde buffer werkt.
Waar gedecodeerde bytes terechtkomen
Base64 duikt in Rust-projecten veel vaker op dan "een willekeurige string" doet vermoeden:
- API-responses en webhooks die binair of geneste JSON als Base64-tekst in hun payloads verstoppen, het klassieke bestand-upload-als-JSON-patroon.
- JWT-inspectie, waarbij je de header en payload decodeert om naar de claims te kijken, en de handtekening aan
jsonwebtokenovergeeft voor het deel dat werkelijk iets betekent. - E-mail-vormige data: MIME-bijlagen en alles wat een mailsysteem is doorgegaan, en daarom bestaat de witruimte-sectie überhaupt.
- PEM-blokken in certificaten en sleutels, de
-----BEGIN CERTIFICATE------secties die elke TLS-stack vermalst; depem-crate parst ze voor je en bouwt, passend bij de naam, intern op debase64-crate. - Data-URIs die verstopt zitten in HTML en CSS die je scrapet of rendert, het
data:image/png;base64,...-type. - HTTP Basic-auth-headers, waar
Basic TWFuOnBhc3M=gewoonMan:passin een vermomming is. - Databases en config-bestanden, waar iemand binair in een tekstkolom of een omgevingsvariabele wilde.
- Overdrachten tussen talen: een Python-service pakt een blob in, Rust pakt hem uit, en beide kanten spreken per definitie hetzelfde alfabet.
Voor het JSON-geval is er een snelweg die de moeite waard is om te kennen: de serde_with-crate (versie 3) kan een struct-veld annoteren zodat serde de Base64 in beide richtingen behandelt, Vec<u8>-velden naar tekst codet bij het uitgaan en ze terug decodeert bij het binnengaan, met een URL-veilige variant op een parameter afstand:
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 vragen om een tweetaps string-operatie gevolgd door een gewone decode: vind de komma, behoud wat erachter zit, en controleer dat de metadata eindigt op het woord 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
En de ene regel die over het geheel heerst, de moeite waard om te herhalen omdat hij mensen nog steeds op het feit betrapt: Base64 is pakband, geen slot. Het is geen versleuteling en geen compressie - het is het tegenovergestelde van compressie - en iedereen met dit artikel kan alles wat het doet omkeren. Decodeer vrij, vertrouw selectief.
Een decennium aan zorgvuldige stappen
De eigen geschiedenis van de crate leest als een langzame aanhaaling van de moeren. Het verscheen voor het eerst op crates.io in december 2015, en versie 0.5.0 voegde trots MIME-ondersteuning toe met instelbare regeleindes en omwikkeling. Daarna verwijderde versie 0.10.0 in 2018 de omwikkeling en de witruimte-behandeling; de bibliotheek besloot dat een allround crate moet decoderen en de poëzie overlaat aan de applicatielaag. Dezelfde release voegde de streaming-encoder en de detectie van ongeldige eindsymbolen toe. Versie 0.20.0 in 2022 introduceerde de engine-abstractie en draaide de padding-standaard om, zodat de standaard-engine canonieke padding vereist; 0.21.0 merkte de oude vrije functies zoals base64::decode() als verouderd, ten gunste van engine-methodes, met de compilernota "Use Engine::decode" (die werken nog steeds, en daarom compileert veel bestaande code zonder morren). In 2024 slijpte versie 0.22.0 de foutsemantiek scherper, verfijnde wat InvalidLength betekent, en maakte decoderen 5 tot 10 procent sneller. En in juli 2026 arriveerde versie 0.23.0 met de SIMD-engines, op maat gemaakte padding-symbolen, het duidelijkere InvalidLastSymbol-bericht en de MSRV-stijging naar 1.71, met de 0.23.1-patch op 4 augustus die de test-suite voor niet-SIMD-architecturen fixeert. Een decennium aan kleine, zorgvuldige stappen, en de crate die begon als "Het is base64. Wat zou iemand meer willen?" levert nu gevectoriseerde kernels.
Het formaat is ouder dan het web, en daarom voelt de strengheid persoonlijk. In 1987 moest het Privacy-Enhanced Mail-protocol (RFC 989) binaire data over 7-bit mailkanalen vervoeren, en het standardiseerde deze codering met regels van 64 tekens; elk -----BEGIN CERTIFICATE------blok dat je TLS-stack ooit vertrouwd heeft, is een directe nakomeling van die beslissing. In 1996 adopteerde de MIME-specificatie (RFC 2045) het schema, noemde het "base64" naar zijn alfabet van 64 tekens, en stelde de regelelengte van 76 tekens in die vandaag nog altijd je e-mailbijlagen omwikkelt. In 2006 werd RFC 4648 de standaard die iedereen citeert: de alfabettabellen, de base64url-variant in sectie 5, en de strengheidsregels die deze crate met zichtbare smaak implementeert.
Leuke feiten
Want een volledige gids moet eindigen met een glimlach:
- Het woord "base64" codeert naar
YmFzZTY0. Een formaat dat zichzelf beschrijft, is de technische equivalent van een spiegel die in Morse praat. - De lege string decodeert naar nul bytes, maar
"AA=="decodeert naar één byte: een NUL. In Base64 zijn "niets" en "een nul" verschillende wezens. - Elke Base64-gecodeerde PNG die je ooit hebt gezien, begint met
iVBORw0K. Dat is het PNG-magische getal in vermomming, en het is een van de meest herkenbare prefixen op internet. - YouTube-video-ID's zijn base64url zonder padding: een waarde van 8 bytes geeft twaalf Base64-tekens, en als je de achterliggende padding weghaalt, blijft de bekende ID van elf tekens over die je overal in een URL kunt plakken. Een van de meest zichtbare toepassingen van de modus zonder padding op het hele internet.
- Bash telt al jaren in basis 64: de aritmetische literaal
$((64#...))neemt zijn cijfers in de volgorde0-9,a-z,A-Z, en tot slot@en_voor de waarden 62 en 63, zodat je shell een alfabet van 64 tekens op volle zichtbaarheid meedraagt. - De oude
crypt(3)-wachtwoordhashes gebruikten een Base64-variant waarvan het alfabet begint met./, en die heeft een lieve eigenschap: als je de gecodeerde strings sorteert, krijg je dezelfde volgorde als bij het sorteren van de originele bytes. Stamboombestanden gebruikten hetzelfde alfabet voor ingebedde multimedia (GEDCOM 5.5; revisie 5.5.1 liet het vallen), en debase64-crate levert het mee alsalphabet::CRYPT. - MIME-rekenkunde, zoals de oude vuistregel het nog altijd berekent: een omwikkelde e-mail-payload kost ongeveer 1,37 keer de oorspronkelijke grootte, plus zo'n enkele honderden bytes aan headers. De mailinfrastructuur van de jaren 90 legde die tol echt op elke bijlage op.
- De hele crate is
#![forbid(unsafe_code)], op het standaard-aan-simd-unsafe-feature na, dat je uitschakelt in plaats van inschakelt. Eén woord, "unsafe", en het is de naam van een feature-flag. - Base64 is vervormbaar op een manier die beveiligingsonderzoekers de stuipen op het lijf jaagt: dezelfde bytes kunnen geschreven worden met of zonder padding, en met afval in de eindbits, en tolerante decoders merken het niet. Een paper uit 2022 toonde de gevolgen in de echte wereld, en daarom voelt de strenge standaard in deze crate als een bodyguard.
- Base64 is geen versleuteling. Anders zou je de uitvoer van geen enkel voorbeeld in dit artikel kunnen lezen. Het is een stoel bij het raam, geen kluis.
Afronding
Kies je engine naar het gezelschap dat je kiest: BASE64_STANDARD voor alles wat je zelf produceert en onder controle hebt, STANDARD_PAD_INDIFFERENT voor de grens met gemengde bronnen, URL_SAFE_NO_PAD voor tokens en URLs, en een zelfgebouwde GeneralPurpose wanneer het protocol zijn eigen regels eist. Laat de vier DecodeError-varianten hun precieze klagen doen, stuur je bytes via std::str::from_utf8() voordat je ze tekst noemt, stream de grote dingen met DecoderReader, haal alleen de witruimte weg die je verwacht, en grijp naar base64ct wanneer timing een bedreiging is. Decodeer alles, vertrouw alleen wat verifieert. En als je op een dag de andere richting op moet, bytes in een string inpakken voor de rit in plaats van uitpakken, dan dekt het zusterartikel encoderen in Rust, van de groottenrekening tot de streaming-afsluiting.
Laatst bijgewerkt: 2026-10-06
Gerelateerd artikel: Base64-codering in Rust: een complete gids