Base64 形式を扱う必要がありますか?それならこのサイトが最適です!データをエンコードまたはデコードするために便利なオンラインツールをご利用ください。

Rust での Base64 デコード:完全ガイド

Rust プログラムに1つの文字列が舞い込みました。アルファベットと数字、ときどき現れるプラスやスラッシュ、そして末尾に怪しげな = が1つか2つぶら下がっている。それが Base64 で、このガイドが扱うのは、驚きなしに元のバイトを取り戻すことです。このサイトのホームページではフォーマットを隅々まで詳しく説明しているので、ここで繰り返すのは取引の形だけです:アルファベットの文字4つが入力バイト3つを表し、末尾の = 1つか2つが本当のデータの終わりを示します。デコードとはこの取引を逆方向に回すことです。この記事のすべては、それを意図的に行う方法についてです。

Rust を他の多くの言語と分ける唯一のひねりはここにあります:標準ライブラリには Base64 が一切ありません。std のどこかに隠れた base64_decode() も、それを呼び出してくれる use std::... もありません。エコシステムはただ base64 と名付けられた1つのクレートに落ち着き、それは今や土台を支える存在になりました:バージョン 0.23.1 が2026年8月4日にリリースされ、2015年12月の初リリース以来45のバージョンが公開され、ダウンロードカウンターは約15億に迫っています。あなたが今まさにこのクレート経由で Base64 をデコードしている可能性はほぼ確実です。直接でも、あるいは jsonwebtoken や pem、serde_with のような、すべてがこれに依存するクレートを介してでも。

1つのクレートと、その仲間たち

まだそのマシンに Rust 自体が入っていないなら、オペレーティングシステムが用意してくれます:Debian や Ubuntu なら rustc と cargo、macOS や Windows ならパッケージかインストーラー、あるいは rustup をセットアップする公式インストーラーです:

# Debian / Ubuntu
sudo apt install rustc cargo
# または公式インストーラー。rustup と cargo をセットアップします
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

ついでクレートです。どんな cargo プロジェクトの中からでも、この1行がインストールのすべてで、引き込む依存はちょうどゼロです:

cargo new my-app
cd my-app
cargo add base64

ビルドの形を決めるのは、オプションのフィーチャー3つです。std はデフォルトで有効で、std::io のストリーミング型、標準の Error 実装、ヒープ割り当てを有効にします。alloc は、完全な標準ライブラリのない組み込み no_std ビルド向けの、メモリ割り当てを行う API を提供します。simd-unsafe はデフォルトで有効で、後で出会うベクトル化エンジンを制御しています。サポートされる Rust の最小バージョンは 1.71.0 なので、最近のどれでも動きます。コアのクレートを取り囲むのは、専門家の小さな輪です。それぞれが、コアが意図的にあなたに委ねる領域を担当します:

クレート バージョン(2026) もたらすもの 使う場面
base64ct 1.8 RustCrypto プロジェクトによる定時間デコード。ヒープ API は alloc フィーチャーの裏にあります デコードするバイトが、キー資料のようにタイミングを通じて情報を漏らす可能性がある場合
data-encoding 2.11 base32、hex 仲間とともに Base64。寛容な MIME 変種と、スライス単位のエンコード/デコードを備えます 1つのコンポーネントが、汚れた、折り返された、複数プロトコルの入力を解析しなければならない場合
base64-turbo 0.3 100 GiB/s を超えるピーク性能の新しいコーデック。AVX512、AVX2、NEON カーネルと、安全なスカラーフォールバック付き スループットだけが目的で、標準エンジンでは CPU サイクルを机上に置き去りにしている場合

これらはいずれも日々の作業の代わりにはなりません。Rust プログラムの大多数にとって、正しく完全な答えは base64 単独です。この記事の残りも、デコード自体にはこの1つのクレートを使い、仕事の内容が base64 より広くなったときだけ、輪の専門家に頼みます。

バイトへの道は3行

デコード人生の90%は3行に収まります。定番のスモークテストには、有名な TWFu 文字列を使います:

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"));
}

