Rust での Base64 エンコード:完全ガイド
あなたはこれから、テキスト専用のお門をいくつかのバイトに通過させようとしています。入場料は、出発点よりおよそ3分の1長い、文字と数字、プラス記号とスラッシュの列です。Base64へようこそ、インターネットの料金所です。このサイトのホームページではフォーマットを隅々まで詳しく説明しているので、ここで繰り返すのは形だけです:Base64 は入力の3バイトを、64記号のアルファベットから選んだ4文字として書き、末尾の = 1つか2つが読者に対して本当のデータの終わりを示します。この4対3の取引がフォーマット全体の経済であり、このガイドが扱うのは Rust でそれをしっかり行うことです。
最初に知っておくべきことは、Rust の標準ライブラリが代わりにやってくれない、ということです。std のどこかに隠れた base64_encode() も、あなたの考えを変える use std::... もありません。エコシステムはただ base64 と名付けられた1つのクレートに落ち着き、それは今や土台を支える存在になりました:バージョン 0.23.1 が2026年8月4日にリリースされ、2015年12月以来45のバージョンが公開され、ダウンロードカウンターは約15億に迫っています。下記のすべてのエンコード例は、その1つのクレートと、行折り返しと PEM アーマーのための小さな相棒2つを使います。
ツールチェーンとクレート
まずツールチェーン。世界ごとに1コマンド:
# Debian / Ubuntu
sudo apt install rustc cargo
# または公式インストーラー。rustup と cargo をセットアップします
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
ついでクレート。どんな cargo プロジェクトの中からも:
cargo new my-app
cd my-app
cargo add base64
その1行がインストールのすべてで、引き込む依存はちょうどゼロです。クレートには、知っておくべきオプションのフィーチャー3つが付属します:std(デフォルトで有効。std::io のストリーミング、標準の Error 実装、ヒープ割り当てを与える)、alloc(組み込み no_std ビルド向けの割り当て API)、simd-unsafe(デフォルトで有効。SIMD エンジンで、数セクション先に出会います)。サポートされる Rust の最小バージョンは 1.71.0 なので、最近のどれでも動きます。その周りに座するのは、コアが意図的にやらない仕事のための相棒たちです:
- line-wrap(バージョン 0.2): MIME と PEM が要求する 76 文字または 64 文字の改行を挿入します。
base64クレート自身は折り返しを拒否します。意図的なことで、後で分かります。 - pem(バージョン 4):証明書とキーのための
-----BEGIN ...-----ブロックを構築・パースします。内部ではbase64に依存し、アーマーと折り返しを足します。 - base64ct(バージョン 1.8): RustCrypto プロジェクトによる定時間デコーダー。往復の読み取り側が敏感な半分であるときに使います。
- base64-turbo(バージョン 0.3):新しい高スループットコーデックで、モダンなハードウェアで 100 GiB/s を超えるピーク性能を発揮します。
1回のエンコード、4文字
考えられる中で最も小さな儀式はこのような形で、これで既に往復全体が証明されます:
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!
}
注目すべきは2つの点です。prelude モジュールは2つをそっと一度に手渡します。BASE64_STANDARD エンジンと、あなたがメソッドを呼び出している Engine トレイトです。そのため、この例に必要なのはただの use base64::prelude::*; のみです。そして encode() は、AsRef<[u8]> 境界のおかげで、バイトとして読めるものを何でも受け取ります:&str、&[u8] リテラル、Vec<u8>、何でも。例のデコード側の半分は、エンコーダーを見張るためにそこにいるだけです。逆方向には姉妹サイトに完全なガイドがあるためです。クレート自身のスモークテストが欲しいなら、そのドキュメントは asdf をエンコードして YXNkZg== を取り戻します。同じアルファベット、同じ数学です。
各バイトの正確な値札
存在するすべての Base64 エンコーダーは同じ税を課し、その計算が見えるようになれば、予算を立てられます。出力の各文字は6ビットを持ち、入力の各バイトは8ビットを持ち、両方を満たす最小の山は24ビット:ちょうど3バイトが入り、ちょうど4文字が出ます。その比率が全部で、3キロバイトのファイルは4キロバイトになり、10メガバイトのアップロードは13.3メガバイトになります。パディングは、見えるようになった繰り上げ誤差です:入力が3バイトの倍数でないと、最後のグループに空き容量が残り、エンコーダーは = で埋めて出力長を4の倍数に保ちます。ここが RFC 4648 の真理表で、標準エンジンはそれを正確に再現します:
| 入力 | 長さ mod 3 | エンコード結果 | 出力長 |
|---|---|---|---|
""(空) |
0 | ""(空) |
0 |
f |
1 | Zg== |
4 |
fo |
2 | Zm8= |
4 |
foo |
0 | Zm9v |
4 |
foobar |
0 | Zm9vYmFy |
8 |
use base64::prelude::*;
let words: [&[u8]; 4] = [b"", b"f", b"fo", b"foo"];
for input in words {
println!("{:?} -> {:?}", String::from_utf8_lossy(input), BASE64_STANDARD.encode(input));
}
// "" -> ""
// "f" -> "Zg=="
// "fo" -> "Zm8="
// "foo" -> "Zm9v"
最初の行を2回読みましょう。頭の中で間違えるのがみんなそこだからです:空の入力は空文字列にエンコードされ、AA== にはなりません。文字列 AA== はちょうど1バイト、NUL のエンコードで、これは確かに別のペイロードです。そしてエンコード前にバッファのサイズを決める必要があれば、クレートは数学を const fn として手渡してくれるので、コンパイル時でさえ配列のサイズを決められます:
let padded = base64::encoded_len(15, true).unwrap();
let slim = base64::encoded_len(15, false).unwrap();
println!("{padded} / {slim}"); // 20 / 20
println!("{:?}", base64::encoded_len(13, true)); // Some(20)
println!("{:?}", base64::encoded_len(13, false)); // Some(18)
println!("{:?}", base64::encoded_len(14, false)); // Some(19)
println!("{:?}", base64::encoded_len(100, false)); // Some(134)
13バイトと14バイトの行を見てください。ざっくり計算を転ばせるのはこの2つです:13バイトはパディングなしで18文字、パディング付きで20文字が必要ですが、14バイトは19と20が必要になります。関数は Option を返し、長さが計算でオーバーフローする場合以外は None にならないので、実際にメモリに存在しうる入力に対しては unwrap() が安全です。メール型の世界では、この税には追加料金が載ります:MIME は1行76文字で折り返し、古い指針では折り返された Base64 は元のサイズのおよそ1.37倍のコストがかかり、さらにヘッダのオーバーヘッドが加わるとされます。クレートの公式 FAQ は、パディング自体についてもう少し礼を失った意見を載せています:= バイトは「「そのパディングは正しくない」と言う機会を与えるほか、デコードには影響しない」のだとか、「エクサバイト規模のストレージと転送が、無意味な = バイトのために確実に浪費されてきた」のだそうです。
パディング:読み手への決定
base64 0.23 では、素のエンコード関数は非推奨になりました - 現在のやり方はEngine のメソッドを呼び出すことで、エンジンは方針です:どのアルファベットで書き、どのパディングを足すか。プリセットは base64::engine::general_purpose に住んでおり、人気のある4つは prelude へ再エクスポートされています:
| エンジン | アルファベット | パディングを追加 | 向いているもの |
|---|---|---|---|
STANDARD / BASE64_STANDARD |
+ / |
はい | すべて。デフォルト |
STANDARD_NO_PAD / BASE64_STANDARD_NO_PAD |
+ / |
いいえ | 自分で消費するスリムなペイロード |
URL_SAFE / BASE64_URL_SAFE |
- _ |
はい | それでもパディングが欲しい URL の中身 |
URL_SAFE_NO_PAD / BASE64_URL_SAFE_NO_PAD |
- _ |
いいえ | JWT、URL、オブジェクト ID |
パディングなしエンコードは、クレートが許容するハックではなく、第一級の立場です。プリ設定済みの NO_PAD と PAD 設定定数が、0.23.0 に追加された *_INDIFFERENT の兄弟たちとともにあります。プリセットが合わなければ、Alphabet と1つのダイヤルを持つ GeneralPurposeConfig から自分のエンジンを組みます。エンジンは作るコストが安いので、リクエストごとに組み直すのではなく、結果を const に収納しましょう:
use base64::engine::general_purpose::{GeneralPurpose, GeneralPurposeConfig};
use base64::prelude::*;
const SLIM: GeneralPurpose = GeneralPurpose::new(
&base64::alphabet::STANDARD,
GeneralPurposeConfig::new().with_encode_padding(false),
);
fn main() {
println!("{}", SLIM.encode("fo")); // Zm8
println!("{}", BASE64_STANDARD.encode("fo")); // Zm8=
}
ここで決定は、他の人たちのデコーダーへの決定になります。あれは決して美学的なものだけではありません。デコード側の厳格さのルールは DecodePaddingMode から来ており、下の表は「向こうが私が書いたものを読めるのか?」に答えます:
| あなたがこれでエンコード | 厳格な STANDARD デコーダー |
STANDARD_NO_PAD デコーダー |
INDIFFERENT デコーダー |
|---|---|---|---|
STANDARD(パディングあり) |
読む | = を拒否 |
読む |
STANDARD_NO_PAD |
拒否:パディングなし | 読む | 読む |
URL_SAFE_NO_PAD |
拒否:アルファベット違い | 拒否:アルファベット違い | URL アルファベットでないと読めない |
実務のルールはその表から落ちます。両端を自分が制御するなら、1つのエンジンを決めてどこでも使い、バイトを節約するためパディングなしを優先してください。外部世界からのデータを消費するなら、あなたのデコーダーがどのエンジンを出力すべきかの投票権を持ちます:標準的な STANDARD デコーダーはあなたのパディングを必要とし、STANDARD_PAD_INDIFFERENT デコーダーは両方を受け入れます。そしてこの選択にはセキュリティの風味もあります。同じペイロードのパディングあり・なしの両方を許すと Base64 は変形可能になり、クレートの公式ドキュメントもリンクを載せている 2022年の論文「実践における Base64 の変形可能性」(Chatzigiannis と Chalkias, ePrint 2022/361)がその理由を示しています。同じデータが2通りに書けるプロトコルには、エンコード済み文字列を識別子として扱うコードを驚かせる癖があるので、フォーマットが1つの正規な表記を定義しているなら、境界でそれを強制してください。
トークンとリンクのための Base64url
標準 Base64 のアルファベットの最後2文字は + と / で、URL の中では、この言語の中で最も高価な文字の2つです:プラスは %2B になり、スラッシュは %2F になり、パディングは %3D になります。RFC 4648 の第5節は、URL とファイル名に安全なアルファベットでこれを解消します。2つの厄介者を - と _ に交換し、通常はパディングもスキップします。エンジンはこの区別を逃がさせないようにしています:
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]
考えうる中で最も手ごわい入力の3バイトが、1つのパーセントエスケープも使わずに URL、ファイル名、クッキー、データベースのキーに貼り付けられる4文字の文字列になります。ここが JSON Web Token が住むアルファベットです:JWT はドットで結ばれた3つの base64url 部分からなり、jsonwebtoken クレート(2026年時点ではバージョン 11)で1つ発行するのは、このような形です:
use serde::Serialize;
use jsonwebtoken::{EncodingKey, Header, encode};
#[derive(Debug, Serialize)]
struct Claims {
sub: String,
company: String,
exp: u64,
}
let key = b"secret";
let my_claims = Claims {
sub: "b@b.com".to_owned(),
company: "ACME".to_owned(),
exp: 19_000_000_000, // 十分に未来のほう
};
let token = encode(&Header::default(), &my_claims, &EncodingKey::from_secret(key)).unwrap();
println!("{token}");
// eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJiQGIuY29t...
バージョン 11 には、初心者を噛む1つのセットアップ要件があります:クレートは Cargo.toml で rust_crypto か aws_lc_rs のフィーチャーをちょうど1つだけ有効にすることを要求し、どちらも有効でないと、トークンを署名・検証した初回にパニックします。構造体の中の exp クレームに注目してください:クレートの検証はデフォルトでそれを必須扱いにするので、実際のトークンはいずれにせよこれを抱えており、トークンの中の base64url アルファベットは完全にライブラリの管轄です。トークンを発行するのではなく検査するだけなら、姉妹の記事が5行ののぞき見を示しています。そして黄金律の再確認。これはトークンに対して特に強い力で適用されます:JWT の3つの部分は、すべてキーなしで読めます。Base64 は窓際の席であって、鍵ではありません。
テキストが入って、バイトが出る
エンコーダーは心を読めないので、Rust で「この文字列をエンコードしろ」とは常に「この文字列の UTF-8 バイトをエンコードしろ」を意味します。str::as_bytes() が渡すものがそれだからです。朗報は、モダンなウェブはほぼ完全に UTF-8 であることで、正直な道は短くて幸せです:
use base64::prelude::*;
let text = "café";
let packed = BASE64_STANDARD.encode(text.as_bytes());
println!("{packed}"); // Y2Fmw6k=
マルチバイトのケースはすべて振る舞います:
| 元のテキスト | Base64 | 往復 |
|---|---|---|
café |
Y2Fmw6k= |
クリーン |
日本語 |
5pel5pys6Kqe |
クリーン |
😀 |
8J+YgA== |
クリーン |
π ≈ 3.14159 |
z4Ag4omIIDMuMTQxNTk= |
クリーン |
本当の判断は1つだけで、どこから始まるバイトなのか、ということです。データがテキストではなくバイトとしてやってきたら、ディスクから読んだファイルやネットワーク呼び出しからのバッファなら、文字列を完全にスキップして Vec<u8> を直接エンコードしてください。PNG や protobuf のような非 UTF-8 のペイロードには、それが唯一の正しい答えでもあります。画像をコードの隣に置いて、読み取りをそこに向ければいい:
use base64::prelude::*;
let file_bytes = std::fs::read("sprite.png").unwrap();
let size = file_bytes.len();
let packed = BASE64_STANDARD.encode(file_bytes);
println!("{size} bytes -> {} base64 chars", packed.len());
// すべてのエンコード済み PNG は iVBORw0K で始まる
assert!(packed.starts_with("iVBORw0K"));
最後のあのアサーションはタダの健全性チェックで、インターネットで最も認識されやすいプレフィックスの1つです。そしてもし同じ論理テキストを2つの異なる文字コードでエンコードしたり、別の文字コードと誤読したバイトをエンコードしたりしたら、往復は顔色一つ変えずに文字化けとして戻ってきます。エンコーダーは決して嘘をつきません。渡されたバイトをエンコードするだけです。それがその最大の強みであり、唯一の罠です。
フォーマットが行を望むとき
base64 クレートは意図的に改行を挿入しません。そう決めたのは今回が初めてでもありません。バージョン 0.5.0 は設定可能な行末付きの組み込み MIME 行折り返しを搭載してリリースされ、バージョン 0.10.0 がそれを削除しました。折り返しは汎用クレートにとって意見が強すぎ、no_std の話を複雑にする、とライブラリが判断したためです。フォーマットが行を要求するなら、line-wrap クレートがまさにそのために存在します。その1つの関数 line_wrap() は、あなたの予割り当てバッファ、入力長、桁数上限、行末を受け取り、挿入した行末バイト数を返します:
use base64::prelude::*;
let data = BASE64_STANDARD.encode(vec![b'a'; 300]); // 400文字
let mut buf = vec![0u8; data.len() + 16];
buf[..data.len()].copy_from_slice(data.as_bytes());
let endings = line_wrap::line_wrap(&mut buf, data.len(), 76, &line_wrap::crlf());
buf.truncate(data.len() + endings);
let wrapped = String::from_utf8(buf).unwrap();
println!("{} chars in, {} bytes out, {} line endings", data.len(), wrapped.len(), endings);
// 400 chars in, 410 bytes out, 10 line endings(CRLF 5組)
バッファは行末の分を余分にサイズ決め、関数を呼び、報告された合計で切り詰める。5組の CRLF が MIME の 76 桁ルールの代償です。PEM では上限と行末を入れ替えて、64 桁と line_wrap::lf() にすれば、アーマーの本文が完成します。それから pem クレートが1回の呼び出しでバナーを足します:
let pem_block = pem::encode(&pem::Pem::new("CERTIFICATE", b"0123456789abcdef"));
println!("{pem_block}");
// -----BEGIN CERTIFICATE-----
// MDEyMzQ1Njc4OWFiY2RlZg==
// -----END CERTIFICATE-----
let back = pem::parse(pem_block).unwrap();
println!("{}: {} bytes", back.tag(), back.contents().len());
// CERTIFICATE: 16 bytes
// 消費者が細かい場合の Unix スタイルの行末
let lf_block = pem::encode_config(
&pem::Pem::new("KEY", b"0123456789abcdef"),
pem::EncodeConfig::new().set_line_ending(pem::LineEnding::LF),
);
デフォルトでは pem::encode は CRLF を使います。歴史的な PEM の慣習です。set_line_ending ビルダーは、それを期待するツール向けに LF に切り替えます。pem クレートのやらないことに注目してください:あなたが見える base64 関数は決して呼びません。エンコーディングは内部の管轄だからです。フォーマットが行を望むとき、アーキテクチャは1つの仕事に1クレートです。
定数空間でのストリーミング
1つの変数に保持するには大きすぎるデータには、クレートが Rust の io の残りと同じストリーミング哲学で答えます:write::EncoderWriter はどんなライターでもラップし、あなたに書かれたものをすべて定数空間で base64 エンコードします。バッファのための完全な儀式はこのような形で、このセクションの主役は finish() の呼び出しです:
use std::io::Write;
use base64::prelude::*;
use base64::write::EncoderWriter;
fn main() {
let mut encoder = EncoderWriter::new(Vec::new(), &BASE64_STANDARD);
encoder.write_all(b"the quick brown fox jumps over the lazy dog").unwrap();
let packed = encoder.finish().unwrap();
println!("{}", String::from_utf8(packed).unwrap());
// dGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw==
}
なぜ finish() が主役なのか?それは、最後の部分グループをフラッシュしパディングを足す唯一の呼び出しだからで、エンコーダーにはそうしない兄弟メソッドがあるからです。クレートの公式ドキュメントは率直に言います:finish() は「残りの入力バイトをエンコードし、適切な場合はパディングを追加します。破棄時に自動的に呼び出されます(Drop 実装を参照)が、基盤のライターを呼び出す際におきたエラーは抑制されます。そのようなエラーを処理したい場合は、自分で finish() を呼び出してください」。Drop 実装は BufWriter のように振る舞います:フラッシュはしますが、ドロップ中のエラーは無視します。最後の部分グループは失われないのですが、「たぶん動いた」はリリース戦略にはできません。それは、教えてくれただろう書き込みエラーが消えてしまうからです。
同じストリームは、パイプライン全体を1回の呼び出しにしたいなら io::copy を通って1つ外れた位置でも動き、さらに「フォーマット文字列の中でこれが欲しいだけ」という瞬間のためのボーナス・ラッパーもあります:
use std::io;
use base64::prelude::*;
use base64::write::EncoderWriter;
let file = b"the quick brown fox jumps over the lazy dog".to_vec();
let mut cursor = io::Cursor::new(file);
let mut encoder = EncoderWriter::new(Vec::new(), &BASE64_STANDARD);
io::copy(&mut cursor, &mut encoder).unwrap();
let packed = encoder.finish().unwrap();
println!("{}", String::from_utf8(packed).unwrap());
use base64::display::Base64Display;
use base64::prelude::*;
let value = Base64Display::new(b"\0\x01\x02\x03", &BASE64_STANDARD);
println!("base64: {value}"); // base64: AAECAw==
その Base64Display ラッパーは小さな宝石です:ヒープ割り当てを1回もせずに、どのフォーマット文字列の中ででもバイトを Base64 として整形してくれます。ログ行やデバッグ出力が急に気持ちよくなります。
割り当て、そしてその不在
便利なメソッドは割り当てを行い、あなたの人生の大半にとって、それが正しい取引です。しかし Engine トレイトは3つの味のエンコードを露出しており、下の表が決定マトリクス全体です:
| メソッド | 出力 | 割り当て |
|---|---|---|
encode() |
新しい String |
常に |
encode_string() |
あなたの String に追記 |
成長が必要ならだけ |
encode_slice() |
あなたの &[u8] に書き込み |
決して |
use base64::prelude::*;
let input = b"Hello, world!";
let mut buf = vec![0u8; base64::encoded_len(input.len(), true).unwrap()];
let written = BASE64_STANDARD.encode_slice(input, &mut buf).unwrap();
buf.truncate(written);
println!("{}", std::str::from_utf8(&buf).unwrap()); // SGVsbG8sIHdvcmxkIQ==
// またはバッファを完全にスタック上に保つ
let mut stack = [0u8; 24];
let n = BASE64_STANDARD.encode_slice(b"abc 123", &mut stack).unwrap();
println!("{}", String::from_utf8(stack[..n].to_vec()).unwrap()); // YWJjIDEyMw==
// サイズ決めを間違えたなら、バッファオーバーフローでなくエラーが得られる
let mut tiny = [0u8; 5];
println!("{:?}", BASE64_STANDARD.encode_slice(input, &mut tiny));
// Err(OutputSliceTooSmall)
バッファは encoded_len() でサイズを決め、encode_slice() で書き、サイズを間違えたら未定義動作の代わりにクリーンな EncodeSliceError::OutputSliceTooSmall が得られます、システムの言語では、退屈な午後と長い午後の違いです。組み込みの仕事では同じ関数が alloc フィーチャーの裏に存在するので、API を保ったままヒープを落とせます。
スピード:SIMD エンジン
2026年7月にリリースされたバージョン 0.23.0 が、見出しのフィーチャーをもたらしました:標準アルファベットと URL 安全アルファベット向けの SIMD 加速エンジンです。3つあり、あなたのハードウェアをどれほど強く信頼するかで分かれます:
| エンジン | 実行時に検出 | no_std で動作 |
|---|---|---|
Simd |
はい。AVX2 か NEON を選び、スカラーエンジンへフォールバック | いいえ。検出には std が必要 |
Avx2 |
いいえ。CPU が AVX2 を持っているとする | はい。x86_64 ターゲットで |
Neon |
いいえ。CPU が NEON を持っているとする | はい。aarch64 ターゲットで |
use base64::engine::general_purpose::GeneralPurposeConfig;
use base64::engine::{Avx2, Simd};
use base64::Engine;
let turbo = Simd::standard(GeneralPurposeConfig::new());
println!("{}", turbo.encode("simd works!"));
// c2ltZCB3b3JrcyE=
if let Some(fixed) = Avx2::standard(GeneralPurposeConfig::new()) {
println!("{}", fixed.encode("hello avx2")); // aGVsbG8gYXZ4Mg==
}
Simd コンストラクタは CPU 検出を1回行って、見つけた中で最良のカーネルを返し、どれも該当しなければスカラーエンジンを返します。そのため const に1回作って収納し、あるいは起動時に1回作って再利用しましょう。能力のあるハードウェアでは、エンコードもデコードもスカラーパスの数倍速いです。率直な脚注を1つ:SIMD パスはクレートの中で unsafe に触れる唯一の場所であり、それがフィーチャー名が simd-unsafe である理由です。フィーチャーを切ったら、クレート全体は再び #![forbid(unsafe_code)] になり、スカラーエンジンは引き続き誠実な仕事をします。生のスループットだけが目的なら、base64-turbo クレートがさらなる限界を押し広げ、実行時検出の裏にある AVX512、AVX2、NEON カーネルで 100 GiB/s を超えるピーク性能を発揮し、それ以外は 100% 安全なスカラーフォールバックです。base64 クレートは MIT/Apache-2.0 のデュアルライセンスなので、スピードを含めてすべて無料です。
もう4つのアルファベット
RFC のアルファベットがデフォルトですが、base64 クレートは4つ同梱しています。それぞれが、独自のひねりが必要だった何らかの実在のプロトコルへの小さな記念碑です:
| アルファベット | ひねり | 使う人 | abc 123 のエンコード結果 |
|---|---|---|---|
alphabet::CRYPT |
./ が最初、続いて数字と文字、パディングなし |
クラシックな Unix の crypt(3) パスワードハッシュ | MK7X612mAk |
alphabet::BCRYPT |
./ が最初、続いて文字、最後に数字 |
bcrypt のパスワードハッシュ | WUHhGBCwKu |
alphabet::IMAP_MUTF7 |
コンマがスラッシュの代わりに立つ、パディングなし | IMAP の modified UTF-7 メールボックス名 | YWJjIDEyMw |
alphabet::BIN_HEX |
見間違いやすい文字を省く、句点が多いアルファベット | BinHex 4、古い Macintosh のファイルラッパー | B@*M)$%b-` |
use base64::engine::general_purpose::{GeneralPurpose, NO_PAD};
use base64::Engine;
let crypt = GeneralPurpose::new(&base64::alphabet::CRYPT, NO_PAD);
println!("{}", crypt.encode(b"abc 123")); // MK7X612mAk
let bcrypt = GeneralPurpose::new(&base64::alphabet::BCRYPT, NO_PAD);
println!("{}", bcrypt.encode(b"abc 123")); // WUHhGBCwKu
let imap = GeneralPurpose::new(&base64::alphabet::IMAP_MUTF7, NO_PAD);
println!("{}", imap.encode(b"abc 123")); // YWJjIDEyMw
同じ入力、3つの異なる出力。すべてが自分の方言で有効な Base64 です。本物のスーパーパワーを持つのが、crypt アルファベットです:記号がビットパターンに合わせて並んでいるので、エンコード済み文字列をソートすると、元のバイトをソートしたのと同じ順序になります。GEDCOM 5.5(1996)がマルチメディアフィールドでこれを使った理由がここにあります - 5.5.1 改訂版は機能を削除 - そしてクレートは今でもこのアルファベットをあなたのために同梱しています。そして必要な方言がクレートになければ、64文字の文字列で定義できます。Alphabet::new() がエンコード表とデコード表をあなたの代わりに作るからです:
use base64::alphabet::Alphabet;
use base64::engine::general_purpose::{GeneralPurpose, PAD};
use base64::Engine;
// ビザード・ワールドの base64: +/ が最後ではなく最初に立つ
let alphabet = Alphabet::new(
"+/ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
).expect("a valid 64 char alphabet");
let bizarro = GeneralPurpose::new(&alphabet, PAD);
println!("{}", bizarro.encode(b"hello 99")); // YETqZE6eMRi=
// 一方、標準エンジンはこう言います:
println!("{}", base64::prelude::BASE64_STANDARD.encode(b"hello 99"));
// aGVsbG8gOTk=
カスタムアルファベットへの道について、1つの警告:方言を発明した瞬間、あなたのデータを読めるのは地球上であなただけになります。プロトコルが要求するときにだけ行い、どれなのかをコメントに書きましょう。
エンコーダーが働く場所
Base64 エンコードは、予想のつきのつく状況の顔ぶれで Rust プロジェクトに現れます:
- JSON API のファイルアップロード:ファイルがテキストのコスチュームを着たバイトフィールドになる、圧倒的多数で最も一般的な用途。
- HTML と CSS の Data URI:
data:image/png;base64,...系のもの。小さなアイコンには見事、ヒーロー画像には怪しい。 - JWT と OAuth:base64url が方言で、
jsonwebtokenクレートが道具。 - 証明書とキーの PEM ブロック:Base64 を1行64文字で折り返す
-----BEGIN CERTIFICATE-----セクション。 - XML と設定ファイルの中のバイナリ:エクスポートされたブックマークや設定ダンプにまだ見かける
<data encoding="base64">パターン。 - LDAP と LDIF ファイル:バイナリの属性値を1行に保つため Base64 を使う。
- QR コードのペイロードとクリップボードの引き渡し:文字は旅を生き延びて、バイナリはそうではない。
- HTTP Basic 認証ヘッダー:
Basic TWFuOnBhc3M=は認証情報ペアであり、これは隠す問題ではなく梱包の問題だという再提醒。
そしてこれらをすべて統べる黄金律:Base64 は梱包テープであって、鍵ではありません。暗号でもなく、圧縮でもありません - 圧縮の反対で、この記事を読んだ誰でも1行でそれがしたことすべてを逆操作できます。自由にエンコードしてください。ただし、パスワード、API キー、シークレットをエンコードして「保護した」と呼ぶことは決してしないでください。隠す必要があるなら本物の暗号を使い、大きいなら、multipart アップロードの方が税より単に安かったのではないか、と考えましょう。
小さな歩みの10年
フォーマットはウェブより古い。1987年、Privacy-Enhanced Mail プロトコル(RFC 989)は 7ビットのメールチャネル上でバイナリデータを運ぶ必要があり、ちょうど64文字行でこのエンコードを標準化しました。インターネットにあるすべての -----BEGIN CERTIFICATE----- ブロックはその決定の子孫であり、だからこそ PEM ファイルは今日でも64で折り返されます。1996年、MIME 仕様(RFC 2045)がこの方式を採用し、64文字のアルファベットにちなんで「base64」と名付け、折り返しを76文字へ移しました。それ以前、Unix の箱には uuencode が、Mac には BinHex が同梱されていました。それぞれ独自のアルファベットを持ち、どちらもファイルヘッダーを持った化石のように、古いシステムに今も顔を出します。2006年、RFC 4648 が誰もが引用する標準になりました:アルファベットの表、base64url 変種、そしてこの記事のすべてのエンジンが実装する正規エンコーディングのルール。その第3.5節は、エンコーダーに使われていない終端ビットをゼロに設定することを要求し、クレートはそうします。もしあなたのペイロードが後に厳格なデコーダーの InvalidLastSymbol チェックに引っかかったなら、破損は上流で起きているということです。
クレート自身の歴史は韻を踏んでいます。crates.io に現れたのは2015年12月で、バージョン 0.5.0 は設定可能な行末付きの MIME 行折り返しを誇らしげに追加しました。それから2018年のバージョン 0.10.0 が、折り返しと空白処理を削除します。汎用のクレートはエンコードをして、詩の部分はアプリケーション層に委ねるべきだと、ライブラリが判断したためです。同じリリースはストリーミングの EncoderWriter も追加しました。2022年のバージョン 0.20.0 はエンジン抽象化を導入し、正規パディングをデフォルトにし、0.21.0 はエンジンメソッドに置き換える形で古いフリー関数を非推奨にしました。コンパイラの注意書きは「Use Engine::encode」(それでも動きます。多くのレガシーコードが快くコンパイルできる理由がここにあります)。2024年、バージョン 0.22.0 はエラーの意味論を鋭くし、デコードを5〜10パーセント速くしました。そして2026年7月、バージョン 0.23.0 が SIMD エンジン、カスタムなパディング記号、より明確なエラーメッセージ、1.71 への MSRV 引き上げとともに到着し、8月4日の 0.23.1 パッチは非 SIMD アーキテクチャ向けのテストスイートを修正しました。
笑顔になるに値するもの
完全なガイドは笑顔で終わるべきなので:
- 単語「base64」は
YmFzZTY0にエンコードされます。自らを記述するフォーマットとは、モールスで話す鏡の技術版です。 - 空文字列は空文字列にエンコードされます。「無」は唯一、コストがかからない入力で、それはある種の非課税です。
AA==は「無」のエンコードではありません:1つの NUL バイトのエンコードです。Base64 では「無」と「ゼロ」は違う生き物で、デコーダーはそれを区別します。- すべての Base64 エンコード済み PNG は
iVBORw0Kで始まります。それは梱包テープに包まれた PNG のマジックナンバーで、インターネットで最も認識されやすいプレフィックスの1つです。 - URL では、標準 Base64 の文字はパーセントエスケープのコスチュームが必要になります:プラスは
%2Bになり、スラッシュは%2Fになり、パディングは%3Dになります。Base64url は、文字が自分の顔を素で出せるように存在します。 - YouTube の動画 ID はパディングなしの base64url です:ID の8バイトが、どこにでも貼れる 11文字の文字列になります。インターネット全体で、パディングなしモードの最も目に見える用途の1つです。
- 古い crypt(3) のパスワードアルファベットは正しくソートできます:ソートされたエンコード済み文字列は、ソートされた平文と同じ順序に並びます。GEDCOM 5.5(1996)はマルチメディアフィールドでそのアルファベットを使い、5.5.1 改訂版は機能を削除しましたが、クレートは今もあなたのためにそれを同梱しています。
- BinHex、古い Macintosh ラッパーは、見間違いやすい
7、O、g、oのような文字を除外するようアルファベットを組みました。スペルチェック以前の世の中で、人間の目のために設計されたエンコーダーです。 - クレートの公式 FAQ はパディングについて率直です:エキサバイト規模のストレージと転送が、無意味な
=バイトのために確実に浪費されてきた、と。料金所は1987年から徴収し続けています。 - Base64 は暗号ではありません。もしそうだったら、この記事の例の出力は読めなかったでしょう。それは窓際の席であって、金庫ではありません。
短いバージョン
エンジンは、データが行く道によって選びましょう:自分でデコードもするすべてには BASE64_STANDARD、両端を制御していてバイトを取り戻したいときは _NO_PAD のエンジン、トークンと URL には URL_SAFE_NO_PAD、カスタムの Alphabet はプロトコルが要求するときにだけ。バッファは encoded_len() でサイズを決め、大きなものは EncoderWriter でストリーミングし、必ず finish() で閉じ、行の折り返しはフォーマットが要求するときにだけ line-wrap と pem で行い、できる限り SIMD エンジンの重労働を任せ、4対3の取引がテキスト専用のお門を通る料金であることを思い出してください。すべてをエンコードし、本物の鍵を必要とするものだけを保護してください。そして逆方向 - 旅を始めたバイトへ文字列を解き戻す - が必要になったら、姉妹の記事が Rust でのデコードを、正確なエラーメッセージの完全なスコアカードつきでカバーしています。
最終更新: 2026-10-09