Base64-Dekodierung in Dart: Ein vollständiger Leitfaden
Er taucht auf in einer API-Antwort, in einer URL oder kopiert in ein Support-Ticket: ein langer Lauf aus Buchstaben und Ziffern, hier und da ein +, /, - oder _, und vielleicht ein Paar =-Zeichen, die am Ende baumeln. Jemand nennt es Base64, und Sie brauchen das, was drinsteckt. Dieser Leitfaden ist das Dart-Rezept, um es zurückzubekommen. Kurze Orientierung, denn die Startseite geht auf das Format im Detail ein: Base64 schreibt jeweils drei Eingabe-Bytes um in vier Zeichen aus einem Alphabet von 64 Zeichen und hängt am Ende ein oder zwei =-Pads an, wenn das letzte Stück zu kurz ist. Dekodieren ist die schrumpfende Richtung dieses Tauschs: vier Zeichen gehen rein, drei Bytes kommen raus, und das Ergebnis braucht deshalb 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 Bibliothek dart:convert dabei, und die API ist seit Dart 2.0 im Jahr 2018 stabil. Ein Import liefert Ihnen einen schnellen, strengen Dekodierer, der sowohl das Standardalphabet als auch das URL-sichere Alphabet liest.
Eine ehrliche Grenze: Dies ist die Dekodierer-Seite der Geschichte. Sie lernen, was der Dekodierer akzeptiert und ablehnt, wie Padding funktioniert, wie man Bytes ohne Mojibake wieder in Text verwandelt, und wie man Base64 in JWTs, Data URIs, Dateien, Streams, E-Mails, Konfiguration und der Kommandozeile trifft. Die andere Richtung, Bytes in einen String zu packen, hat ihren eigenen Leitfaden, verlinkt am Ende von diesem.
Vier Türen zu einer strengen Maschine
Hier ist die gesamte öffentliche Oberfläche, die Sie nutzen werden, alles in dart:convert:
| Eingang | Was es ist | Greifen Sie danach, wenn |
|---|---|---|
base64Decode(source) |
Top-Level-Funktion, dekodiert in eine Uint8List |
Alltags-Dekodieren, fast immer diese |
base64.decode(source) |
Die decode-Methode des Codecs, identisches Verhalten | wenn Sie den Codec für fuse oder Stream-Transformationen benötigen |
base64Url.decode(source) |
Die decode-Methode des URL-sicheren Codecs | Die Eingabe als URL-sicher dokumentiert war (die Maschine ist dieselbe) |
base64Url.normalize(source) |
Validiert und repariert einen String, gibt ihn gepaddet zurück | Die Eingabe ohne Padding kommen kann, Alphabete mischt oder Prozent-Escapes nutzt |
Zwei Dinge fallen auf. Erstens führen alle vier Straßen zu demselben Dekodierer: einem strengen Zustandsautomaten mit einer Nachschlagetabelle. Zweitens ist die letzte Zeile gar kein Dekodierer. Es ist eine Reparaturstation, und sie wird sich bezahlt machen, sobald zum ersten Mal ein entkleideter JWT oder ein halb gereinigter Config-Wert auftaucht.
Ihr erstes Dekodieren
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 dazu, was gerade passiert ist. Erstens gibt der Einstiegspunkt Bytes zurück, nicht Text: base64Decode liefert eine Uint8List, und das ist bewusst so, denn der Payload könnte ein Satz, ein JPEG oder ein Hash sein, und keines davon sollte gleich behandelt werden, bevor man weiß, was man da hat. Zweitens ist der Sprung von Bytes zu Text ein separater, expliziter Schritt mit einer expliziten Kodierung, und genau in diesem Schritt wird aus "café" Mojibake, wenn man unachtsam ist. Drittens ist der leere String ein Wert erster Klasse: base64Decode('') gibt eine Liste mit der Länge null zurück, ohne Ausnahme und ohne Drama.
Was der Dekodierer akzeptiert und ablehnt
Darts Dekodierer ist von Design her strikt. RFC 4648 sagt, dass Implementierungen Eingaben mit Zeichen außerhalb des Alphabets verwerfen sollten, und Dart folgt dieser Lesart auf den Buchstaben: kein Überspringen von Leerzeichen, keine Ignorierung von Zeilenumbrüchen, keine zweite Chance. Wenn die Eingabe falsch ist, bekommen Sie eine FormatException, die die Eingabe zeigt und auf das exakte Zeichen weist. Hier ist das Verhalten bei den klassischen Übeltätern:
| 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 hinter 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 Zeichen-Zählung, und die Eingabe wird direkt unter dem Zeiger ausgegeben, so dass man einen beschädigten Payload schnell durch Halbieren eingrenzen kann. Eine angenehme Überraschung steckt in der Strenge: Der Dekodierer akzeptiert beide Alphabete. Ein - oder _ mitten in einem Standard-String ist in Ordnung, und ein + oder / in einem URL-sicheren String ist es auch. Die Alphabetwahl spielt nur eine Rolle, wenn Sie derjenige sind, der den Text erzeugt, nicht wenn Sie ihn lesen.
Padding: Das Unverhandelbare
Hier ist die Regel, die die meisten überrascht: Darts Dekodierer verlangt korrektes Padding. Die Eingabe muss eine Vielfache von vier Zeichen lang sein, und die =-Zeichen am Ende müssen in exakt der richtigen Anzahl vorhanden sein. Es gibt keinen nachsichtigen Modus, keinen Schalter zum Losen und keine Einstellung zum Ändern. Die Gründe sind solide: Dekodieren ohne Padding ist in Grenzfällen mehrdeutig, und der RFC warnt, dass zu großzügiges Dekodieren einen verdeckten Kanal öffnen kann, also ist die strenge Lesart die sichere. Was das in der Praxis bedeutet:
| Eingabe | Ergebnis |
|---|---|
'' |
leere 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 voll von Werten ohne Padding, ist der Reparatur-Schritt ein einziger Aufruf von normalize. Er validiert den String, wandelt URL-sichere Zeichen in das Standardalphabet um und ergänzt die fehlenden Pads:
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-Eigenbau. Wenn Base64 in einer Data URI auftaucht, percent-kodieren einige Tools das Padding und schreiben %3D statt =, denn ein nacktes = kann in der URL-Syntax "Parameter-Trenner" bedeuten. In den meisten Sprachen müssten Sie vorher die Escapes entfernen. Darts Dekodierer nicht: Seine Nachschlagetabelle behandelt %3D als native Schreibweise des Padding-Zeichens, also können Sie ihm den rohen Payload direkt geben:
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, also in der Endposition. Setzen Sie %3D dort hin, wo ein = abgelehnt würde, und es wird auf dieselbe Weise abgelehnt, und %25 scheitert an der Padding-Prüfung statt daran - % ist Darts natives Padding-Escape-Zeichen, also liest der Dekodierer es als escapetes = und lehnt die 2 mit Invalid padding character ab. In der Praxis bedeutet das: Ein ;base64,-Payload, das gerade aus dem Entwickler-Tool des Browsers kopiert wurde, dekodiert ohne jegliche Vorverarbeitung, ein kleiner, aber wirklich bequemer Trick.
URL-sicheres Base64
RFC 4648 definiert aus einem einzigen Grund ein zweites Alphabet: Das Standardalphabet 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 außerdem oft das Padding weg. Es ist das Alphabet der JWTs, Objekt-IDs, Shared-Links und alles, was in einer URL oder einem Dateinamen lebt.
Auf der Dekodierer-Seite gibt 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 ohne. 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 Stolperfallen zum Schluss. Machen Sie kein eigenes --zu-+-Ersetzen vor dem Dekodieren; es ist unnötig, und normalize erledigt die Alphabetumwandlung bereits bei Bedarf. Und gehen Sie nicht davon aus, dass ein URL-sicherer String ohne Padding ankommt: Manche Erzeuger behalten die Pads bei, 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 die Kodierung wählen, die sie wieder in einen String verwandelt, und diese Entscheidung treffen Sie explizit selbst. Die Standardannahme 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, was das richtige Verhalten ist, weit besser als stilles Mojibake. Wenn Sie wissen, dass die Daten veralteter Einzelbyte-Text sind, verwenden Sie die passende Kodierung:
| Kodierung | Verwenden Sie sie für | Dekodieren mit |
|---|---|---|
utf8 |
Moderner Text, JSON, alles im Web | utf8.decode(bytes) |
latin1 |
Alte westliche Einzelbyte-Daten | latin1.decode(bytes) |
ascii |
Einfacher 7-Bit-Text | ascii.decode(bytes) |
Eine Falle verdient ihre eigene Warnung: String.fromCharCodes ist kein Zeichensatz. Er liest Bytes als UTF-16-Codeeinheiten, also füttern Sie ihm die UTF-8-Bytes von Héllo, und er druckt Héllo mit geradem Gesicht. 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, Payload, Signatur. Base64 wird hier wegen Kompaktheit und URL-Sicherheit verwendet, nicht wegen Geheimhaltung. Jeder, der den Token hat, kann Header und Payload lesen, und das ist beabsichtigt. Die Signatur ist das, was Sie prüfen, mit dem gemeinsamen Secret oder dem öffentlichen Schlüssel 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
}
Achten Sie auf den Padding-Tanz: JWTs werden ohne Padding gebaut, also scheitert ein Teil beim direkten base64Decode immer dann, wenn seine Länge keine Vielfache von vier ist. (Im obigen Beispiel ist der Header zufällig 36 Zeichen lang und dekodiert direkt; die Payload hat 74 Zeichen und dekodiert nicht direkt.) Der normalize-Aufruf macht die Reparatur unabhängig von der Länge einheitlich. Zwei weitere Warnungen. Dekodieren ist nicht Verifizieren: Die Prüfung der Signatur und des exp-Claims ist ein separater, obligatorischer Schritt, üblicherweise 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, keine Funktion.
Data URIs: Dateien im URL-Kostüm
Eine Data URI, definiert durch RFC 2397, ist eine URL, deren Payload die Daten selbst sind: data:image/png;base64, gefolgt von den kodierten Bytes. Sie existieren, damit reine Textkanäle - HTML-Attribute, CSS-Regeln, JSON-Dokumente - Binärdaten ohne eine separate Datei tragen können. Base64 ist das Payload-Format der Wahl, denn die Alternative, die Prozent-Kodierung, wird für Binärdaten deutlich länger.
Und Dart kann sie nativ parsen: Die 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, das isBase64-Flag, den rohen Payload-Text und den dekodierten Inhalt als String oder als Bytes. Zwei Stolperfallen: Der deklarierte MIME-Typ kann lügen, also prüfen Sie in sicherheitsrelevantem Code die tatsächlichen Magic Bytes; und Data URIs sind für kleine Assets da, denn der gesamte Payload reist im Dokument mit, das ihn referenziert.
Dateien: Base64 auf der Platte
Base64-Dateien tauchen in Exportformaten, in Provisionierungs-Bundles und in jedem Text-only-Transport auf, der Binärdaten mitnehmen muss. Das Rezept lautet: Text lesen, glätten, 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 voller Zeilenumbrüche, oft das 76-Zeichen-MIME-Wrapping, und der strenge Dekodierer lehnt sie ab, also glätten Sie vorher. Der Regex entfernt jedes Whitespace-Zeichen, was für eine reine base64-Datei genau das Richtige ist. Wenn die Datei andere Annotationen enthalten könnte, wie einen PEM-Header, entfernen Sie diese vor dem Dekodieren explizit, und lassen Sie die Fehler des Dekodierers alles auffangen, was tatsächlich beschädigt 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, in dem Credentials mit dem Standardalphabet und Padding base64-kodiert werden:
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 Stolperfallen: Manche APIs senden URL-sichere oder ungepaddete Werte, obwohl die Doku base64 sagt, also wenn direktes Dekodieren wirft, laufen Sie den Wert zuerst durch base64Url.normalize; und denken Sie daran, dass Basic-Auth Verhüllung ist, kein Schutz, deshalb gehört sie 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 ein 80-Spalten-Display - und RFC 2045 sagt Dekodierern, die Zeilenumbrüche zu ignorieren. Darts Dekodierer macht das nicht, absichtlich: Er lehnt sie ab. Die Lösung ist, vorher zu glätten:
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: Whitespace entfernen, nichts anderes. Entfernen Sie keine anderen Zeichen in der Hoffnung, hilfreich zu sein; der Dekodierer ist der Validator, und Sie wollen, dass er sich über echte Beschädigung beschwert. Wenn Sie E-Mail in großem Maßstab verarbeiten, ist der Glättungsschritt billig, ein einziger Regex-Durchlauf, und er hält den Rest der Pipeline ehrlich.
Konfiguration und Umgebungsvariablen
Token und Credentials, die in textbasierten Konfigurationen leben, werden manchmal base64-kodiert, damit sie auf einer Zeile bleiben und wie Token aussehen. Die ehrliche Einordnung: Base64 ist Verhüllung, keine Verschlüsselung, also dient dieses Muster der Ordnung, niemals 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 als API_TOKEN_B64=c2stbGl2ZS1hYmMxMjM= in einer .env-Datei und kommt nach dem Dekodieren als Klartext zurück. Die gleiche Form funktioniert mit String.fromEnvironment für Compile-Zeit-dart-define-Werte, mit einer Warnung: dart-define-Werte sind in den kompilierten Binary eingebrannt, also gehört alles Geheime in die Laufzeit-Konfiguration oder einen Secret-Manager, nicht dort hinein.
Streams: Chunk für Chunk
Wenn der kodierte Text in Stückchen ankommt - ein Netzwerk-Stream, eine große Datei in Blöcken gelesen - kommt der Dekodierer klar. Sein Zustandsautomat trägt die teilweise Gruppe über Chunk-Grenzen hinweg, so dass die Chunks nicht an Vier-Zeichen-Grenzen ausgerichtet sein müssen:
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 nutzt den Dekodierer als Stream-Transformer; der erste Chunk, drei Zeichen, parkt seine Bits im Zustand des Dekodierers, und der zweite Chunk vollendet die Gruppe. Fehler treten als Stream-Fehler mit den gleichen FormatException-Details auf, und ein leerer Stream produziert einfach keine Ausgabe. Wenn Sie Sinks bevorzugen, gibt base64.decoder.startChunkedConversion Ihnen einen StringConversionSink, der an denselben Zustandsautomaten angeschlossen ist.
Big Data: Die Mathematik und der Speicher
Dekodieren schrumpft: vier Zeichen werden drei Bytes, also ist die Ausgabe immer ein kleines Stück unter drei Vierteln der Eingabelänge. Das bedeutet: Die Ausgabegröße ist vor dem Dekodieren berechenbar, was den Speicher vorhersehbar 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 Durchlauf über eine Nachschlagetabelle ohne String-Allokationen pro Zeichen, so dass mehrmegabytegroße Strings Routine sind. Wo base64 Sie kostet, ist auf der Eingabe-Seite: der kodierte Text ist etwa 33 Prozent größer als die Daten, und er ist ein String, der auf der VM als UTF-16-Codeeinheiten lebt, ungefähr das Doppelte der Byte-Länge der kodierten Zeichen. Für Payloads, die groß werden können, streamen Sie das Dekodieren, statt einen großen String zu joinen.
Von der Kommandozeile
Darts VM macht aus dem Dekodierer eine saubere CLI. Dieses kleine Werkzeug liest ein Datei-Argument oder die Standard-Eingabe, glättet Whitespace und schreibt rohe Bytes auf die Standard-Ausgabe:
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 pipen Sie es: cat token.b64 | dart run bin/decode.dart. Der stdout.add-Aufruf nimmt die Uint8List direkt, ohne Zwischen-String, was genau das Richtige ist, wenn Binärdaten durch eine Pipeline fließen sollen.
Fallen, die Dart-Entwickler beißen
- Die Padding-Mauer. Eingaben im JWT-Stil oder von URL-Tools kommen oft ohne
=-Zeichen, und der Dekodierer lehnt sie mitInvalid length, must be multiple of fourab. Lassen Sie unzuverlässige Eingabe zuerst durchbase64Url.normalizelaufen. - Die Whitespace-Falle. Textdateien, E-Mails und Copy-Paste bringen alle Zeilenumbrüche mit, und der Dekodierer überspringt sie nie. Glätten Sie mit
replaceAll(RegExp(r'\s+'), ''), bevor Sie dekodieren. - Alphabet-Überzeugung. Weil beide Alphabete überall dekodiert werden, bauen Sie keine Logik darauf, welcher Dekodierer einen String produziert hat. Der String ist der Vertrag, nicht die Einstellungen des Erzeugers.
- String.fromCharCodes ist kein Zeichensatz. Er liest UTF-16-Codeeinheiten, also verwandelt er UTF-8-Text in Mojibake. Verwenden Sie
utf8.decodeoder eine explizite Kodierung. - Zwei verschiedene Fehler-Typen. Dekodier-Probleme sind
FormatExceptions; der Kodierer wirftArgumentErrorfür Werte außerhalb des Bereichs von 0 bis 255. Fangen Sie sie getrennt, wenn Sie eine Grenze bauen. - Das Ergebnis ist festlängig. Eine
Uint8Listkann nicht wachsen, also wirftbytes.add(1)einenUnsupportedError. Kopieren Sie mitList<int>.from(bytes), wenn Sie eine wachsende Liste brauchen. - Wandeln Sie %3D nicht manuell zurück. Der Dekodierer liest prozent-escapetes Padding nativ; ein vorzeitiges
replaceAll('%3D', '=')koppelt Ihren Code an ein Detail, das das SDK bereits beherrscht. - Das Dekodieren eines JWT-Payloads ist nicht die Verifizierung. Die Claims zu lesen und ihnen zu vertrauen, ist ein Sicherheitsbug, der auf einen entschlossenen Nutzer wartet.
Best Practices, Kurzliste
- Standardmäßig
base64Decodeverwenden; zunormalizenur an der Grenze greifen, wo die Eingabe unzuverlässig ist. - Seien Sie mit
utf8.decode(bytes)explizit über den Zeichensatz, auch wenn Sie UTF-8 annehmen. - Behalten Sie Bytes als Bytes, bis Sie wissen, was sie sind; die
Uint8Listwandert sauber inFile.writeAsBytesund ähnliche. - Fangen Sie an Vertrauensgrenzen
FormatExceptionab und loggen Sie die Eingabe-Position, die die Meldung gibt. - Streamen Sie alles, was mehrere Megabyte überschreiten könnte.
- Betrachten Sie base64 als Format, nicht als Schutz: Es verbirgt nichts vor jemandem, der weiß, dass es base64 ist.
Eine kurze Geschichte von Base64 in Dart
Der Dekodierer, den Sie gerade kennengelernt haben, ist älter als Dart 3, Null-Safety und die Flutter-Ära. Die Kurzversion:
- 18. November 2015, Dart 1.13: Base64 kommt als
BASE64-Konstante indart:convertdazu, zusammen mit den KlassenBase64Codec,Base64EncoderundBase64Decoder. Vor diesem Release hatte das SDK gar kein base64. - 28. Januar 2016, Dart 1.14:
Base64Decoder.convertbekommt die Bereichs-Parameterstartundend, und dasselbe Release fügtdart:coredie Data-URI-Unterstützung hinzu, denUri.parse-Weg, auf den dieser Artikel setzt. - 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 in Kleinschreibung zu
base64undbase64Urlumbenannt, die Top-Level-base64Decode-Funktionen und ihre Kollegen kommen, Dekodieren liefert eineUint8Liststatt einer wachsendenList<int>, undBase64Codec.normalizestößt zur Familie dazu und macht aus Validierung und Reparatur einen 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, das Sie oben angetroffen haben, ist dieselbe strenge, prozentbewusste Maschine, die beide Alphabete liest und seit 2015 läuft.
Die Strenge ist kein Zufall der Implementierung. Es ist der Dekodierer, der der Anweisung von RFC 4648 folgt, dass Implementierungen Zeichen außerhalb des Alphabets verwerfen sollen, wobei die MIME-artige Nachsicht den Anwendungen überlassen bleibt, die sie brauchen, was in Dart einen Glättungsschritt vor dem Dekodieren bedeutet.
Spielerische Fakten
- Der Dekodierer liest
%3Dals natives Padding. Geben Sie ihm den rohen Payload einer Data URI, Escape inklusive, und er dekodiert. Sehr wenige Laufzeitumgebungen von Programmiersprachen schaffen das ohne Vorverarbeitung. base64.decoderundbase64Url.decodersind buchstäblich dasselbe Objekt: Beide sind die kanonisierteconst Base64Decoder()-Instanz. Der "URL-sichere Dekodierer" ist der Standard-Dekodierer in anderem Kostüm.- Der ganze Dekodierer passt in eine 128-einträgige Nachschlagetabelle, eine
Int8List, die sich Interpreter und AOT-kompilierter Code teilen, wobei sowohl+als auch-auf Alphabet-Slot 62 zeigen und sowohl/als auch_auf 63. - Darts base64 und seine Data-URI-Unterstützung sind zwei Releases auseinander gelandet, in 1.13 und 1.14, und sie wurden offensichtlich als Paar geplant: einer, um das Format zu lesen, einer, um es direkt aus einer URL zu lesen.
- Der leere String dekodiert zu einer 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
BASE64zubase64im Zuge einer SDK-weiten Bewegung zu kleingeschriebenen Konstantennamen, derselben Welle, die Ihnenascii,jsonundutf8brachte.
Sie haben jetzt den gesamten Dekodierer: was er akzeptiert, was er ablehnt, wie man beschädigte Eingabe repariert, und wie man ihn in JWTs, Data URIs, Dateien, Streams, E-Mails und der Shell trifft. 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 behandelt, der am Ende dieser Seite verlinkt ist.
Zuletzt aktualisiert: 2026-09-08
Verwandter Artikel: Base64-Kodierung in Dart: Ein vollständiger Leitfaden