出力は Man で、この小さな儀式には覚える価値のあることが3つあります。第一に、decode() が渡すのは常に Vec<u8> で、文字列ではありません。これは仕様であり、偶然ではありません。Base64 は一文でも JPEG でも証明書でも運べます。中身が何であるか分かるまで、どれを特別扱いしてはいけません。第二に、バイトからテキストへの飛躍は String::from_utf8() 経由の、独立した意図的なステップです。文字コードの判断が住んでいるのはまさにこのステップです。第三に、prelude モジュールは2つをそっと一度に手渡します。BASE64_STANDARD エンジンと、あなたがメソッドを呼び出している Engine トレイトです。明示的なインポートを好むなら、use base64::engine::general_purpose::STANDARD; と use base64::Engine; の組み合わせは、銘板をつけた同じドアです。

いずれあなたがエンコードしたものを自分でデコードする日が来るでしょう。ここには、両方向が一致することを証明する往復の例です。エンコードには姉妹サイトに独自の完全ガイドがあり、ここではテストデータを作るためだけに顔を出します:

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!
}

TWFu を懐に忍ばせておきましょう。あなたが書くあらゆるデコード経路のスモークテストになります。TWFu が Man に変わるなら、そのマシンは正直です。

「ダメ」の言い方は4つ

このセクションが、あなたの深夜2時を救います。本番の文字列が爆発したとき、クレートが何を文句にしているのかを正確に知りたいからです。朗報は、大きく、かつ正確に文句を言うことです。DecodeError はちょうど4つのバリアントを持ち、以下は典型的な違反者たちに対して、厳格な標準エンジンを通じてそれぞれどう聞こえるかです:

入力 何が悪い 正確なエラー
"SGVs bG8s" 半角スペースが紛れ込んだ Invalid symbol 32, offset 4.
"SGVs\nbG8s" 改行が紛れ込んだ Invalid symbol 10, offset 4.
"SG=VsbG8="" パディングが文字列の途中にある Invalid symbol 61, offset 2.
"SGVsbG8sIHdvcmxkIQ==xx" パディングの後にゴミが続く Invalid symbol 61, offset 18.
"S" 記号1つでは1バイトを形成できない Invalid input length: 1
"SGV" 必ず付くべきパディングなしの3記号 Invalid padding
"SGVs$bG8="" $ はアルファベットに存在しない Invalid symbol 36, offset 4.

注目してください。Invalid symbol メッセージは、問題のバイトの値とそのオフセットの両方を教えてくれるので、現場にそのままジャンプできます。InvalidLength バリアントがうるさく厳しいのです。バージョン 0.22.0 以降、有効な記号の数が不可能なときにだけ発火します。つまり4の倍数より1つ大きい長さで、その他の不正な長さはパディングエラーとして顔を出します。各失敗を別々に扱いたい日に備えて、完全な match をここに置きます:

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"),
  }
}

この意図的な厳格さは、規格そのものに遡ります。RFC 4648 の第12節は、アルファベット外の文字が帯域外情報を密輸する隠しチャネルとして悪用されたり、雑なパーサのバグを突かれたりする警告を発し、デコーダーはそれらを拒否することを勧めています。MIME 仕様が有名な例外で、デコーダーに紛れ込んだ文字を無視するよう明示しています。それが、下のメールのセクションで手懐け方を教える入力の形です。

パディングへの3つのスタンス

野に溢れるあらゆる Base64 文字列は、パディングについて無言の約束をしています。バージョン 0.23 では、DecodePaddingMode 列挙型を通じて、どの約束を強制するか選べます。モードは3つあり、挙動の違いは暗記する価値があります。以下は、= を落とした単語 fo である Zm8 のスコアカードです:

モード "Zm8"、パディングなし "Zm8="、パディングあり 使う場面
RequireCanonical、デフォルト Err(Invalid padding) Ok([102, 111]) データを自分が作り、自分が消費する場合
Indifferent Ok([102, 111]) Ok([102, 111]) 出所の混ざったデータを受け取る場合
RequireNone Ok([102, 111]) Err(Invalid padding) パディングなしのプロトコルを動かしている場合
use base64::engine::general_purpose::{GeneralPurpose, GeneralPurposeConfig, STANDARD_PAD_INDIFFERENT};
use base64::engine::DecodePaddingMode;
use base64::prelude::*;
let strict = BASE64_STANDARD;  // デフォルトは RequireCanonical
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)

