Base64-Dekodierung in Dart: Ein vollständiger Leitfaden
Er kommt in einer API-Antwort an, in einer URL oder kopiert in ein Support-Ticket: ein langer Zug aus Buchstaben und Ziffern mit dem gelegentlichen +, /, - oder _, und vielleicht ein Paar =-Zeichen, die am Ende baumeln. Jemand nennt es Base64, und Sie brauchen, was drinsteckt. Dieser Leitfaden ist das Dart-Rezept, es wieder zurückzubekommen. Kurze Orientierung, denn die Startseite geht das Format ausführlich durch: Base64 schreibt jeweils drei Eingabe-Bytes als vier Zeichen aus einem Alphabet von 64 Zeichen um und hängt ein oder zwei =-Pads ans Ende, wenn das letzte Stück kurz ist. Dekodieren ist die schrumpfende Richtung dieses Tauschs: Vier Zeichen gehen rein, drei Bytes kommen raus, also braucht das Ergebnis immer etwa ein Viertel weniger Platz als die Eingabe.
Die gute Nachricht: Es gibt nichts zu installieren. Base64 ist seit Dart 1.13 im Jahr 2015 in der dart:convert-Bibliothek dabei, und die API ist seit Dart 2.0 im Jahr 2018 stabil. Ein Import gibt Ihnen einen schnellen, strengen Dekodierer, der sowohl das Standard-Alphabet als auch das URL-sichere Alphabet liest.
Eine ehrliche Grenze: Dies ist die Dekodierer-Seite der Geschichte. Sie lernen, was der Dekodierer annimmt und ablehnt, wie Padding funktioniert, wie man Bytes in Text zurückverwandelt, ohne Mojibake zu erzeugen, und wie man Base64 in JWTs, Data URIs, Dateien, Streams, E-Mails, Konfiguration und der Kommandozeile begegnet. Die andere Richtung, Bytes in einen String zu packen, hat ihren eigenen Leitfaden, verlinkt am Ende dieses hier.
Die vier Türen zu einer einzigen strengen Maschine
Hier ist die ganze öffentliche Oberfläche, die Sie verwenden werden, alles in dart:convert:
| Eintrag | Was es ist | Greifen Sie danach, wenn |
|---|---|---|
base64Decode(source) |
Top-Level-Funktion, dekodiert in einen Uint8List |
Dekodieren im Alltag, fast immer diese |
base64.decode(source) |
Die decode-Methode des Codec, identisches Verhalten | Sie den Codec für fuse oder Stream-Transformationen wollen |
base64Url.decode(source) |
Die decode-Methode des URL-sicheren Codec | Die Eingabe als URL-sicher dokumentiert wurde (die Maschine ist dieselbe) |
base64Url.normalize(source) |
Validiert und repariert einen String, gibt ihn gepaddet zurück | Die Eingabe ohne Padding sein könnte, Alphabete mischt oder Prozent-Escapes verwendet |
Zwei Dinge fallen auf. Erstens führen alle vier Wege zum selben Dekodierer: eine strenge Zustandsmaschine mit einer Nachschlagetabelle. Zweitens ist die letzte Zeile gar kein Dekodierer. Es ist eine Reparaturwerkstatt, und sie bewährt sich vom ersten Mal an, in dem ein JWT ohne Padding oder ein halb gereinigter Konfigurationswert auftaucht.
Ihr erstes Decode
Neunzig Prozent des Dekodier-Alltags passen in fünf Zeilen. Hier ist das kleinste Beispiel, das die ganze Form der Arbeit zeigt:
import 'dart:convert';
void main() {
final bytes = base64Decode('TWFu');
final text = utf8.decode(bytes);
print(text); // Man
}
Drei Sätze über das, was gerade passiert ist. Erstens gibt der Einstieg Bytes zurück, keinen Text: base64Decode liefert einen Uint8List, und das ist bewusst so, denn die Nutzlast könnte ein Satz, ein JPEG oder ein Hash sein, und keines davon sollte gleich behandelt werden, bevor Sie wissen, was Sie haben. Zweitens ist der Sprung von Bytes zu Text ein eigener, expliziter Schritt mit einem expliziten Zeichensatz, und genau an diesem Schritt wird aus "café" Mojibake, wenn Sie nachlässig sind. Drittens ist der leere String ein Wert erster Klasse: base64Decode('') gibt Ihnen eine Liste der Länge null, ohne Exception und ohne Drama.
Was der Dekodierer annimmt und ablehnt
Darts Dekodierer ist per Design streng. RFC 4648 sagt, dass Implementierungen Eingaben mit Zeichen außerhalb des Alphabets ablehnen sollen, und Dart folgt dieser Lesart aufs Wort: kein Überspringen von Leerzeichen, kein Ignorieren von Zeilenumbrüchen, keine zweiten Chancen. Wenn die Eingabe falsch ist, bekommen Sie eine FormatException, die die Eingabe zeigt und auf das exakte Zeichen verweist. Hier ist das Verhalten an den klassischen Unruhestiftern:
| Eingabe | Was falsch ist | Exakter Fehler |
|---|---|---|
'SGVs bG8s' |
ein Leerzeichen hat sich eingeschlichen | FormatException: Invalid character (at character 5) |
'SGVs\nbG8s' |
ein Zeilenumbruch hat sich eingeschlichen | FormatException: Invalid character (at character 5) |
'SGVs$bG8s' |
ein Dollarzeichen ist nicht im Alphabet | FormatException: Invalid character (at character 5) |
'Zm8' |
gar kein Padding | FormatException: Invalid length, must be multiple of four (at character 4) |
'Zm8==' |
zwei Pads, wo eines hingehört | FormatException: Invalid padding character (at character 5) |
'Zm=8' |
Padding in der Mitte der Daten | FormatException: Invalid encoding before padding (at character 3) |
'Zm8=xx' |
Müll nach den Pads | FormatException: Invalid padding character (at character 5) |
'Zé' |
ein Nicht-ASCII-Zeichen | FormatException: Invalid character (at character 2) |
Die Position in der Meldung ist eine einsbasierte Zeichenanzahl, und die Eingabe wird direkt unter dem Karet ausgegeben, also geht es schnell, eine korrupte Nutzlast per Halbierung einzugrenzen. Eine angenehme Überraschung steckt in der Strenge: Der Dekodierer akzeptiert beide Alphabete. Ein - oder _ in der Mitte eines Standard-Strings ist in Ordnung, und ein + oder / in einem URL-sicheren String ist es auch. Die Alphabetwahl zählt nur, wenn Sie selbst den Text erzeugen, nicht wenn Sie ihn lesen.
Padding: Unverhandelbar
Hier ist die Regel, die die meisten überrascht: Darts Dekodierer verlangt korrektes Padding. Die Eingabe muss ein Vielfaches von vier Zeichen lang sein, und die abschließenden =-Zeichen müssen in genau der richtigen Menge vorhanden sein. Es gibt keinen toleranten Modus, keine Flag zum Lockern und keine Einstellung, die man ändern könnte. Die Gründe sind schlüssig: Dekodieren ohne Padding ist in Grenzfällen mehrdeutig, und der RFC warnt, dass großzügiges Dekodieren einen verdeckten Kanal eröffnen kann, also ist die strenge Lesart die sichere. Was das in der Praxis bedeutet:
| Eingabe | Ergebnis |
|---|---|
'' |
leerer Uint8List, kein Fehler |
'QQ==' |
1 Byte: A |
'QUI=' |
2 Bytes: AB |
'QUJD' |
3 Bytes: ABC |
'Zm8' |
FormatException: ungültige Länge |
'Zm8==' |
FormatException: ungültiges Padding-Zeichen |
Wenn die Eingabe aus einem System kommt, das Padding entfernt, und JWTs sind voller Werte ohne Padding, ist der Reparatur-Schritt ein einziger Aufruf von normalize. Er validiert den String, konvertiert URL-sichere Zeichen ins Standard-Alphabet und fügt die fehlenden Pads hinzu:
import 'dart:convert';
void main() {
final stripped = '-__--Q';
final repaired = base64Url.normalize(stripped);
print(repaired); // +//++Q==
final bytes = base64Decode(repaired);
print('decoded ${bytes.length} bytes'); // decoded 4 bytes
}
Die Prozentzeichen-Überraschung
Die hier ist ein Dart-Original. Wenn Base64 in einer Data URI auftaucht, kodieren manche Tools das Padding prozentkodiert und schreiben %3D statt =, denn ein nacktes = kann in der URL-Syntax "Parameter-Trenner" bedeuten. Die meisten Sprachen würden verlangen, dass Sie zuerst entescape'n. Darts Dekodierer nicht: seine Nachschlagetabelle behandelt %3D als native Schreibweise des Padding-Zeichens, also können Sie ihm die rohe Nutzlast übergeben:
import 'dart:convert';
void main() {
final fromDataUri = 'SGVsbG8%3D';
final bytes = base64Decode(fromDataUri);
print(utf8.decode(bytes)); // Hello
}
Der Escape wird genau dort akzeptiert, wo Padding legal ist, und das ist die abschließende Position. Setzen Sie %3D dorthin, wo ein = abgelehnt würde, und es wird auf dieselbe Weise abgelehnt, und %25 scheitert stattdessen am Padding-Check - % ist Darts natives Padding-Escape-Zeichen, also liest der Dekodierer es als escaped = und lehnt die 2 mit Invalid padding character ab. In der Praxis heißt das, dass eine ;base64,-Nutzlast, direkt aus den Entwickler-Tools eines Browsers kopiert, ohne jede Vorverarbeitung dekodiert, ein kleiner, aber genuinely bequemer Trick.
URL-sicheres Base64
RFC 4648 definiert ein zweites Alphabet aus einem Grund: Das Standard-Alphabet hat drei Zeichen, +, / und =, die mit der URL-Syntax kollidieren. Das URL-sichere Alphabet, im RFC base64url genannt, tauscht + gegen - und / gegen _ aus und lässt oft auch das Padding weg. Es ist das Alphabet von JWTs, Objekt-IDs, Share-Links und allem, was in einer URL oder einem Dateinamen lebt.
Auf der Dekodier-Seite gibt Ihnen Dart eine einzige Antwort: Beide Alphabete werden von derselben Maschine gelesen. base64Decode und base64Url.decode sind zwei Namen für denselben Dekodierer, also ist die einzige echte Arbeit das Padding, denn URL-sichere Erzeuger liefern sehr oft ganz ohne Padding. Genau dafür ist normalize da:
import 'dart:convert';
void main() {
final bytes = [0xfb, 0xff, 0xfe, 0xf9];
final urlSafe = base64UrlEncode(bytes);
print(urlSafe); // -__--Q==
final repaired = base64Url.normalize(urlSafe.replaceAll('=', ''));
print(repaired); // +//++Q==
print(base64Decode(repaired).length); // 4
}
Zwei Fallen zum Mitnehmen. Machen Sie vor dem Dekodieren keine eigene --zu-+-Ersetzung; sie ist unnötig, und normalize macht die Alphabet-Konvertierung bereits, wenn nötig. Und nehmen Sie nicht an, dass ein URL-sicherer String ohne Padding ankommt: Manche Erzeuger behalten die Pads, und der Dekodierer akzeptiert beides, solange das Padding korrekt ist.
Von Bytes zu Text: Die Zeichensatz-Entscheidung
Base64-Dekodieren gibt Ihnen Bytes. Wenn diese Bytes Text sind, müssen Sie den Zeichensatz wählen, der sie in einen String zurückverwandelt, und diese Entscheidung treffen Sie explizit selbst. Die Standard-Annahme in modernen Systemen ist UTF-8, und utf8.decode ist das Arbeitspferd:
import 'dart:convert';
void main() {
final payload = base64Encode(utf8.encode('Héllo Wörld'));
final bytes = base64Decode(payload);
print(utf8.decode(bytes)); // Héllo Wörld
final legacy = base64Encode(latin1.encode('Héllo'));
print(latin1.decode(base64Decode(legacy))); // Héllo
}
Wenn die Bytes kein gültiges UTF-8 sind, wirft utf8.decode eine FormatException, und das ist das richtige Verhalten, weit besser als stilles Mojibake. Wenn Sie wissen, dass die Daten Legacy-Single-Byte-Text sind, verwenden Sie den passenden Zeichensatz:
| Zeichensatz | Wofür | Dekodieren mit |
|---|---|---|
utf8 |
Moderne Texte, JSON, alles aus dem Web | utf8.decode(bytes) |
latin1 |
Legacy-West-Single-Byte-Daten | latin1.decode(bytes) |
ascii |
Einfacher 7-Bit-Text | ascii.decode(bytes) |
Eine Falle verdient ihre eigene Warnung: String.fromCharCodes ist kein Zeichensatz. Es liest Bytes als UTF-16-Codeeinheiten, also füttern Sie ihm die UTF-8-Bytes von Héllo, und es druckt Héllo völlig gelassen. Wenn Sie dieses Mojibake-Muster in Ihrer Ausgabe sehen, ist die Lösung fast immer utf8.decode.
JWTs: Den Token lesen
Ein JSON Web Token besteht aus drei base64url-Teilen, verbunden durch Punkte: Header, Nutzlast, Signatur. Base64 wird hier für Kompaktheit und URL-Sicherheit verwendet, nicht für Geheimhaltung. Jeder mit dem Token kann den Header und die Nutzlast lesen, und das ist beabsichtigt. Die Signatur ist das, was Sie verifizieren, mit dem gemeinsamen Secret oder dem öffentlichen Key des Ausstellers. Die lesbaren Teile in Dart zu dekodieren dauert ein paar Zeilen:
import 'dart:convert';
Map<String, dynamic> readJwtPayload(String token) {
final parts = token.split('.');
if (parts.length != 3) {
throw FormatException('Not a compact JWT');
}
final padded = base64Url.normalize(parts[1]);
final bytes = base64Decode(padded);
return jsonDecode(utf8.decode(bytes)) as Map<String, dynamic>;
}
void main() {
const token =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9'
'.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkRhcnQgRGV2IiwiaWF0IjoxNTE2MjM5MDIyfQ'
'.c2lnbmF0dXJl';
print(readJwtPayload(token)['name']); // Dart Dev
}
Beachten Sie den Padding-Tanz: JWTs werden ohne Padding gebaut, also scheitert ein Teil am direkten base64Decode, wann immer seine Länge kein Vielfaches von vier ist. (Im Beispiel oben ist der Header zufällig 36 Zeichen lang und dekodiert direkt; die Nutzlast ist 74 Zeichen lang und das tut sie nicht.) Der normalize-Aufruf macht die Reparatur gleichmäßig, unabhängig von der Länge. Zwei weitere Warnungen. Dekodieren ist keine Verifizierung: Das Prüfen der Signatur und des exp-Claims ist ein separater, obligatorischer Schritt, normalerweise mit dem crypto-Paket für HMAC-Algorithmen. Und seien Sie misstrauisch gegenüber Tokens, die alg: none behaupten; ein Parser, der sie akzeptiert, ist eine Sicherheitslücke, kein Feature.
Data URIs: Dateien in einem URL-Kostüm
Eine Data URI, definiert durch RFC 2397, ist eine URL, deren Nutzlast die Daten selbst sind: data:image/png;base64, gefolgt von den kodierten Bytes. Sie existieren, damit rein textbasierte Kanäle - HTML-Attribute, CSS-Regeln, JSON-Dokumente - Binärdaten tragen können, ohne eine separate Datei zu brauchen. Base64 ist das Nutzlast-Format der Wahl, denn die Alternative, Prozent-Kodierung, ist für Binärdaten deutlich länger.
Und Dart kann sie nativ parsen: Data-URI-Unterstützung ist seit 2016 in dart:core dabei, also wird keine URI-Bibliothek benötigt:
import 'dart:convert';
void main() {
final uri = Uri.parse('data:image/png;base64,iVBORw0KGgo=');
final data = uri.data!;
print(data.mimeType); // image/png
print(data.isBase64); // true
print('decoded ${data.contentAsBytes().length} bytes');
final textUri = Uri.parse('data:text/plain;base64,SGVsbG8sIERhcnQh');
print(textUri.data!.contentAsString()); // Hello, Dart!
}
Das UriData-Objekt gibt Ihnen den MIME-Typ, die isBase64-Flag, den rohen Nutzlast-Text und den dekodierten Inhalt als String oder als Bytes. Zwei Fallen: Der deklarierte MIME-Typ kann lügen, also prüfen Sie in sicherheitskritischem Code die tatsächlichen Magic Bytes; und Data URIs sind für kleine Assets, denn die ganze Nutzlast reitet im Dokument mit, das darauf verweist.
Dateien: Base64 auf der Platte
Base64-Dateien tauchen in Export-Formaten, Provisioning-Bundles und jedem rein textbasierten Transport auf, der Binärdaten tragen muss. Das Rezept lautet: Text lesen, alle Umbrüche entfernen, dekodieren, Bytes schreiben:
import 'dart:convert';
import 'dart:io';
Future<void> main() async {
final encoded = await File('image.b64').readAsString();
final flat = encoded.replaceAll(RegExp(r'\s+'), '');
final bytes = base64Decode(flat);
await File('image.png').writeAsBytes(bytes);
print('wrote ${bytes.length} bytes');
}
Dieses replaceAll leistet echte Arbeit. Textdateien sind voll mit Zeilenumbrüchen, oft das 76-Zeichen-MIME-Wrapping, und der strenge Dekodierer lehnt sie ab, also zuerst alle Umbrüche entfernen. Der Regex entfernt jedes Leerzeichen-Zeichen, genau das, was Sie für eine reine base64-Datei wollen. Wenn die Datei andere Annotationen enthalten könnte, wie einen PEM-Header, entfernen Sie diese explizit vor dem Dekodieren, und lassen Sie die Fehler des Dekodierers alles einfangen, was tatsächlich korrupt ist.
HTTP und APIs
Base64 in HTTP trägt zwei Kostüme. Erstens, API-Antworten: ein JSON-Feld, das Binärdaten als String trägt. Zweitens, der Authorization: Basic-Header, wo Zugangsdaten base64-kodiert werden, mit dem Standard-Alphabet und Padding:
import 'dart:convert';
import 'package:http/http.dart' as http;
Future<void> main() async {
final response = await http.get(
Uri.parse('https://httpbin.org/get?attachment=TWFuIGlzIGhlcmU%3D&name=man.txt'),
);
final payload = jsonDecode(response.body) as Map<String, dynamic>;
final args = payload['args'] as Map<String, dynamic>;
final bytes = base64Decode(args['attachment'] as String);
print('got ${bytes.length} bytes');
final credentials = utf8.decode(base64Decode('b2N0b2NhdDpzZWNyZXQ='));
print(credentials.split(':').first); // octocat
}
Das http-Paket ist der Standard-Client, ein dart pub add http entfernt. Für Basic-Auth dekodieren Sie den Teil nach dem Basic -Präfix. Zwei Fallen: Manche APIs senden URL-sichere oder nicht gepaddete Werte, wo die Doku base64 sagt, also laufen Sie den Wert zuerst durch base64Url.normalize, wenn das direkte Dekodieren wirft; und denken Sie daran, dass Basic-Auth Obskurierung ist, kein Schutz, deshalb gehört Basic-Auth nur auf TLS-Verbindungen.
E-Mail und MIME: Das Zeilenumbruch-Problem
E-Mail ist der älteste Base64-Kunde. MIME bricht Base64-Zeilen bei 76 Zeichen um - 76 plus CRLF passt bequem auf eine 80-spaltige Anzeige - und RFC 2045 sagt Dekodierern, die Zeilenumbrüche zu ignorieren. Darts Dekodierer nicht, absichtlich: er lehnt sie ab. Die Lösung ist, vor dem Dekodieren alle Umbrüche zu entfernen:
import 'dart:convert';
List<int> decodeMimeBody(String wrapped) {
final flat = wrapped.replaceAll(RegExp(r'\s+'), '');
return base64Decode(flat);
}
void main() {
const wrapped =
'SGVsbG8gZnJvbSBhbiBlbWFpbCBhdHRhY2htZW50LCB3cmFwcGVkIGF0IDc2IGNoYXJhY3RlcnMg'
'\r\n'
'dGhlIHdheSBNSU1FIHdhbnRzIGl0IHRvIGJlLCB3aXRoIENSTEYgYmV0d2VlbiB0aGUgbGluZXMu';
print(utf8.decode(decodeMimeBody(wrapped)));
}
Die Regel ist einfach: Leerzeichen entfernen, sonst nichts. Entfernen Sie keine anderen Zeichen in der Hoffnung, hilfreich zu sein; der Dekodierer ist der Validator, und Sie wollen, dass er sich über echte Korruption beschwert. Wenn Sie E-Mails im großen Maßstab verarbeiten, ist der Bereinigungsschritt billig, ein Regex-Pass, und er hält den Rest der Pipeline ehrlich.
Konfiguration und Umgebungsvariablen
Token und Zugangsdaten, die in textbasierten Konfigurationen leben, werden manchmal base64-kodiert, um sie auf einer Zeile zu halten und wie Token aussehen zu lassen. Der ehrliche Rahmen: Base64 ist Obskurierung, keine Verschlüsselung, also dient dieses Muster der Ordnung, nie der Geheimhaltung. Das Muster selbst ist trivial:
import 'dart:convert';
import 'package:dotenv/dotenv.dart';
Future<void> main() async {
final env = DotEnv()..load();
final encoded = env['API_TOKEN_B64'];
if (encoded == null) {
return;
}
final token = utf8.decode(base64Decode(encoded));
print('loaded a ${token.length}-char token');
}
Mit dem dotenv-Paket sitzt der Wert in einer .env-Datei als API_TOKEN_B64=c2stbGl2ZS1hYmMxMjM= und kommt nach dem Dekodieren als Klartext zurück. Die gleiche Form funktioniert mit String.fromEnvironment für Compile-Time-dart-define-Werte, mit einer Warnung: dart-define-Werte werden in die kompilierte Binary eingebacken, also gehört alles Geheime in eine Laufzeit-Konfiguration oder einen Secret-Manager, nicht dort.
Streams: Chunk für Chunk
Wenn der kodierte Text in Stücken ankommt - ein Netzwerk-Stream, eine große Datei, die in Blöcken gelesen wird - kommt der Dekodierer damit zurecht. Seine Zustandsmaschine trägt die unvollständige Gruppe über Chunk-Grenzen hinweg, also müssen die Chunks nicht auf vier-Zeichen-Grenzen ausgerichtet sein:
import 'dart:convert';
Future<void> main() async {
final incoming = Stream.fromIterable(['TWF', 'uaGVsbG8=']);
final text = await incoming
.transform(base64.decoder)
.map(utf8.decode)
.join();
print(text); // Manhello
}
Der transform-Aufruf verwendet den Dekodierer als Stream-Transformer; das erste Chunk, drei Zeichen, parkt seine Bits im Zustand des Dekodierers, und das zweite Chunk vervollständigt die Gruppe. Fehler tauchen als Stream-Fehler mit denselben FormatException-Details auf, und ein leerer Stream produziert einfach keine Ausgabe. Wenn Sie Sinks bevorzugen, gibt Ihnen base64.decoder.startChunkedConversion eine StringConversionSink, die an dieselbe Zustandsmaschine angeschlossen ist.
Big Data: Die Mathematik und der Speicher
Dekodieren schrumpft: Vier Zeichen werden zu drei Bytes, also ist die Ausgabe immer etwas unter drei Vierteln der Eingabelänge. Das heißt, die Ausgabegröße ist vor dem Dekodieren bekannt, was den Speicher planbar macht. Ein kleiner Helper berechnet sie aus dem String allein:
import 'dart:convert';
int decodedLength(String encoded) {
var padding = 0;
for (var i = encoded.length - 1; i >= 0 && padding < 2; i--) {
if (encoded.codeUnitAt(i) == 0x3d) {
padding++;
} else {
break;
}
}
return (encoded.length ~/ 4) * 3 - padding;
}
void main() {
print(decodedLength('QQ==')); // 1
print(decodedLength('QUI=')); // 2
print(decodedLength('QUJD')); // 3
}
Der eingebaute Dekodierer ist schnell: ein einzelner Pass über eine Nachschlagetabelle ohne Zeichen-für-Zeichen-String-Zuweisungen, also sind Strings mit mehreren Megabyte Routine. Wo base64 Sie etwas kostet, ist auf der Eingabe-Seite: der kodierte Text ist etwa 33 Prozent größer als die Daten, und es ist ein String, der auf der VM als UTF-16-Codeeinheiten lebt, etwa das Doppelte der Byte-Länge der kodierten Zeichen. Für Nutzlasten, die groß werden können, streamen Sie das Dekodieren, statt einen großen String zu verbinden.
Von der Kommandozeile
Darts VM macht einen sauberen CLI aus dem Dekodierer. Dieses kleine Tool liest ein Datei-Argument oder die Standardeingabe, entfernt alle Umbrüche und schreibt rohe Bytes auf die Standardausgabe:
import 'dart:convert';
import 'dart:io';
Future<void> main(List<String> args) async {
String encoded;
if (args.isNotEmpty) {
encoded = await File(args[0]).readAsString();
} else {
encoded = await stdin
.transform(utf8.decoder)
.join();
}
final flat = encoded.replaceAll(RegExp(r'\s+'), '');
stdout.add(base64Decode(flat));
await stdout.flush();
}
Speichern Sie es als bin/decode.dart und führen Sie dart run bin/decode.dart image.b64 > image.png aus, oder leiten Sie es um: cat token.b64 | dart run bin/decode.dart. Der stdout.add-Aufruf nimmt den Uint8List direkt, ohne Zwischen-String, genau so, wie Binärdaten durch eine Pipeline wandern sollten.
Fallen, die Dart-Entwickler beißen
- Die Padding-Mauer. JWT-Stil- und URL-Tool-Eingaben kommen oft ohne
=-Zeichen an, und der Dekodierer lehnt sie mitInvalid length, must be multiple of fourab. Laufen Sie unzuverlässige Eingaben zuerst durchbase64Url.normalize. - Die Leerzeichen-Falle. Textdateien, E-Mails und Copy-Paste führen alle Zeilenumbrüche ein, und der Dekodierer überspringt sie nie. Entfernen Sie vor dem Dekodieren alle Umbrüche mit
replaceAll(RegExp(r'\s+'), ''). - Alphabet-Vertrauen. Weil beide Alphabete überall dekodiert werden, bauen Sie keine Logik darauf, welcher Dekodierer einen String erzeugt hat. Der String ist der Vertrag, nicht die Einstellungen des Erzeugers.
- String.fromCharCodes ist kein Zeichensatz. Es liest UTF-16-Codeeinheiten, also macht es aus UTF-8-Text Mojibake. Verwenden Sie
utf8.decodeoder einen expliziten Zeichensatz. - Zwei verschiedene Fehler-Typen. Dekodier-Probleme sind
FormatExceptions; der Kodierer wirftArgumentErrorfür Werte außerhalb des Bereichs 0 bis 255. Fangen Sie sie separat ab, wenn Sie eine Grenze bauen. - Das Ergebnis ist fixe Länge.
Uint8Listkann nicht wachsen, also wirftbytes.add(1)einenUnsupportedError. Kopieren Sie mitList<int>.from(bytes), wenn Sie eine wachsende Liste brauchen. - Entescape'n Sie %3D nicht von Hand. Der Dekodierer liest prozent-escaped Padding nativ; ein vorzeitiges
replaceAll('%3D', '=')koppelt Ihren Code an ein Detail, das das SDK bereits beherrscht. - Ein JWT-Payload zu dekodieren heißt nicht, es zu verifizieren. Claims zu lesen und ihnen zu vertrauen ist ein Sicherheits-Bug, der auf einen bestimmten Nutzer wartet.
Best Practices, die kurze Liste
- Standardmäßig
base64Decode; greifen Sie nachnormalizenur an der Grenze, wo die Eingabe nicht vertrauenswürdig ist. - Seien Sie mit
utf8.decode(bytes)explizit über den Zeichensatz, auch wenn Sie UTF-8 annehmen. - Halten Sie Bytes als Bytes, bis Sie wissen, was sie sind; der
Uint8Listwandert sauber inFile.writeAsBytesund Freunde. - An Vertrauensgrenzen fangen Sie
FormatExceptionab und loggen Sie die Eingabe-Position, die Ihnen die Meldung gibt. - Streamen Sie alles, was mehrere Megabyte übersteigen könnte.
- Behandeln Sie base64 als Format, nicht als Schutz: Es versteckt nichts vor jemandem, der weiß, dass es base64 ist.
Eine kurze Geschichte von Base64 in Dart
Der Dekodierer, den Sie gerade getroffen haben, ist älter als Dart 3, Null Safety und das Flutter-Zeitalter. Die kurze Version:
- 18. November 2015, Dart 1.13: Base64 kommt in
dart:convertalsBASE64-Konstante plus dieBase64Codec,Base64EncoderundBase64Decoder-Klassen. Vor diesem Release hatte das SDK gar kein base64. - 28. Januar 2016, Dart 1.14:
Base64Decoder.convertbekommtstart- undend-Bereichsparameter, und dasselbe Release fügtdart:coredie Data-URI-Unterstützung hinzu, denUri.parse-Weg, auf den sich dieser Artikel stützt. - 26. April 2016, Dart 1.16: Das URL-sichere Alphabet kommt als
BASE64URLund derBase64Codec.urlSafe-Konstruktor dazu. - 7. August 2018, Dart 2.0: Die Konstanten werden zu kleingeschriebenen
base64undbase64Urlumbenannt, die Top-Level-base64Decode-Funktion und ihre Freunde kommen, das Dekodieren gibt einenUint8Liststatt einer wachsendenList<int>zurück, undBase64Codec.normalizekommt in die Familie und macht Validierung und Reparatur zu einem Ein-Aufruf-Schritt. - 2021, Dart 2.12: Null Safety kommt, und die ganze
dart:convert-Geschichte, base64 inklusive, wird null-sicher. - Heute, Dart 3.13: Die Klassen sind als
finalmarkiert, und das Verhalten, dem Sie oben begegnet sind, ist dieselbe strenge, beide Alphabete lesende, Prozent-erkennende Maschine, die seit 2015 läuft.
Die Strenge ist kein Zufall der Implementierung. Es ist der Dekodierer, der die Anweisung von RFC 4648 befolgt, dass Implementierungen Nicht-Alphabet-Zeichen ablehnen sollen, wobei die MIME-Stil-Nachgiebigkeit den Anwendungen überlassen bleibt, die sie brauchen, was in Dart einen Bereinigungsschritt vor dem Dekodieren bedeutet.
Fun-Fakten
- Der Dekodierer liest
%3Dals natives Padding. Reichen Sie ihm die rohe Nutzlast einer Data URI, Escape inklusive, und er dekodiert sie. Sehr wenige Sprach-Runtimes tun das ohne einen Vorverarbeitungsschritt. base64.decoderundbase64Url.decodersind buchstäblich dasselbe Objekt: Beide sind die kanonisierteconst Base64Decoder()-Instanz. Der "URL-sichere Dekodierer" ist der Standard-Dekodierer in einem anderen Kostüm.- Der ganze Dekodierer passt in eine Nachschlagetabelle mit 128 Einträgen, eine
Int8List, die zwischen Interpreter und AOT-kompiliertem Code geteilt wird, wobei+und-beide auf Alphabet-Slot 62 zeigen und/und_beide auf 63. - Darts base64 und seine Data-URI-Unterstützung landeten zwei Releases auseinander, in 1.13 und 1.14, und sie waren offensichtlich als Paar geplant: das eine zum Lesen des Formats, das andere zum direkten Auslesen aus einer URL.
- Der leere String dekodiert zu einem leeren
Uint8Listohne Fehler, und der leere String kodiert zum leeren String: base64 behandelt das Fehlen von Daten als vollkommen gültige Nachricht. - 2018, als Dart 2.0 seine Konstanten umbenannte, wurde
BASE64zubase64, Teil einer SDK-weiten Bewegung zu kleingeschriebenen Konstantennamen, derselben Welle, die Ihnenascii,jsonundutf8brachte.
Sie haben jetzt den kompletten Dekodierer: was er annimmt, was er ablehnt, wie man beschädigte Eingaben repariert und wie man ihm in JWTs, Data URIs, Dateien, Streams, E-Mails und der Shell begegnet. Die andere Richtung des Tauschs, Bytes zu nehmen und eines der beiden Alphabete zu produzieren, mit den Padding-Entscheidungen und der Größen-Mathematik, ist im Detail im Base64-Kodierungs-Leitfaden abgedeckt, der am Ende dieser Seite verlinkt ist.
Zuletzt aktualisiert: 2026-09-08
Verwandter Artikel: Base64-Kodierung in Dart: Ein vollständiger Leitfaden