Base64-Dekodierung in Rust: Ein vollständiger Leitfaden
Ein String landet in Ihrem Rust-Programm: Buchstaben und Ziffern, hin und wieder ein Plus oder ein Schrägstrich, und ein oder zwei verdächtige =-Zeichen, die am Ende baumeln. Das ist Base64, und dieser Leitfaden dreht sich darum, die ursprünglichen Bytes zurückzubekommen, ohne Überraschungen. Die Startseite dieser Site erklärt das Format in voller Tiefe, also muss hier nur noch die Form des Tauschs wiederholt werden: Vier Alphabetzeichen stehen für drei Eingabe-Bytes, und ein Schwanz aus ein oder zwei =-Zeichen markiert, wo die echten Daten aufhörten. Das Dekodieren läuft denselben Tausch in die Gegenrichtung, und alles Weitere in diesem Artikel dreht sich darum, das mit Bedacht zu tun.
Der eine Twist, der Rust von den meisten anderen Sprachen abhebt: Die Standard-Bibliothek hat gar kein Base64 an Bord. Es lauert kein base64_decode() in std, und kein use std::... beschwört eines herbei. Das Ökosystem hat sich auf ein einziges Crate einfach namens base64 geeinigt, und es ist lasttragend geworden: Version 0.23.1 erschien am 4. August 2026, das Crate hat seit seinem ersten Release im Dezember 2015 45 Versionen veröffentlicht, und der Download-Zähler steht bei knapp 1,5 Milliarden. Sie dekodieren Base64 mit hoher Wahrscheinlichkeit ohnehin schon über dieses Crate, direkt oder mitgezogen von etwas wie jsonwebtoken, pem oder serde_with, die alle von ihm abhängen.
Das eine Crate und sein Kreis
Falls Rust selbst noch nicht auf dem Rechner ist, liefert Ihr Betriebssystem es mit: rustc und cargo auf Debian und Ubuntu, ein Paket oder Installer auf macOS und Windows, oder der offizielle Installer, der rustup einrichtet:
# Debian / Ubuntu
sudo apt install rustc cargo
# oder der offizielle Installer, der rustup und cargo einrichtet
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Dann das Crate, in einem beliebigen cargo-Projekt. Diese eine Zeile ist die gesamte Installation, und sie zieht genau null Abhängigkeiten mit:
cargo new my-app
cd my-app
cargo add base64
Drei optionale Features formen den Build. std ist standardmäßig an und schaltet die std::io-Streaming-Typen, die üblichen Error-Implementierungen und die Heap-Zuweisung frei. alloc stellt die allozierenden APIs für eingebettete no_std-Builds ohne vollständige Standard-Bibliothek bereit. simd-unsafe ist standardmäßig an und gibt die vektorisierten Engines frei, auf die Sie später stoßen. Die minimal unterstützte Rust-Version ist 1.71.0, also läuft es auf jeder aktuellen Version. Um das Kern-Crate kreist ein kleiner Kreis von Spezialisten, jeder für einen Grenzfall, den der Kern Ihnen bewusst überlässt:
| Crate | Version (2026) | Was es bringt | Greifen Sie danach, wenn |
|---|---|---|---|
base64ct |
1.8 | Konstantzeit-Dekodierung aus dem RustCrypto-Projekt; die Heap-APIs sitzen hinter einem alloc-Feature |
Die Bytes, die Sie dekodieren, könnten über Timing Informationen preisgeben, etwa Schlüsselmaterial |
data-encoding |
2.11 | Base64 neben base32, hex und Freunden, mit nachsichtigen MIME-Varianten und Kodierung/Dekodierung auf Slice-Ebene | Eine Komponente muss schmutzige, umgebrochene oder Eingaben aus mehreren Protokollen parsen |
base64-turbo |
0.3 | Ein neuerer Codec, der Spitzen über 100 GiB/s erreicht, mit AVX512-, AVX2- und NEON-Kernels plus sicherem skalarem Fallback | Durchsatz ist der ganze Punkt und die Standard-Engine lässt Taktzyklen ungenutzt |
Keines davon ersetzt die Alltagsarbeit. Für die große Mehrheit der Rust-Programme ist base64 allein die richtige und vollständige Antwort, und der Rest dieses Artikels verwendet genau dieses eine Crate für das Dekodieren selbst und ruft die Spezialisten des Kreises nur herbei, wenn die Aufgabe breiter ist als base64.
Drei Zeilen zu Ihren Bytes
Neunzig Prozent des Dekodier-Alltags passen in drei Zeilen. Der kanonische Smoke-Test verwendet den berühmten 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"));
}
Die Ausgabe ist Man, und drei Dinge in dieser kleinen Zeremonie lohnen es, behalten zu werden. Erstens übergibt Ihnen decode() immer einen Vec<u8>, niemals einen String. Das ist eine Eigenschaft, kein Zufall: Base64 kann einen Satz tragen, ein JPEG oder ein Zertifikat, und keines davon sollte anders behandelt werden, bevor Sie wissen, was Sie da haben. Zweitens ist der Sprung von Bytes zu Text ein eigener, bewusster Schritt über String::from_utf8(), und genau in diesem Schritt lebt die Zeichensatz-Entscheidung. Drittens reicht Ihnen das prelude-Modul still und leise gleich zwei Dinge: die BASE64_STANDARD-Engine und den Engine-Trait, dessen Methoden Sie aufrufen. Wenn Sie explizite Imports bevorzugen, ist use base64::engine::general_purpose::STANDARD; zusammen mit use base64::Engine; dieselbe Tür, nur mit Namensschild.
Und weil Sie irgendwann etwas dekodieren werden, das Sie selbst kodiert haben, hier der Round Trip, der beweist, dass sich beide Richtungen einig sind. Das Kodieren bekommt seinen eigenen vollständigen Leitfaden auf der Schwestersite; es taucht hier nur auf, um Testdaten zu fabrizieren:
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!
}
Bewahren Sie TWFu in der Hosentasche als Smoke-Test für jeden Decode-Weg, den Sie schreiben: Wenn er aus TWFu ein Man macht, ist die Maschine ehrlich.
Vier Arten zu sagen: Nein
Das ist der Abschnitt, der Sie um 2 Uhr nachts rettet, denn wenn ein Produktiv-String explodiert, wollen Sie genau wissen, worüber das Crate meckert. Die gute Nachricht: Es meckert laut und präzise. DecodeError hat genau vier Varianten, und so hört man jede von ihnen an einer Familie typischer Übeltäter, allesamt über die strenge Standard-Engine gefüttert:
| Eingabe | Was falsch ist | Exakter Fehler |
|---|---|---|
"SGVs bG8s" |
ein Leerzeichen hat sich hereingeschlichen | Invalid symbol 32, offset 4. |
"SGVs\nbG8s" |
ein Zeilenumbruch hat sich hereingeschlichen | Invalid symbol 10, offset 4. |
"SG=VsbG8="" |
Padding in der Mitte des Strings | Invalid symbol 61, offset 2. |
"SGVsbG8sIHdvcmxkIQ==xx" |
Müll hinter dem Padding | Invalid symbol 61, offset 18. |
"S" |
ein Symbol kann kein Byte bilden | Invalid input length: 1 |
"SGV" |
drei Symbole ohne das Padding, das folgen muss | Invalid padding |
"SGVs$bG8="" |
ein $ steht nicht im Alphabet |
Invalid symbol 36, offset 4. |
Beachten Sie, wie Ihnen die Invalid symbol-Meldung sowohl den Wert des Übeltäter-Bytes als auch seinen Offset verrät, damit Sie direkt zur Tatstelle springen können. Die InvalidLength-Variante ist die pingelige: Seit Version 0.22.0 tritt sie gezielt auf, wenn die Anzahl der gültigen Symbole unmöglich ist, das heißt eine Länge, die um eins größer ist als ein Vielfaches von vier, während andere falsche Längen sich als Padding-Fehler zeigen. Hier ist der vollständige Match, für die Tage, an denen Sie jeden Fehler anders behandeln wollen:
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"),
}
}
Die bewusste Strenge geht auf den Standard selbst zurück. RFC 4648, Abschnitt 12 warnt, dass Zeichen außerhalb des Alphabets als verborgener Kanal missbraucht werden können, um Informationen außerhalb des Bandes zu schmuggeln, oder um Bugs in schlampigen Parsers anzustupsen, und empfiehlt, dass Dekodierer sie ablehnen. Die MIME-Spezifikation ist die berühmte Ausnahme, die Dekodierern ausdrücklich sagt, streunende Zeichen zu ignorieren, und genau das ist die Eingabeform, der der E-Mail-Abschnitt unten zeigt, wie man sie zähmt.
Drei Haltungen zum Padding
Jeder Base64-String in der Wildnis macht ein stillschweigendes Versprechen über das Padding, und Version 0.23 lässt Sie wählen, welches Versprechen durch die DecodePaddingMode-Enum erzwungen wird. Es gibt drei Modi, und der Verhaltensunterschied ist es wert, gemerkt zu werden. Hier ist die Scorecard für Zm8, das Wort fo ohne sein =:
| Modus | "Zm8", ohne Padding |
"Zm8=", mit Padding |
Verwenden Sie es, wenn |
|---|---|---|---|
RequireCanonical, der Standard |
Err(Invalid padding) |
Ok([102, 111]) |
Sie erzeugen und verbrauchen die Daten selbst |
Indifferent |
Ok([102, 111]) |
Ok([102, 111]) |
Sie empfangen Daten von gemischten Quellen |
RequireNone |
Ok([102, 111]) |
Err(Invalid padding) |
Sie betreiben ein no-padding-Protokoll |
use base64::engine::general_purpose::{GeneralPurpose, GeneralPurposeConfig, STANDARD_PAD_INDIFFERENT};
use base64::engine::DecodePaddingMode;
use base64::prelude::*;
let strict = BASE64_STANDARD; // RequireCanonical ist der Standard
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)
Die Defaults folgen dem Standard: RFC 4648, Abschnitt 3.2 sagt, dass Implementierungen passende Pad-Zeichen am Ende kodierter Daten enthalten müssen, es sei denn, die umgebende Spezifikation sagt etwas anderes, und deshalb verlangt die vorgegebene STANDARD-Engine sie. Und die Wahl ist eine Sicherheitsfrage, nicht nur Pedanterie. Beide Schreibweisen derselben Daten zu akzeptieren, mit und ohne Padding, macht Base64 formbar: dieselbe logische Nutzlast kann auf zwei Arten geschrieben werden, und jeder Code, der von genau einer kanonischen Schreibweise eines Wertes ausgeht, kann überrascht werden. Das Paper von 2022 "Base64 Malleability in Practice" (Chatzigiannis und Chalkias, ePrint 2022/361) dokumentiert reale Konsequenzen, und die Dokumentation des Crates selbst verlinkt darauf. Die praktische Regel: Wählen Sie einen Modus pro Protokoll, und seien Sie an jeder Grenze streng, an der Sie den Erzeuger nicht kontrollieren.
Die verborgenen Bits des letzten Symbols
Hier ist eine Korruption, die jede Zeichenprüfung überlebt. Jedes Base64-Symbol trägt 6 Bits, und 3 Eingabe-Bytes (24 Bits) werden genau zu 4 Symbolen. Wenn die Eingabe nur 1 oder 2 Bytes hat, hat das letzte Symbol ungenutzte Bits, und der RFC ist eindeutig: konforme Encoder müssen diese freien Bits auf null setzen. Ein fehlerhafter oder bösartiger Encoder kann stattdessen dort Müll hinterlassen, und das Ergebnis besteht trotzdem den Alphabet-Test, den Längentest und den Padding-Test, während es still einen korrupten Schwanz trägt. Die strenge Engine hat Ihren Rücken, mit einem ungewöhnlich detaillierten Fehler, der Ihnen sogar die verdächtigen Bits zeigt:
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
Das 0b00010011 im Fehler ist der dekodierte Wert des Übeltäter-Symbols, inklusive der illegalen hohen Bits, und Version 0.23.0 machte dieses Detail sichtbar, gerade weil es sich sonst so schwer debuggen lässt. Wenn Sie wissen, dass Ihre Erzeuger schlampig sind, schluckt with_decode_allow_trailing_bits(true) den Müll statt ihn abzulehnen. Browser haben die gegenteilige Wette platziert: Der forgiving-base64-Algorithmus von WHATWG, der hinter JavaScripts atob() steckt, ist ausdrücklich nachsichtig gegenüber Bits am Ende, während Rusts Default der forensische Untersucher ist. Wissen Sie, auf welcher Seite des Tisches Sie sitzen.
Base64url: Das Alphabet, das mitreist
Standard-Base64 gibt seine letzten zwei Alphabetplätze an + und /, genau dorthin, wo sie in URLs niemand haben will: In einer Query-String bedeutet Plus Leerzeichen, ein Schrägstrich beginnt ein neues Pfadsegment, und ein baumelndes = liest sich wie ein Trennzeichen. Deshalb definiert RFC 4648, Abschnitt 5, das URL- und Dateinamen-sichere Alphabet, das die beiden Unruhestifter gegen - und _ tauscht und, weil sich die Länge meist zurückgewinnen lässt, meist auch das Padding weglässt. Der RFC warnt sogar, dass diese Kodierung nicht als identisch mit Standard-Base64 betrachtet werden sollte, und die Engine-Namen stimmen zu:
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]
// die Standard-Engine lehnt dieselbe Eingabe ab,
// denn ein Gedankenstrich steht in ihrem Alphabet überhaupt nicht
println!("{:?}", base64::prelude::BASE64_STANDARD.decode("----"));
// Err(Invalid symbol 45, offset 0.)
Und jetzt der Grund, warum die meisten Entwickler überhaupt auf base64url treffen: JSON Web Tokens. Ein JWT sind drei base64url-Teile, verbunden durch Punkte, und ein Blick hinein ist eine Sache von fünf Zeilen:
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}
Zwei ehrliche Hinweise. Ein JWT zu dekodieren heißt hineinsehen, nicht vertrauen: Der dritte Teil ist die Signatur, und sie bedeutet nichts, bevor sie gegen einen Schlüssel geprüft ist, und das ist die Aufgabe des jsonwebtoken-Crate (Version 11 im Jahr 2026). Version 11 hat eine scharfe Kante: Sie braucht genau eines der Features rust_crypto oder aws_lc_rs aktiviert in Cargo.toml, sonst panikt sie beim ersten Signieren oder Verifizieren eines Tokens, und ihr Validation-Builder behandelt den exp-Claim als standardmäßig erforderlich, so dass Tokens, die für andere Bibliotheken geprägt wurden, eine angepasste Validierung brauchen können. Und beim Dekodieren beißt die Alphabetswahl zurück: Füttern Sie einen URL-sicheren String an BASE64_STANDARD, oder umgekehrt, und Sie bekommen eine Ablehnung, denn -, _ und fehlendes Padding sind für die andere Engine alle ungültig. Passen Sie die Engine immer an das Protokoll an.
Bytes sind keine Wörter
Jeder Dekodierer in diesem Artikel hält aus Absicht an den Bytes an, und in Rust ist das leichter als in den meisten Sprachen, denn es gibt keinen versteckten Zeichensatz-Schritt, der schiefgehen kann. Base64 ist ein Byte-Format, Punkt. Die Frage "Was für ein Text war das?" beantworten Sie selbst, und die Standardantwort für das moderne Web ist UTF-8, das Sie mit einer Zeile aus der Standard-Bibliothek kontrollieren:
use base64::prelude::*;
let packed = "Y2Fmw6k="; // das Wort cafe mit Akzent, gepackt
let bytes = BASE64_STANDARD.decode(packed).unwrap();
match std::str::from_utf8(&bytes) {
Ok(text) => println!("{text}"),
Err(_) => eprintln!("not utf-8: {bytes:02x?}"),
}
Der Multibyte-Happy-Path deckt alles ab, was Sie auf der Leitung antreffen werden:
| Originaltext | Base64 | Dekodiert zurück |
|---|---|---|
café |
Y2Fmw6k= |
ja |
日本語 |
5pel5pys6Kqe |
ja |
naïve résumé |
bmHDr3ZlIHLDqXN1bcOp |
ja |
😀 |
8J+YgA== |
ja |
π ≈ 3.14159 |
z4Ag4omIIDMuMTQxNTk= |
ja |
Und wenn die Nutzlast gar kein Text ist, bekommt derselbe Code nur ein anderes Ende. Hier ist die Magie-Zahl einer PNG-Datei, die vier Bytes 89 50 4E 47 plus das CRLF-Paar, das folgt:
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 raus, kein Text raus
Die Faustregel ist kurz: Nehmen Sie UTF-8 an, verifizieren Sie mit std::str::from_utf8(), und behandeln Sie alles, was fehlschlägt, als Byte-Nutzlast für fs::write, ein Datenbank-Blob oder welchen Sink auch immer, aus dem sie stammt. Der eine Moment, in dem Sie nach einem echten Zeichensatz greifen, sind Legacy-Daten, die nie migriert wurden. Das encoding_rs-Crate (Version 0.8) benennt die alten Kodierungen und wandelt um:
use base64::prelude::*;
use encoding_rs::Encoding;
let packed = "Y2Fm6Q=="; // cafe mit Akzent, gepackt aus Latin-1-Bytes
let bytes = BASE64_STANDARD.decode(packed).unwrap();
let (text, _, _) = Encoding::for_label(b"windows-1252").unwrap().decode(&bytes);
println!("{text}"); // cafe mit Akzent, als UTF-8
Im base64-Crate gibt es keinen "als Latin-1 dekodieren"-Modus, den Sie falsch konfigurieren könnten, denn es rät nie für Sie. Das ist die Disziplin, die Sie bewahren: Das Crate gibt Ihnen Bytes, und Sie entscheiden, was sie bedeuten.
Eingabe, die die E-Mail überlebt hat
Base64, das ein Mailsystem überlebt hat, trägt Zeilenumbrüche: MIME bricht bei 76 Zeichen pro Zeile um (PEM-Blöcke bei 64), und die MIME-Spezifikation sagt konformen Dekodierern ausdrücklich, Zeichen außerhalb des Alphabets zu ignorieren, Zeilenumbrüche eingeschlossen. Unsere Engine ist das Gegenteil von MIME-konform: Sie lehnt schon den allerersten Zeilenumbruch ab, und der Streaming-Reader meldet die Ablehnung als I/O-Fehler, der denselben präzisen DecodeError einwickelt:
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 Positionen gehen auf denselben RFC zurück, der die Wahl der umgebenden Spezifikation überlässt, und das base64-Crate hat sich entschieden, das strenge zu sein. Es ist nicht das erste Mal, dass es seine Meinung geändert hat: Version 0.5.0 lieferte eingebauten MIME-Zeilenumbruch und Weißraum-Handling, und Version 0.10.0 entfernte beides, mit der Begründung, dass Umbrechen für eine allgemeine Bibliothek zu meinungsvoll war und die no_std-Geschichte verkomplizierte. Also ist das Rezept für echte umgebrochene Eingabe aus der Praxis dasselbe, das die Dokumentation des Crates selbst vorschlägt: Entfernen Sie zuerst die Nicht-Alphabet-Zeichen, und dekodieren Sie dann. Für einen String im Speicher ist es ein 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
}
Wenn Sie lieber einen Dekodierer hätten, der die Umbrüche einfach durchsieht, ist die data-encoding-Crate-Konstante BASE64_MIME_PERMISSIVE genau das: Sie dekodiert "SGVsbG8s\r\nd29ybGQh\r\n" zu Hello,world!, ohne dass Sie eine Zeile anfassen. Für Streams, bei denen Sie die ganze Eingabe nicht im Griff haben, verweist die FAQ des Crates auf das iter_read-Crate zum Filtern des Byte-Streams, oder darauf, einen kleinen Read-Wrapper zu schreiben, der die unerwünschten Bytes verwirft, sobald sie eintreffen. Eine Warnung, bevor Sie einen "nachsichtigen Dekodierer" von Hand bauen: Nicht-Alphabet-Zeichen still zu ignorieren ist genau das Verhalten, das RFC 4648, Abschnitt 12, als verborgenen Kanal markiert, also entfernen Sie nur den Weißraum, den Sie erwarten, und lehnen Sie alles andere ab.
Wenn die ganze Nachricht vorliegt, erledigt ein Mail-Parser die Base64-Arbeit für Sie. Das mail-parser-Crate (Version 0.11) dekodiert jedes Content-Transfer-Encoding: base64-Teil während des Parsens, so dass Anhänge als rohe Bytes zurückkommen, bereits entpackt und dekodiert:
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
Der GIF-Anhang dekodiert zu 42 Bytes, beginnend mit den vier Bytes 47 49 46 38, den ASCII-Buchstaben GIF8. Sie haben keine einzige Zeile Base64-Code geschrieben, und genau das ist der Punkt bei der Verwendung eines Parsers: Die Kodierungsdetails sind das Problem der Bibliothek.
Die große Nutzlast
Strings sind einfach; Dateien sind der Ort, an dem Base64 seinen Lohn verdient, und das Crate antwortet mit derselben Streaming-Philosophie wie der Rest von Rusts io. Der read::DecoderReader verpackt jeden Reader und reicht Ihnen transparent dekodierte Bytes, während Sie lesen, so dass eine mehr-Gigabyte-große kodierte Datei nie in den Speicher passen muss. Für das folgende Beispiel speichern Sie den String dGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw== in eine einfache Textdatei namens 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
}
Dieselbe Idee schrumpft mit io::copy auf eine Zeile: Bauen Sie einen DecoderReader um eine Datei und kopieren Sie sie in einen beliebigen Writer, und das Dekodieren passiert unterwegs. Außerdem gibt es einen herrlichen Trick aus der offiziellen Dokumentation, um eine Nutzlast in konstantem Speicher zu validieren, mit einem statisch dimensionierten Buffer und ohne jegliche Allokation der dekodierten Daten:
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, // bis ans Ende gelesen, ohne Fehler
Ok(_) => continue,
Err(_) => return false, // etwas war kein base64
}
}
}
fn main() {
println!("{}", is_valid_base64("SGVsbG8sIHdvcmxkIQ==")); // true
println!("{}", is_valid_base64("dt==")); // false
}
Für Nutzlasten, die groß sind, aber trotzdem in einen Buffer passen, den Sie verwalten, ist die Slice-API die überraschungsfreie Option: base64::decoded_len_estimate(len) gibt eine konservative maximale dekodierte Größe für len Symbole, und decode_slice() schreibt direkt in Ihren vorallokierten Buffer und gibt genau zurück, wie viele Bytes es geschrieben hat:
use base64::prelude::*;
let packed = "SGVsbG8sIHdvcmxkIQ==";
let cap = base64::decoded_len_estimate(packed.len()); // 15, konservatives 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!
Ist Ihr Buffer zu klein, bekommen Sie ein sauberes DecodeSliceError::OutputSliceTooSmall statt eines Panics, und es gibt eine decode_slice_unchecked()-Variante, die per Design panikt, für die Stellen, an denen "zu klein" ein Programmierfehler ist, auf dem Sie lieber abstürzen, als herumzuschieben. Seit 0.22.0 ist der Slice-Check konservativ zu Ihren Gunsten: Er schlägt nur fehl, wenn die Ausgabe wirklich nicht passt, so dass ein exakt dimensionierter Buffer funktioniert.
Wohin dekodierte Bytes gehen
Base64 taucht in Rust-Projekten viel häufiger auf, als "ein zufälliger String" vermuten lässt:
- API-Antworten und Webhooks, die binäre Daten oder verschachteltes JSON als Base64-Text in ihren Nutzlasten einbetten, das klassische Datei-Upload-als-JSON-Muster.
- JWT-Inspektion, bei der Sie Header und Nutzlast dekodieren, um einen Blick auf die Claims zu werfen, und die Signatur an
jsonwebtokenübergeben, für den Teil, der tatsächlich etwas bedeutet. - E-Mail-förmige Daten: MIME-Anhänge und alles, was durch ein Mailsystem gegangen ist, deshalb existiert der Weißraum-Abschnitt überhaupt.
- PEM-Blöcke in Zertifikaten und Schlüsseln, die
-----BEGIN CERTIFICATE------Abschnitte, die jeder TLS-Stack zermagt; daspem-Crate parst sie für Sie und baut, passend, intern auf dembase64-Crate auf. - Data-URIs, die in HTML und CSS versteckt sind, die Sie scrapen oder rendern, die
data:image/png;base64,...-Art. - HTTP-Basic-Auth-Header, bei denen
Basic TWFuOnBhc3M=einfachMan:passin Verkleidung ist. - Datenbanken und Config-Dateien, in denen jemand binäre Daten in einer Textspalte oder einer Umgebungsvariablen haben wollte.
- Übergaben zwischen Sprachen: Ein Python-Service packt einen Blob, Rust packt ihn aus, und beide Seiten sprechen per Definition dasselbe Alphabet.
Für den JSON-Fall gibt es einen Shortcut, den es zu kennen lohnt: Das serde_with-Crate (Version 3) kann ein Struct-Feld annotieren, damit serde das Base64 in beide Richtungen erledigt, Vec<u8>-Felder beim Rausgehen in Text kodiert und beim Hereinkommen wieder dekodiert, mit einer URL-sicheren Variante nur einen Parameter entfernt:
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 brauchen eine zwei-stufige String-Operation, gefolgt von einem gewöhnlichen Decode: Finden Sie das Komma, behalten Sie das, was danach kommt, und prüfen Sie, dass die Metadaten mit dem Wort base64 enden:
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
Und die eine Regel, die all das regiert, und sich zu wiederholen lohnt, weil sie Menschen noch immer beim Tun erwischt: Base64 ist Verpackungsklebefilm, kein Schloss. Es ist keine Verschlüsselung und keine Komprimierung - es ist das Gegenteil von Komprimierung - und jeder, der diesen Artikel hat, kann alles, was es tut, rückgängig machen. Dekodieren Sie frei, vertrauen Sie selektiv.
Ein Jahrzehnt behutsamer Schritte
Die eigene Geschichte des Crates liest sich wie ein langsames Anziehen der Schrauben. Es erschien erstmals im Dezember 2015 auf crates.io, und Version 0.5.0 fügte stolz MIME-Unterstützung mit konfigurierbaren Zeilenumbrüchen und -umbruch hinzu. Dann entfernte Version 0.10.0 im Jahr 2018 das Umbrechen und das Weißraum-Handling, die Bibliothek entschied, dass ein Allzweck-Crate dekodieren und die Poesie der Anwendungsebene überlassen sollte; dasselbe Release fügte den Streaming-Encoder und die Erkennung ungültiger Endsymbole hinzu. Version 0.20.0 im Jahr 2022 führte die Engine-Abstraktion ein und drehte das Padding-Default, so dass die vorgegebene Engine kanonisches Padding verlangt; 0.21.0 erklärte die alten freien Funktionen wie base64::decode() zugunsten von Engine-Methoden für veraltet, mit dem Compiler-Hinweis "Use Engine::decode" (sie funktionieren noch, und deshalb kompiliert ein Großteil des Legacy-Codes fröhlich weiter). Im Jahr 2024 schärfte Version 0.22.0 die Fehler-Semantik, verfeinerte, was InvalidLength bedeutet, und beschleunigte das Dekodieren um 5 bis 10 Prozent. Und im Juli 2026 traf Version 0.23.0 ein, mit den SIMD-Engines, eigenen Padding-Symbolen, der klareren InvalidLastSymbol-Meldung und dem MSRV-Anstieg auf 1.71, wobei das 0.23.1-Patch am 4. August die Test-Suite für nicht-SIMD-Architekturen reparierte. Ein Jahrzehnt kleiner, behutsamer Schritte, und das Crate, das als "Das ist base64. Was könnte man noch mehr wollen?" begann, liefert jetzt vektorisierte Kernels.
Das Format ist älter als das Web, deshalb fühlt sich die Strenge persönlich an. 1987 musste das Privacy-Enhanced-Mail-Protokoll (RFC 989) binäre Daten über 7-Bit-Mail-Kanäle tragen und standardisierte diese Kodierung mit 64-Zeichen-Zeilen; jeder -----BEGIN CERTIFICATE------Block, dem Ihr TLS-Stack je vertraut hat, ist ein direkter Nachkomme dieser Entscheidung. 1996 übernahm die MIME-Spezifikation (RFC 2045) das Schema, nannte es nach seinem 64-Zeichen-Alphabet "base64" und legte die 76-Zeichen-Zeilenlänge fest, die Ihre E-Mail-Anhänge heute noch umbrechen lässt. 2006 wurde RFC 4648 der Standard, den alle zitieren: die Alphabettabellen, die base64url-Variante in Abschnitt 5 und die Strenge-Regeln, die dieses Crate mit so sichtbarer Inbrunst umsetzt.
Fun-Fakten
Denn ein vollständiger Leitfaden sollte mit einem Lächeln enden:
- Das Wort "base64" kodiert zu
YmFzZTY0. Ein Format, das sich selbst beschreibt, ist das technische Äquivalent eines Spiegels, der in Morse spricht. - Der leere String dekodiert zu null Bytes, aber
"AA=="dekodiert zu einem Byte: einem NUL. In Base64 sind "nichts" und "eine Null" verschiedene Wesen. - Jede Base64-kodierte PNG, die Sie je gesehen haben, beginnt mit
iVBORw0K. Das ist die PNG-Magie-Zahl in Verkleidung, und sie ist einer der wiedererkennbarsten Präfixe im gesamten Internet. - YouTube-Videoids sind base64url ohne Padding: Ein 8-Byte-Wert ergibt zwölf Base64-Zeichen, und das Weglassen des nachgestellten Paddings lässt die vertraute elf Zeichen lange ID, die Sie an jede Stelle einer URL einfügen können. Eine der sichtbarsten Nutzungen des unpadded-Modus im gesamten Internet.
- Bash zählt seit Jahren in Basis 64: Das arithmetische Literal
$((64#...))nimmt seine Ziffern in der Reihenfolge0-9,a-z,A-Z, und schließlich@und_für die Werte 62 und 63, so dass Ihre Shell ein 64-Zeichen-Alphabet in aller Öffentlichkeit mit sich trägt. - Die alten
crypt(3)-Passwort-Hashes verwendeten eine Base64-Variante, deren Alphabet mit./beginnt, und sie hat eine hübsche Eigenschaft: Das Sortieren der kodierten Strings ergibt dieselbe Reihenfolge wie das Sortieren der Original-Bytes. Genealogiedateien verwendeten dasselbe Alphabet für eingebettete Multimedia-Dateien (GEDCOM 5.5; die 5.5.1-Revision ließ es fallen), und dasbase64-Crate liefert es alsalphabet::CRYPT. - MIME-Mathematik, wie sie die alte Faustregel noch berechnet: Eine umgebrochene E-Mail-Nutzlast kostet etwa das 1,37-fache ihrer Originalgröße, dazu in der Größenordnung von ein paar hundert Bytes an Headern. Die Mail-Infrastruktur der 1990er-Jahre hat diese Maut wirklich bei jedem Anhang kassiert.
- Das gesamte Crate ist
#![forbid(unsafe_code)], ausgenommen das standardmäßig aktivesimd-unsafe-Feature, bei dem Sie sich eher heraus- als hineinoptieren. Ein Wort, "unsafe", und es ist der Name eines Feature-Flags. - Base64 ist formbar auf eine Weise, die Sicherheitsforscher gruselt: Dieselben Bytes können mit oder ohne Padding geschrieben werden, und mit Müll in den Bits am Ende, und nachsichtige Dekodierer bemerken es nicht. Ein Paper aus 2022 demonstrierte reale Konsequenzen, und deshalb fühlt sich das strenge Default in diesem Crate wie ein Bodyguard an.
- Base64 ist keine Verschlüsselung. Wäre es es, könnten Sie die Ausgabe keines Beispiels in diesem Artikel lesen. Es ist ein Fensterplatz, kein Tresor.
Zusammenfassung
Wählen Sie Ihre Engine nach dem Kreis, in dem Sie sich bewegen: BASE64_STANDARD für alles, was Sie erzeugen und kontrollieren, STANDARD_PAD_INDIFFERENT für die Grenze zu gemischten Quellen, URL_SAFE_NO_PAD für Tokens und URLs, und ein von Hand gebautes GeneralPurpose, wenn das Protokoll eigene Regeln verlangt. Lassen Sie die vier DecodeError-Varianten ihre präzisen Beschwerden machen, lassen Sie Ihre Bytes durch std::str::from_utf8() gehen, bevor Sie sie Text nennen, streamen Sie die großen Dinge mit DecoderReader, entfernen Sie nur den Weißraum, den Sie erwarten, und greifen Sie zu base64ct, wenn Timing eine Bedrohung ist. Dekodieren Sie alles, vertrauen Sie nur dem, was sich verifiziert. Und falls Sie eines Tages die andere Richtung einschlagen müssen, Bytes in einen String für die Reise packen statt auspacken, deckt der Schwesternartikel das Kodieren in Rust ab, von der Größengerechnung bis zum Streaming-Finish.
Zuletzt aktualisiert: 2026-09-08
Verwandter Artikel: Base64-Kodierung in Rust: Ein vollständiger Leitfaden