デフォルトは標準に従います:RFC 4648 の第3.2節は、周囲の仕様が別と定めない限り、実装はエンコードしたデータの末尾に適したパッド文字を含めなければならないと述べており、標準的な STANDARD エンジンがそれを要求する理由はそこです。そしてこの選択は、気取った堅さのためだけでなく、セキュリティ的にも重要です。同じデータのパディングあり・なしの両方を許すと、Base64 は変形可能になります:同じ論理ペイロードが2通りの書き方をし、値に1つの正規な表記を前提とするコードは裏切られるのです。2022年の論文「実践における Base64 の可変性」(Chatzigiannis と Chalkias, ePrint 2022/361) は実際の結果を記録しており、クレートの公式ドキュメントもそれへのリンクを載せています。実務のルール:プロトコルごとに1つのモードを決め、生成側を自らが制御できない境界では厳格でいることです。

最終記号に隠れたビット

すべての文字チェックをくぐり抜けてしまう破損があります。Base64 の記号は1つあたり6ビットを持ち、入力の3バイト(24ビット)はちょうど4記号になります。入力が1バイトか2バイトのとき、最終記号には使われないビットが残ります。RFC は明確に述べています:準拠したエンコーダーは、これらの空きビットをゼロに設定しなければならない、と。バグのある、あるいは悪意のあるエンコーダーは代わりにそこにゴミを残せます。その結果はアルファベットチェック、長さチェック、パディングチェックをすべてパスしながら、こっそり破損した尾を運びます。厳格なエンジンがあなたの背中を守り、怪しいビットまで突きつけてくれる唯一無二の詳細なエラーメッセージをくれます:

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

エラーの中の 0b00010011 は、問題の記号を不正な上位ビット込みでデコードした値です。バージョン 0.23.0 がこの詳細を見えるようにしたのは、それが普段どれだけデバッグしにくいからにほかなりません。生成側が雑だと分かっていれば、with_decode_allow_trailing_bits(true) はゴミを拒否する代わりに飲み込んでくれます。ブラウザは逆の賭けをしました:JavaScript の atob() の裏にある WHATWG の forgiving-base64 アルゴリズムは、終端ビットに対して明示的に寛容です。一方、Rust のデフォルトは法医学の検査官です。あなたがテーブルのどちら側に座っているか、知っておくことです。

Base64url:旅をするアルファベット

標準 Base64 はアルファベットの最後2枠を + と / に使っていますが、それはまさに URL が望まない場所です。クエリ文字列の中ではプラスはスペースを意味し、スラッシュは新しいパス区間の始まりで、ぶら下がった = は区切り記号と読まれます。そこで RFC 4648 の第5節が、URL とファイル名に安全なアルファベットを定義します。2つの厄介者を - と _ に交換し、長さは通常復元できるので、パディングも通常は落とします。RFC はこのエンコードを標準 Base64 と同一視してはならないとまで警告しており、エンジンの名前もそれに同意しています:

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]
// 標準エンジンは同じ入力を拒否します。
// ダッシュはそのアルファベットにまったく存在しないためです
println!("{:?}", base64::prelude::BASE64_STANDARD.decode("----"));
// Err(Invalid symbol 45, offset 0.)

さて、なぜ多くの開発者が base64url に出会うのか:JSON Web Token です。JWT はドットで結ばれた3つの base64url 部分からなり、中をのぞき見るのは5行の作業です:

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}

