Dart での Base64 デコード:完全ガイド
API のレスポンスの中、URL の中、あるいはサポートチケットに貼り付けられた形でやってくるのが、この値です:文字と数字の長い列で、ときどき + や /、-、_ が混ざり、もしかすると = の2文字が末尾にぶら下がっている。誰かが「これは Base64」と呼び、あなたは中身が欲しい。このガイドは、それをもとに戻すための Dart レシピです。方向感覚を少しだけ:フォーマットの詳しい解説はホームページにあるので、ここではざっと。Base64 は入力の3バイトを、64文字のアルファベットから選んだ4文字に書き換え、最後のチャンクが短いときは末尾に = パッドを1つか2つつけます。デコードは、その取引の縮む方向です:4文字が入り、3バイトが出てくるので、結果は常に、入力より約4分の1少ないスペースで済みます。
嬉しい知らせです:インストールするものは何もありません。Base64 は2015年の Dart 1.13 から dart:convert ライブラリに同梱されており、API は2018年の Dart 2.0 から安定しています。import を1つ加えるだけで、速くて厳格なデコーダーが手に入ります。標準アルファベットも URL 安全アルファベットも読みます。
正直な境界線を1つだけ:これは物語のデコーダー側です。デコーダーが受け入れるものと拒否するもの、パディングの仕組み、バイトを文字化けなく文字に戻す方法、そして JWT、data URI、ファイル、ストリーム、メール、設定、コマンドラインで Base64 に出会う方法を学びます。もう一方の方向、つまりバイトを文字列に詰める仕事には専用のガイドがあり、この記事の最後にリンクしています。
厳しい1台の機械への4つの扉
使う公開インタフェースの全体像はこの通りです。すべて dart:convert にあります:
| エントリ | 内容 | いつ使うか |
|---|---|---|
base64Decode(source) |
トップレベル関数。Uint8List にデコードする |
日常的なデコード。ほぼ常にこれ |
base64.decode(source) |
コーデックのデコードメソッド。挙動は同一 | fuse やストリーム変換にコーデックが欲しいとき |
base64Url.decode(source) |
URL 安全コーデックのデコードメソッド | 入力が URL 安全として文書化されているとき(機械は同じ) |
base64Url.normalize(source) |
文字列を検証して修復し、パディング済みで返す | 入力にパディングがない、アルファベットが混在、パーセントエスケープを使っている可能性があるとき |
注目すべき点は2つです。第一に、4つの道はすべて同じデコーダーに続きます:1つの参照テーブルを持つ、1台の厳しい状態機械です。第二に、最後の行はそもそもデコーダーではありません。あれは修理ステーションで、パディングが剥がれた JWT や、半分しか片付いていない設定値が初めて現れたときに、ちゃんとその価値を返してくれます。
最初のデコード
デコード生活の9割は5行で収まります。仕事の全体像を示す最小の例がこれです:
import 'dart:convert';
void main() {
final bytes = base64Decode('TWFu');
final text = utf8.decode(bytes);
print(text); // Man
}
さっき起きたことについて3文です。第一に、入口が返すのはバイトで、テキストではありません:base64Decode は Uint8List を返しますが、それは意図的なことです。ペイロードは文章かもしれない、JPEG かもしれない、ハッシュかもしれないからで、中身が何かわからない時点で、どれも同じ扱いにしてはいけないのです。第二に、バイトからテキストへの移り方は、明示的なエンコーディングを持つ独立した1ステップです。「café」が文字化けになるのは、気をつけなければいけないまさにこのステップです。第三に、空文字列も一級市民です:base64Decode('') は、例外も騒ぎもなしに長さ0のリストを返します。
デコーダーが受け入れ、拒否するもの
Dart のデコーダーは、設計上厳格です。RFC 4648 は、実装はアルファベット外の文字を含む入力を拒否すべきだと述べており、Dart はその読み方を文字通りに守ります:スペースのスキップも、改行の無視も、第2のチャンスも許さない。入力が悪いと、入力を表示して、問題の正確な文字を指し示す FormatException が返ってきます。いつもの厄介者たちに対する挙動はこうです:
| 入力 | 何が悪い | 正確なエラー |
|---|---|---|
'SGVs bG8s' |
スペースが忍び込んだ | FormatException: Invalid character (at character 5) |
'SGVs\nbG8s' |
改行が忍び込んだ | FormatException: Invalid character (at character 5) |
'SGVs$bG8s' |
ドル記号はアルファベットにない | FormatException: Invalid character (at character 5) |
'Zm8' |
パディングがまったくない | FormatException: Invalid length, must be multiple of four (at character 4) |
'Zm8==' |
1つでいい場所にパッドが2つ | FormatException: Invalid padding character (at character 5) |
'Zm=8' |
データの真ん中にパディング | FormatException: Invalid encoding before padding (at character 3) |
'Zm8=xx' |
パッドの後にゴミ | FormatException: Invalid padding character (at character 5) |
'Zé' |
ASCII 以外の文字 | FormatException: Invalid character (at character 2) |
メッセージの中の位置は1始まりの文字数で、入力はカーソルの真下に表示されるので、破損したペイロードのバイセクションは素早くできます。厳格さの中に隠れた嬉しい驚きが1つあります:デコーダーは両方のアルファベットを受け入れます。標準文字列の真ん中に - や _ があるのは平気ですし、URL 安全文字列に + や / があるのも平気です。アルファベットの選択が重要なのは、テキストを生成する側にいるときだけで、読む側にいるときには関係ありません。
パディング:交渉の余地なし
最も多くの人を驚かせるルールがここにあります:Dart のデコーダーは正しいパディングを要求するのです。入力は4文字の倍数の長さでなければならず、末尾の = はちょうど正しい個数だけ存在しなければなりません。寛容モードも、それを緩めるフラグも、変えられる設定もありません。理由は妥当です:パディングなしのデコードは隅のケースで曖昧になり、RFC も寛容なデコードは隠れ通路を開きうることを警告しているため、厳しい読み方が安全な方です。実践で何を意味するか:
| 入力 | 結果 |
|---|---|
'' |
空の Uint8List、エラーなし |
'QQ==' |
1バイト:A |
'QUI=' |
2バイト:AB |
'QUJD' |
3バイト:ABC |
'Zm8' |
FormatException:長さが不正 |
'Zm8==' |
FormatException:パディング文字が不正 |
入力がパディングを剥がすシステムから来た場合(JWT にはパディングなしの値が満ち溢れています)、修復ステップは normalize の1コールです。文字列を検証し、URL 安全の文字を標準アルファベットに変換し、足りないパッドを追加します:
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
}
パーセント記号の驚き
これは Dart 独自の話です。Base64 が data URI に現れるとき、一部のツールはパディングをパーセントエンコーディングして、= の代わりに %3D と書きます。素の = は URL 構文では「パラメータの区切り」を意味しうるからです。多くの言語なら、まずエスケープを解除してからだと欲しがるでしょう。Dart のデコーダーはそうしません:その参照テーブルは %3D をパディング文字のネイティブな表記として扱うので、生のペイロードをそのまま渡すことができます:
import 'dart:convert';
void main() {
final fromDataUri = 'SGVsbG8%3D';
final bytes = base64Decode(fromDataUri);
print(utf8.decode(bytes)); // Hello
}
エスケープが受け入れられるのは、パディングが合法な場所、つまり末尾の位置だけです。= が拒否される場所に %3D を置けば、同じ形で拒否されます。また %25 はパディングチェックで失敗します - % は Dart のネイティブなパディングエスケープ文字なので、デコーダーはそれをエスケープされた = として読み、2 を Invalid padding character で拒否するのです。実用上これは、ブラウザの開発者ツールからそのままコピーした ;base64, ペイロードは、前処理なしでデコードできるということを意味します。小さな、しかし本当に便利な裏技です。
URL 安全な Base64
RFC 4648 が2番目のアルファベットを定義した理由が1つあります:標準のアルファベットには、URL 構文と衝突する3文字、+、/、= が含まれているからです。RFC では base64url と呼ばれる URL 安全アルファベットは、+ を - に、/ を _ に置き換え、しばしばパディングも落とします。JWT、オブジェクト ID、共有リンク、URL やファイル名の内部に住むあらゆるもののアルファベットです。
デコード側では、Dart は1つの答えをくれます:両方のアルファベットは同じ機械が読むのです。base64Decode と base64Url.decode は同じデコーダーの2つの名前なので、本当の仕事はパディングだけです。URL 安全の生成者は、しばしばパディングなしで出荷するからです。まさにそれのために normalize があるのです:
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
}
最後に残してゆく落とし穴が2つあります。デコードの前に、手で - から + への置換を書くのはやめましょう。不要ですし、normalize は必要に応じてアルファベット変換をすでにやっています。また、URL 安全の文字列はパディングなしでやって来るとも決めつけないでください:一部の生成者はパッドを残していますし、デコーダーはパディングが正しければどちらの形も受け入れます。
バイトからテキストへ:エンコーディングの選択
Base64 デコードがあなたに渡すのはバイトです。そのバイトがテキストであるなら、それを String に戻すエンコーディングを選ぶ必要があります。その選択は、あなたが明示的に行うものです。モダンなシステムのデフォルトの前提は UTF-8 で、utf8.decode がその主力です:
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
}
バイトが有効な UTF-8 でない場合、utf8.decode は FormatException を投げます。これは正しい挙動で、無言の文字化けよりはるかに良いことです。データがレガシーな1バイト文字のテキストだと分かっているなら、それに合ったエンコーディングを使いましょう:
| エンコーディング | 使うもの | デコードに使うもの |
|---|---|---|
utf8 |
モダンなテキスト、JSON、Web 上のあらゆるもの | utf8.decode(bytes) |
latin1 |
レガシーな西系の1バイトデータ | latin1.decode(bytes) |
ascii |
素の7ビットテキスト | ascii.decode(bytes) |
1つの罠は、専用の警告に値します:String.fromCharCodes は文字符号化方式ではありません。これはバイトを UTF-16 のコードユニットとして読むので、Héllo の UTF-8 バイトを与えると、平気な顔をして Héllo を表示します。出力にその文字化けのパターンが見えたら、直し方はほぼ常に utf8.decode です。
JWT:トークンを読む
JSON Web Token は、ドットで結ばれた3つの base64url 部分から成ります:ヘッダー、ペイロード、署名。ここでは Base64 が使われている理由は秘密ではなく、コンパクトさと URL 安全性のためです。トークンを持つ者は誰でもヘッダーとペイロードを読め、それは設計どおりです。あなたが検証するのは署名で、共有シークレットか発行元の公開鍵を使います。Dart で読める部分をデコードするのは数行で済みます:
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
}
パディングのダンスに注目してください:JWT はパディングなしで組まれているので、部分の長さが4の倍数でないたびに、直接の base64Decode は失敗します。(先の例ではヘッダーがたまたま36文字で直接デコードできましたが、ペイロードは74文字なのでできません。) normalize の呼び出しは、長さに関係なく修復を均一にします。警告はもう2つ。デコードは検証ではありません:署名と exp クレームの確認は別の、必須のステップで、HMAC アルゴリズムには通常 crypto パッケージを使います。また、alg: none と主張するトークンには疑いの目を向けましょう:それを受け入れるパーサーは脆弱性であって、機能ではありません。
Data URI: URL の仮装をしたファイル
RFC 2397 で定義される data URI は、ペイロードそのものがデータである URL です:data:image/png;base64, の後にエンコードされたバイトが続きます。これがあるのは、テキスト専用のチャネル - HTML 属性、CSS ルール、JSON ドキュメント - が、別のファイルなしにバイナリを運べるようにするためです。Base64 が選ばれるペイロード形式なのは、選択肢であるパーセントエンコーディングはバイナリデータでははるかに長くなるからです。
しかも Dart はこれをネイティブでパースできます:data URI のサポートは2016年から dart:core にあり、URI ライブラリは不要です:
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!
}
UriData オブジェクトは、MIME タイプ、isBase64 フラグ、生のペイロード文字列、そして文字列またはバイトとしてのデコード済みコンテンツをくれます。落とし穴は2つ:宣言された MIME タイプは嘘をつきうるため、セキュリティ的に大切なコードでは実際のマジックバイトを確認してください。また、data URI は小さなアセットのためにあります。ペイロード全体が、それを参照するドキュメントの内部に乗っているからです。
ファイル:ディスク上の Base64
Base64 ファイルは、エクスポート形式、プロビジョニングバンドル、バイナリを運ぶ必要があるテキスト専用転送のあらゆる場面に現れます。レシピはこうです:テキストを読み、平坦化し、デコードし、バイトを書き出す:
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');
}
その replaceAll は本物の仕事をしています。テキストファイルは改行で満ちていて、よくあるのは76文字ごとの MIME ラッピングです。厳しいデコーダーはそれを拒否するので、先に平坦化します。この正規表現はすべての空白文字を削除しますが、純粋な base64 ファイルに対してはまさに望ましいことです。ファイルに PEM ヘッダーのような他の注記が含まれる可能性があるなら、デコード前にそれらを明示的に取り除き、本当に破損しているものはデコーダーのエラーに捕まらせましょう。
HTTP と API
HTTP における Base64 は2つの仮装を着ています。1つ目は API レスポンスです:バイナリを文字列として運ぶ JSON フィールド。2つ目は Authorization: Basic ヘッダーで、ここでは認証情報が標準アルファベットとパディングで base64 エンコードされます:
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
}
http パッケージが標準のクライアントで、dart pub add http 1発の距離にあります。Basic 認証では、Basic プレフィックスの後ろの部分をデコードします。落とし穴は2つ:ドキュメントが base64 と言っているのに、一部の API は URL 安全またはパディングなしの値を送ります。そのため直接のデコードが例外を投げる場合は、まずその値を base64Url.normalize に通してください。また、Basic 認証は保護ではなく曖昧化にすぎないことも覚えておきましょう。それが TLS 接続の上でのみ存在してよい理由です。
メールと MIME:改行の問題
メールは最も古い base64 の顧客です。MIME は base64 の行を76文字でラップします - 76文字に CRLF を加えても、80列のディスプレイに余裕で収まるからです - そして RFC 2045 はデコーダーに改行を無視すべきだと述べています。Dart のデコーダーは意図的にそうしませんが、それらを拒否します。直し方は、デコード前に平坦化することです:
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)));
}
ルールは簡単です:空白を取り除く。それだけで十分です。役に立ちたいと願って他の文字まで取り除いてはいけません。デコーダーこそが検証者であり、本当に破損した部分には文句を言わせておきたいのです。大規模にメールを処理しているなら、平坦化ステップは安価です。正規表現を1回通すだけですし、パイプラインの残り部分を正直に保ってくれます。
設定と環境変数
テキストベースの設定の中で暮らすトークンや認証情報は、1行に収めて、トークンらしく見せるために、ときどき base64 エンコードされます。正直な枠組み:base64 は暗号化ではなく曖昧化なので、このパターンは整理のために使われ、決して秘密のためには使われません。パターン自体は自明です:
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');
}
dotenv パッケージを使うと、値は .env ファイルに API_TOKEN_B64=c2stbGl2ZS1hYmMxMjM= の形で座し、デコードの後で素のテキストとして戻ってきます。同じ形状は、コンパイル時の dart-define 値に対して String.fromEnvironment でも動きますが、警告が1つあります:dart-define の値はコンパイルされたバイナリに焼き込まれるので、秘密なものはランタイムの設定やシークレットマネージャに属し、そこには属しません。
ストリーム:チャンクごとに
エンコードされたテキストが断片で到着するとき - ネットワークストリーム、ブロックごとに読む大きなファイル - デコーダーは対処できます。その状態機械は、不完全なグループをチャンクの境界をまたいで保持するので、チャンクは4文字の境界に揃える必要がありません:
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
}
transform の呼び出しは、デコーダーをストリーム変換器として使います。最初のチャンク、3文字分は、ビットをデコーダーの状態に停め、2番目のチャンクがグループを完成させます。エラーは同じ FormatException の詳細を伴うストリームエラーとして現れ、空のストリームは単に出力を生成しません。sink を好むなら、base64.decoder.startChunkedConversion が、同じ状態機械に接続された StringConversionSink をくれます。
ビッグデータ:計算とメモリ
デコードは縮みます:4文字が3バイトになるので、出力は常に、入力長の4分の3をわずかに下回ります。つまり、出力サイズはデコードの前に分かるということで、メモリが予測可能になります。小さなヘルパーが、文字列だけでそれを計算します:
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
}
組み込みのデコーダーは速いです:参照テーブルを1パスで回し、文字ごとの文字列割り当てもないので、数メガバイトの文字列も日常茶飯事です。base64 があなたにコストを払わせるのは入力側です:エンコードされたテキストはデータより約33%大きく、しかもそれは文字列であり、VM 上では UTF-16 のコードユニットとして暮らします。エンコードされた文字のバイト長の約2倍です。大きく成長しうるペイロードに対しては、1つの大きな文字列に結合する代わりに、ストリームでデコードしてください。
コマンドラインから
Dart の VM は、デコーダーからきれいな CLI を作り出します。この小さなツールはファイル引数または標準入力を読み、空白を平坦化し、生のバイトを標準出力に書き出します:
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();
}
それを bin/decode.dart として保存し、dart run bin/decode.dart image.b64 > image.png を実行します。パイプしてもいいです:cat token.b64 | dart run bin/decode.dart。stdout.add の呼び出しは Uint8List を直接受け取り、中間文字列は不要です。バイナリがパイプラインを通るとき、まさにこうあるべきなのです。
Dart 開発者を噛みつく落とし穴
- パディングの壁。JWT 風や URL ツールの入力には、
=記号なしで到着するものが多いです。デコーダーはInvalid length, must be multiple of fourでそれを拒否します。信頼できない入力は、まずbase64Url.normalizeに通してください。 - 空白の罠。テキストファイル、メール、コピー&ペースト、どれも改行を持ち込みます。デコーダーがそれをスキップすることはありません。デコード前に
replaceAll(RegExp(r'\s+'), '')で平坦化してください。 - アルファベットへの過信。両方のアルファベットはどこでもデコードできるので、「どのデコーダーがその文字列を作ったか」を前提にロジックを組まないでください。契約であるのは文字列で、生成側の設定ではありません。
- String.fromCharCodes は文字符号化方式ではない。これは UTF-16 のコードユニットを読むので、UTF-8 のテキストを文字化けにしてしまいます。
utf8.decodeか明示的なエンコーディングを使いましょう。 - 2つの異なるエラー種別。デコードの問題は
FormatExceptionですが、エンコーダーは 0 から 255 の範囲外の値に対してArgumentErrorを投げます。信頼境界を組んでいるなら、別々にキャッチしてください。 - 結果は固定長です。
Uint8Listは成長しないので、bytes.add(1)はUnsupportedErrorを投げます。成長可能なリストが必要なときは、List<int>.from(bytes)でコピーしてください。 - %3D を手動でアンエスケープしない。デコーダーはパーセントエスケープされたパディングをネイティブで読みます。早計な
replaceAll('%3D', '=')は、あなたのコードを SDK がすでに所有する詳細に結びつけてしまいます。 - JWT のペイロードをデコードすることは、それを検証することではない。クレームを読んでそれを信頼するのは、決意あるユーザーを待っているだけのセキュリティバグです。
ベストプラクティス、短いリスト
- デフォルトは
base64Decode。normalizeに手を伸ばすのは、入力が信頼できない境界だけです。 - UTF-8 を前提としていても、
utf8.decode(bytes)でエンコーディングは明示にしましょう。 - 中身が何かわかるまで、バイトはバイトのままにしておきましょう。
Uint8ListはFile.writeAsBytesや仲間にきれいに渡っていきます。 - 信頼境界では
FormatExceptionをキャッチし、メッセージが教えてくれる入力の位置をログに記録します。 - 数メガバイトを超えうるものは、すべてストリームにします。
- base64 は保護ではなく形式として扱います:それが base64 だと知っている者には、何も隠せていないからです。
Dart における Base64 の短い歴史
あなたが出会ったばかりのこのデコーダーは、Dart 3 も、null safety も、Flutter 時代も、それよりも長くここにあります。短いバージョン:
- 2015年11月18日、Dart 1.13:Base64 は
BASE64定数とBase64Codec、Base64Encoder、Base64Decoderクラスとしてdart:convertにやってきた。このリリースの前は、SDK に base64 はまったくなかった。 - 2016年1月28日、Dart 1.14:
Base64Decoder.convertがstartとendの範囲パラメータを獲得し、同じリリースでdart:coreに data URI のサポートが加わった。この記事が頼りにしているUri.parseの道だ。 - 2016年4月26日、Dart 1.16:URL 安全アルファベットが
BASE64URLとBase64Codec.urlSafeコンストラクターとして仲間入り。 - 2018年8月7日、Dart 2.0:定数が小文字の
base64とbase64Urlに改名され、トップレベルのbase64Decodeと仲間たちがやってきた。デコードは成長可能なList<int>の代わりにUint8Listを返すようになり、Base64Codec.normalizeが家族に加わって、検証と修復を1コールのステップに変えた。 - 2021年、Dart 2.12:null safety がリリースされ、base64 を含む
dart:convertの物語全体が null 安全になった。 - 今日、Dart 3.13:クラスは
finalとマークされ、上で出会った挙動は、2015年から走り続けているのと同じ、厳しい、両アルファベット対応、パーセントを認識する機械です。
厳格さは実装の偶然ではありません。これはデコーダーが RFC 4648 の指針、すなわち実装はアルファベット外の文字を拒否すべきだという指示に従っているだけで、MIME 風の寛容さはそれを必要とするアプリケーションに残されています。Dart では、それはデコード前の平坦化ステップを意味します。
トリビア
- デコーダーは
%3Dをネイティブなパディングとして読む。data URI の生のペイロードを、エスケープごと渡せば、そのままデコードしてくれる。前処理ステップなしでそれをやる言語ランタイムは、本当に少ない。 base64.decoderとbase64Url.decoderは、文字どおり同じオブジェクトだ:両方とも正規化されたconst Base64Decoder()インスタンスである。「URL 安全デコーダー」は、別の仮装をした標準デコーダーにすぎない。- デコーダー全体は128エントリの参照テーブル1つに収まる。
Int8Listで、インタープリタと AOT コンパイルコードの両方で共有される。+と-はどちらもアルファベットのスロット 62 を指し、/と_はどちらもスロット 63 を指す。 - Dart の base64 とその data URI サポートは、1.13 と 1.14 の2つのリリースに分かれてやってきた。明らかにペアとして計画されていた:一方は形式を読むため、もう一方は URL からそのまま読むため。
- 空文字列はエラーなしで空の
Uint8Listにデコードされ、空文字列は空文字列にエンコードされる:base64 はデータの不在を、完全に正当なメッセージとして扱うのだ。 - 2018年、Dart 2.0 が定数を改名したとき、
BASE64はbase64になった。これは SDK 全体の定数名の小文字化という動きの一環で、ascii、json、utf8をくれたのと同じ波だ。
これで完成したデコーダーを手に入れた:それが受け入れるもの、拒否するもの、傷んだ入力を修復する方法、そして JWT、data URI、ファイル、ストリーム、メール、シェルでそれに出会う方法。取引のもう一方の方向、つまりバイトを取り、2つのアルファベットのどちらかを生成する仕事には、パディングの決断とサイズ計算がついてきます。それは Base64 エンコーディングガイドで詳しく扱われており、このページの最後にリンクしています。
最終更新: 2026-10-09