率直な注意を2つ。JWT をデコードすることは、のぞき見であって信頼することではありません:第3部分は署名で、キーに対してチェックされるまで何の意味も持ちません。それが jsonwebtoken クレート(2026年時点ではバージョン 11)の仕事です。バージョン 11 には1つの鋭い刃があります:Cargo.toml で rust_crypto か aws_lc_rs のフィーチャーをちょうど1つだけ有効にしておかないと、トークンを署名・検証した初回にパニックします。またその Validation ビルダーはデフォルトで exp クレームを必須扱いにするので、他のライブラリ向けに発行されたトークンは調整した検証を必要とすることがあります。そしてデコードこそが、アルファベットの選択が効いてくる場所です:URL 安全な文字列を BASE64_STANDARD に渡すと、またはその逆にすると、拒否されます。-、_、パディングの欠けは、相手のエンジンにとってはすべて不正だからです。エンジンはプロトコルに合わせること。毎回、です。

バイトは単語ではない

この記事のすべてのデコーダーは、意図的にバイトで足を止めます。Rust ではそれが多くの言語より容易で、間違えうる隠れた文字コードのステップが存在しないからです。Base64 はバイト形式、それまでです。「それはどんなテキストだったのか」という質問への答えはあなたが負うもので、モダンなウェブのデフォルトの答えは UTF-8 です。標準ライブラリの1行で門番できます:

use base64::prelude::*;
let packed = "Y2Fmw6k=";   // アクセント付きの cafe という単語をパックしたもの
let bytes = BASE64_STANDARD.decode(packed).unwrap();
match std::str::from_utf8(&bytes) {
  Ok(text) => println!("{text}"),
  Err(_) => eprintln!("not utf-8: {bytes:02x?}"),
}

マルチバイトの陽の道は、ワイヤー上で出会うすべてをカバーします:

元のテキスト Base64 復元できる
café Y2Fmw6k= はい
日本語 5pel5pys6Kqe はい
naïve résumé bmHDr3ZlIHLDqXN1bcOp はい
😀 8J+YgA== はい
π ≈ 3.14159 z4Ag4omIIDMuMTQxNTk= はい

そしてペイロードがそもそもテキストでなかったら、同じコードの終わりだけが違ってきます。ここは PNG ファイルのマジックナンバーです。4バイトの 89 50 4E 47 と、それに続く CRLF のペア:

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();     // 出てくるのはバイトで、テキストではない

指針は短いものです:UTF-8 と仮定し、std::str::from_utf8() で検証し、失敗したものはすべて、fs::write、データベースの blob、あるいはそれが来た元の出口(sink)向けのバイトペイロードとして扱う。本当に文字コードを必要とするのは、移行しなかったレガシーデータに出会うときだけです。encoding_rs クレート(バージョン 0.8)が古いエンコーディングの名前を呼び、変換してくれます:

use base64::prelude::*;
use encoding_rs::Encoding;
let packed = "Y2Fm6Q==";   // Latin-1 バイトからパックした、アクセント付きの cafe
let bytes = BASE64_STANDARD.decode(packed).unwrap();
let (text, _, _) = Encoding::for_label(b"windows-1252").unwrap().decode(&bytes);
println!("{text}");   // UTF-8 としての、アクセント付きの cafe

base64 クレートには誤設定しうる「Latin-1 としてデコード」モードは存在しません。それは決してあなたの代わりに推測しないからです。保つべき規律がここにあります:クレートはバイトを渡し、それが何を意味するかはあなたが決める。

メールを生き延びた入力

メールシステムを生き延びた Base64 には改行が入っています:MIME は1行76文字で折り返し(PEM ブロックは64文字)、MIME 仕様は準拠したデコーダーにアルファベット外の文字(改行も含む)を無視するよう明示しています。このエンジンは MIME 準拠の正反対です:最初の改行を拒否し、ストリーミングリーダーはその拒否を、同じく正確な DecodeError を包んだ I/O エラーとして報告します:

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. })

両方の立場は同じ RFC に遡り、RFC は選択を周囲の仕様に委ねています。base64 クレートが選んだのは厳格な側でした。考え直したのは今回が初めてではありません:バージョン 0.5.0 は組み込みの MIME 行折り返しと空白処理を搭載してリリースされ、バージョン 0.10.0 が両方を削除しました。理由として、折り返しは汎用ライブラリにとって意見が強すぎ、no_std の話を複雑にするから、とされています。ですから、現実の折り返し済み入力を扱うレシピは、クレート自身のドキュメントが提案するのと同じものです:まずアルファベット外の文字を除去し、それからデコードする。メモリ内の文字列なら、それは1つのフィルタです:

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
}

改行を素通りしてくれるデコーダーが欲しいなら、data-encoding クレートの BASE64_MIME_PERMISSIVE 定数がまさにそれです:"SGVsbG8s\r\nd29ybGQh\r\n" を Hello,world! へデコードするのに、あなたが触る行はありません。全体を入力に保持できないストリームでは、クレートの FAQ が iter_read クレートを指します。バイトストリームをフィルタするか、不要なバイトが到着したら捨てていく小さな Read ラッパーを書くかのどちらかです。手づくりの「寛容デコーダー」を作る前に1つの警告:アルファベット外の文字をこっそり無視することは、RFC 4648 の第12節が隠しチャネルとして旗を上げる挙動そのものですから、期待する空白のみを除去し、それ以外は拒否してください。

メッセージ全体が手元にあれば、メールパーサが Base64 を代わりにやってくれます。mail-parser クレート(バージョン 0.11)はパースしながらすべての Content-Transfer-Encoding: base64 部分をデコードするので、添付ファイルはもはや折り返しも解き、デコードも済んだ生バイトとして戻ってきます:

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

その GIF 添付は42バイトにデコードされ、先頭の4バイトは 47 49 46 38、つまり ASCII の GIF8 という文字です。あなたは1行も Base64 のコードを書きませんでした。そしてそれが、パーサを使うことの要点です:エンコーディングの詳細はライブラリが引き受けることです。

巨大ペイロード

文字列は簡単です。Base64 が真価を発揮するのはファイルで、クレートの答えは Rust の io の残りと同じストリーミング哲学です。read::DecoderReader はどんなリーダーでもラップし、読み進めるにつれ透明にデコード済みバイトを手渡してくれます。そのため、数ギガバイトのエンコード済みファイルでもメモリに収まる必要はありません。次の例では、文字列 dGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw== を 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
}

同じ考え方は io::copy で1行に縮みます:ファイルを DecoderReader で包み、どのライターへでもコピーするだけ。デコードは移動経路で起きます。公式ドキュメントには、定数空間でペイロードを検証する、美しい小技もあります。静的にサイズ決めされたバッファを使い、デコード済みデータの割り当てはまったくしません:

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,   // 最後までエラーなしで読み終えた
      Ok(_) => continue,
      Err(_) => return false, // 何かは base64 じゃなかった
    }
  }
}
fn main() {
  println!("{}", is_valid_base64("SGVsbG8sIHdvcmxkIQ=="));  // true
  println!("{}", is_valid_base64("dt=="));                  // false
}

大きくてもあなたが管理するバッファに収まるペイロードには、スライス API がゼロ・サプライズの選択肢です:base64::decoded_len_estimate(len) が len 記号に対する保守的な最大デコードサイズを与え、decode_slice() は予割り当てバッファに直接書き、実際に書き込んだバイト数を正確に返します:

use base64::prelude::*;
let packed = "SGVsbG8sIHdvcmxkIQ==";
let cap = base64::decoded_len_estimate(packed.len());  // 15、保守的な最大値
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!

バッファが小さすぎたら、パニックの代わりにクリーンな DecodeSliceError::OutputSliceTooSmall が返ります。そして「小さすぎた」のはプログラマのエラーで、柔軟に回すよりその場でクラッシュしたい場所には、設計どおりパニックする decode_slice_unchecked() バリアントもあります。0.22.0 以降、スライスのチェックはあなたに有利な保守的になりました:出力が本当に収まらないときしか失敗しないので、ぴったりサイズのバッファは動作します。

デコードされた行きの先

Base64 が Rust プロジェクトに現れる頻度は、「ただのランダムな文字列」という印象が示すよりはるかに高いです:

  • API レスポンスとウェブフック:ペイロードの中で Base64 テキストとしてバイナリや入れ子 JSON を埋め込む、定番の JSON としてのファイルアップロードパターン。
  • JWT の検査:ヘッダーとペイロードをデコードしてクレームをのぞき見、実際に意味のある部分(署名)は jsonwebtoken に渡す。
  • メール型のデータ:MIME 添付と、メールシステムを経由したものすべて。空白のセクションが存在する理由がこれです。
  • 証明書やキーの PEM ブロック:すべての TLS スタックが噛み砕く -----BEGIN CERTIFICATE----- セクション。pem クレートが代わってパースし、ふさわしいことに、内部では base64 クレートの上に成り立っています。
  • Data URI:スクレイピングやレンダリング中の HTML と CSS に隠れている、data:image/png;base64,... 系のもの。
  • HTTP Basic 認証ヘッダー:Basic TWFuOnBhc3M= は、ただ変装した Man:pass にすぎません。
  • データベースと設定ファイル:テキスト列や環境変数のなかにバイナリを置きたかった誰かがいる。
  • 言語間の引き渡し:Python サービスが blob をパックし、Rust がunpackし、定義上、両側は同じアルファベットを話す。

JSON の場合、知っておく価値のあるショートカットがあります:serde_with クレート(バージョン 3)は構造体のフィールドを注釈できるようにし、serde が Base64 を両方向で扱います。出口で Vec<u8> フィールドをテキストにエンコードし、入口で戻してデコードし、URL 安全な変種はパラメータ1つの距離にあります:

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 URI は、2ステップの文字列操作の後に通常どおりデコードするだけです:コマを見つけ、その後の部分を保持し、メタデータが単語 base64 で終わるか確認します:

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

そしてこれらをすべて統べる唯一のルール。今も人が現場で捕まり続けるので、繰り返す価値があります:Base64 は梱包テープであって、鍵ではありません。暗号でもなく、圧縮でもありません - 圧縮の反対です - この記事を読んだ誰でも、それがしたことすべてを逆操作できます。自由にデコードし、選択的に信頼せよ。

慎重な歩みの10年

クレート自身の歴史は、ねじをゆっくり締め上げる記録のように読めます。crates.io に現れたのは2015年12月で、バージョン 0.5.0 は設定可能な行末と折り返し付きの MIME サポートを、誇らしげに追加しました。それから2018年のバージョン 0.10.0 が、折り返しと空白処理を削除します。汎用のクレートはデコードをして、詩の部分はアプリケーション層に委ねるべきだと、ライブラリが判断したためです。同じリリースはストリーミング・エンコーダーと、不正な終端記号の検出も追加しています。2022年のバージョン 0.20.0 はエンジン抽象化を導入し、パディングのデフォルトを反転させて、標準エンジンは正規パディングを要求するようになりました。0.21.0 はエンジンメソッドに置き換える形で、base64::decode() のような古いフリー関数を非推奨にし、コンパイラの注意書きは「Use Engine::decode」(それでも動きます。多くのレガシーコードが快くコンパイルできる理由がここにあります)。2024年、バージョン 0.22.0 はエラーの意味論を鋭くし、InvalidLength の意味を精緻化して、デコードを5〜10パーセント速くしました。そして2026年7月、バージョン 0.23.0 がやってくる - SIMD エンジン、カスタムなパディング記号、より明確な InvalidLastSymbol メッセージ、1.71 への MSRV 引き上げとともに。8月4日の 0.23.1 パッチは、非 SIMD アーキテクチャ向けのテストスイートを修正しました。小さな慎重な歩みの10年。「Base64 じゃないか。何を求める?」と出発したクレートが、今ではベクトル化カーネルを搭載しています。

フォーマットはウェブより古く、だからこそ厳格さが個人的に感じられるのです。1987年、Privacy-Enhanced Mail プロトコル(RFC 989)は 7ビットのメールチャネル上でバイナリデータを運ぶ必要があり、64文字行でこのエンコードを標準化しました。あなたの TLS スタックが今まで信頼してきたすべての -----BEGIN CERTIFICATE----- ブロックは、その決定の直接の子孫です。1996年、MIME 仕様(RFC 2045)がこの方式を採用し、64文字のアルファベットにちなんで「base64」と名付け、今日もあなたのメール添付を折り返し続ける 76文字の行長を決めました。2006年、RFC 4648 が誰もが引用する標準になります:アルファベットの表、第5節の base64url 変種、そしてこのクレートが見るからに愉しむ厳格さのルール。

お遊びの雑学

完全なガイドは笑顔で終わるべきなので:

  • 単語「base64」は YmFzZTY0 にエンコードされます。自らを記述するフォーマットとは、モールスで話す鏡の技術版です。
  • 空文字列は 0 バイトにデコードされますが、"AA==" は1バイト、つまり NUL にデコードされます。Base64 では「無」は「ゼロ」とは違う生き物です。
  • あなたが今まで見たすべての Base64 エンコード済み PNG は iVBORw0K で始まります。それは変装した PNG のマジックナンバーで、インターネットで最も認識されやすいプレフィックスの1つです。
  • YouTube の動画 ID はパディングなしの base64url です:8バイトの値は12個の Base64 文字になり、末尾のパディングを落とすと、URL のどこにでも貼れるなじみのある 11文字の ID が残ります。インターネット全体で、パディングなしモードの最も目に見える用途の1つです。
  • Bash は何年も前から 64 進で数を数えています:$((64#...)) という演算リテラルは、数字を 0-9、a-z、A-Z の順に、最後に 62 と 63 として @ と _ を受け取るので、あなたのシェルは 64 文字のアルファベットをあからさまに抱えています。
  • 古い crypt(3) パスワードハッシュは、アルファベットが ./ で始まる Base64 変種を使いました。それは美しい性質を持ちます:エンコード済み文字列をソートすると、元のバイトをソートしたのと同じ順序になるのです。系譜ファイルも同じアルファベットで埋め込みマルチメディアに使っていました(GEDCOM 5.5; 5.5.1 改訂版はこれを削除)。そして base64 クレートはそれを alphabet::CRYPT として同梱しています。
  • MIME の数学、古い指針が今も計算するように:折り返されたメールのペイロードは元のサイズのおよそ 1.37 倍のコストがかかり、さらに数百バイト規模のヘッダが加わります。1990年代のメールインフラは、本当にすべての添付にそのたびにこの関税を徴収していたのです。
  • クレート全体が #![forbid(unsafe_code)] ですが、デフォルトで有効な simd-unsafe フィーチャーだけは例外で、しかもこれは入れるのではなく抜く側の選択です。1つの単語「unsafe」が、フィーチャーフラグの名前なのです。
  • Base64 は、セキュリティ研究者を怯えるほど変形可能です:同じバイトはパディングあり・なしで書け、終端ビットにゴミを入れたままでも書け、寛容なデコーダーはそれに気づきません。2022年の論文が現実世界の結果を実証したので、このクレートの厳格なデフォルトはボディガードのように感じられます。
  • Base64 は暗号ではありません。もしそうだったら、この記事の例の出力は読めなかったでしょう。それは窓際の席であって、金庫ではありません。

まとめ

エンジンは、データが行く先によって選びましょう:自分が作り、自分で制御するすべてには BASE64_STANDARD、出所の混ざった境界には STANDARD_PAD_INDIFFERENT、トークンと URL には URL_SAFE_NO_PAD、プロトコルが独自のルールを要求するときは手づくりの GeneralPurpose。4つの DecodeError バリアントに正確な文句を言わせ、バイトをテキストと呼ぶ前に std::str::from_utf8() の門番を通過させ、大きなものは DecoderReader でストリーミングし、除去するのは期待する空白だけにして、タイミングが脅威になるなら base64ct に手を伸ばす。すべてをデコードし、検証されるものだけを信頼してください。そしてある日、逆方向 - 開封ではなく、道中用にバイトを文字列へパックする - が必要になったら、姉妹の記事が Rust でのエンコードをサイズ計算からストリーミングの仕上げまでカバーしています。

最終更新: 2026-10-09

関連記事: Rust での Base64 エンコード:完全ガイド