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

Swift での Base64 エンコード:完全ガイド

送り出したいものがあり、道はテキスト専用:生のバイトを拒む JSON API、7ビットの出身を覚えているメールのチャネル、名乗れないものにはつまずく URL、最も素朴な文字しか受け入れない設定ファイル。base64 の詰め側へようこそ。Swift は1つのメソッド呼び出しで、あなたのバイトを気持ちのいい文字の壁に変えてくれます:3バイトごとに1文字余分に載る手数料、そして2つの異なる10年がそれぞれ行の長さに意見を持ったために存在する、いくつかのラップオプション。

このサイトのホームページでは、フォーマットの詳細(64個の表示可能な文字、入力3バイトあたり4文字、最後のグループには最大2つの = のパディング)はすでに説明してあるので、フォーマットの講義は始まる前に終わっています。この記事に持って行くべき2つの事実:base64 は詰め物であり、鍵ではない、そして詰めはデータを約 33% 膨らませる、そしてそれはサイズの上限に近づいていれば毎回問題になります。Swift では、仕事全体が1つの型 Data と、1つの全メソッド base64EncodedString(options:) を通って回ります。本当に必要な唯一のスキルは、そのメソッドの周りの2つのステップで何が起こるかを知られていることです。メソッドそのものは決して失敗しないからです。失敗するのはステップの方です。

1つのメソッド、言い訳はゼロ

Swift の base64 に関係するものはすべて、Foundation フレームワークの Data に住んでいて、言語の最初のリリースからずっとそこにいます(Apple はこのメソッドを iOS 8.0, macOS 10.10, tvOS 9.0, watchOS 2.0, visionOS 1.0 からと挙げています)。パイプラインは常に同じ3ステップです:内容を Data に入れる、メソッドを呼ぶ、文字列を送り出す。

import Foundation

let note = "Pack it, wrap it, ship it."
let packed = Data(note.utf8).base64EncodedString()
print(packed) // UGFjayBpdCwgd3JhcCBpdCwgc2hpcCBpdC4=

その3行の中には、よく見る価値がある詳細が2つあります。第一に、Data(note.utf8) が静かなステップです:utf8 ビューは定義上、すべての Unicode スカラを表せるので決して失敗しません。そのためほとんどの例でデフォルトになっています。失敗可能な親戚の note.data(using:) は、いくつかのエンコーディングに対して nil と答えることができますし、実際にそうします。「どのバイトか」という決定全体は、データが消えうる最初の場所なので、下で専用のセクションをもらいます。第二に、メソッドそのものは全です:常に答えを返し、エラーケースは持たず、あなたに聞く唯一のことは、どの行ラップが欲しいかです。兄弟の base64EncodedData(options:) もいて、文字列の代わりに ASCII バイトの Data として詰められた結果を返します。次の停留所がテキストフィールドではなくバイナリ API のようなパイプラインのために。

そして半数のあなたが「Swift アプリに base64 の依存が必要だ」という道から来たなら:インストールするものは何もありません。Base64 は Foundation の一部で、Foundation はツールチェーンの一部で、ツールチェーンは全プラットフォームで同じ形で届きます。macOS では Xcode かコマンドラインツール、Linux と Windows では swift.org のインストーラー(執筆時点の現在の安定版ラインは 6.3.x で、推奨される入口は Swiftly というバージョンマネージャー)、そしてコンテナ勢は公式 Docker イメージがカバーします。あなたの Package.swift は空のままです。そのはずです。

最初の本物の決断:どのバイトか?

base64 の文字が1文字も生まれる前に、あなたはずっと最も重要な決断を下しています。なぜなら、base64 が詰めるのはバイトで、文字列はあなたがそのバイト形式を選ぶまで、ただの文字列にすぎないからです。UTF-8 が健全なデフォルトであり、ほぼすべての場合の正しい答えですが、データがレガシーシステム、バイナリプロトコル、Unicode の端っこから来た瞬間に、その選択はもはや背景に溶け込みなくなります:

import Foundation

let phrase = "héllo"
print(phrase.data(using: .utf8)?.count ?? -1)              // 6
print(phrase.data(using: .ascii) == nil)                   // true
print(phrase.data(using: .utf16)?.count ?? -1)             // 12
print(phrase.data(using: .utf16LittleEndian)?.count ?? -1) // 10
print(phrase.data(using: .utf8)!.base64EncodedString())
// aMOpbGxv
print(phrase.data(using: .utf16LittleEndian)!.base64EncodedString())
// aADpAGwAbABvAA==
変換 「héllo」のバイト数 base64 が運ぶもの
.utf8 6 aMOpbGxv、モダンな API が期待する表記
.ascii nil で失敗 アクセント付き文字は 0x7F より上にあり、ASCII はそれを拒否する
.utf16 12 UTF-8 の2倍の大きさ、さらに2バイトのバイトオーダーマークが先頭に同乗
.utf16LittleEndian 10 BOM タグなしの同じ単語:10バイト、ここに列挙した BOM なしのオプションの中では依然として最重量

その出力には3つの教訓が隠れています。data(using:) の形は失敗可能で、.ascii は失敗の有力候補なので、それを強制アンラップすることは、完全に健全なフレーズがクラッシュしたアプリになる方法です。素の .utf16 変換は2バイトのバイトオーダーマーク(FF FE、リトルエンディアンマシンでは)を先頭に付け、その BOM は詰められた出力の中へ旅立ち、それを期待していないあらゆるデコーダーを混乱させます。そしてサイズの足し算は容赦がありません:不注意な文字セットの選択は、2倍のデータに対して base64 手数料を払わせます。だから問いは「これでエンコードできるか」ではなく、「向こう側は開けたとき、何を見出すことを期待するのか」です。黄金律:旅の両端は、base64 が始まる前にバイト形式で合意しておかなければなりません。デコーダーにはあなたが選んだものを推測する手段がなく、聞いてもこないからです。

ラップ:2つの癖、1つのパラメータ

このメソッドのオプションはすべて行の折り返しについてで、それらすべてが存在するのは、20世紀の2つのフォーマットが文字の行をどれくらいの長さにすべきかで折り合いをつけられなかったからです。MIME、つまり1996年のメール標準は、base64 を76文字、CRLF 改行で折り返します。PEM、1987年の Privacy-Enhanced Mail の系譜は64文字で折り返し、それが証明書と鍵の中にある形です。サーバーが設定ディレクトリに保管している -----BEGIN CERTIFICATE----- ブロックの、あの形です。

import Foundation

let certBytes = Data((0..<300).map { UInt8($0 % 256) })
let raw = certBytes.base64EncodedString()
let pemStyle = certBytes.base64EncodedString(options: [.lineLength64Characters, .endLineWithLineFeed])
let mimeStyle = certBytes.base64EncodedString(options: [.lineLength76Characters,
  .endLineWithCarriageReturn, .endLineWithLineFeed])
print(raw.count)                                        // 1行で400文字
print(pemStyle.components(separatedBy: "\n").count)    // 最大64の7行
print(mimeStyle.components(separatedBy: "\r\n").count) // 最大76の6行
オプション 仕事 注意
.lineLength64Characters 64文字の後に1行を切る、PEM の癖 別指定がない限り行末は CRLF
.lineLength76Characters 76文字の後に1行を切る、MIME の癖 同じ CRLF デフォルト
.endLineWithCarriageReturn 行末にキャリッジリターンを含める 単体では CR のみ、旧 Mac 風。あなたが欲しがっていることはめったにない
.endLineWithLineFeed 行末にラインフィードを含める CRLF を意味するなら、両方のオプションを渡す

では、人々を驚かせるデフォルトの話:.lineLength オプションのどれかを、行末を選ばずに求めると、受け取る行末はCRLF、つまりキャリッジリターン+ラインフィードのフルペアです。このメソッドにはハウススタイルがあり、そのハウススタイルは1996年です。LF のみがいいなら、.endLineWithLineFeed を、それだけを選んで明示的に指定してください。記録に残すべきもう1つのハウスルール:最後の行には末尾改行が付きません。ラップされた結果は、どのオプションを選んでも、最後のデータ文字か = パッドで終わるので、末尾に孤児のような空行を残さず結合・貼り付けできます。そしてオプションをまったく付けないと、出力は1本の途切れぬ行になり、それが JSON ボディ、URL、API ペイロードにとっての正しい形です:モダンな Swift アプリが実際に大部分の時間でやっている仕事です。

Base64url:旅ができる文字列

標準アルファベットは JSON では立派な市民ですが、URL では最悪の市民です。クエリ文字列の中では、+ はフォーム解析にスペースとして読まれ、/ はパスの区切りで、= はキーと値を分けます。だからこそ、標準アルファベットをパーセントエンコードすると、短くなるどころか長く、汚くなります。RFC 4648 のセクション5は、まさにそれを直すために存在します:「URL 安全・ファイル名安全アルファベット」。ここで + は - に、/ は _ に変わります。= のパディングはたいてい落とされ、URL 中のパッドは普通 %3D になって目的を台無しにするためです。RFC は額に掛ける価値のある警告を追加しています:このエンコーディングは「base64 エンコーディングと同じだとみなすべきではない」。YouTube の動画 ID、JWT、そして大多数のモダンな API 識別子はこれを話します。使うことを覚悟してください。

import Foundation

extension Data {
  var base64URLEncoded: String {
    base64EncodedString()
      .replacingOccurrences(of: "+", with: "-")
      .replacingOccurrences(of: "/", with: "_")
      .replacingOccurrences(of: "=", with: "")
  }
}

let tricky = Data("The + / and = trio goes home.".utf8)
print(tricky.base64EncodedString())
// VGhlICsgLyBhbmQgPSB0cmlvIGdvZXMgaG9tZS4=
print(tricky.base64URLEncoded)
// VGhlICsgLyBhbmQgPSB0cmlvIGdvZXMgaG9tZS4

その出力をよく見てください:この特定のペイロードはたまたま + も / も生成しなかったため、2つの綴りは落とされたパディングだけで違います。1バイト変えればアルファベットで分かれます - それがすべてです。交戦ルールを2つ。方言は1度だけ選び、あなたのデータが外の世界に会う境界で選び、同じドキュメントの中でアルファベットを絶対に混ぜない:base64url を受け取った標準デコーダー(あるいはその逆)は、入力を拒否するか、寛容なモードでは異国の文字を削除して、間違ったバイトを手渡します。そしてヘルパーには正直な名前を付け、次の開発者がその文字列は base64url であって誤字ではないことを分かるようにする。同じエクステンションは、新しいツールチェーンでは短くなれます:最新の SDK ベータには現在、フレームワークの中でアルファベット交換をするネイティブの .base64URLAlphabet オプションが含まれ、対応する .omitPaddingCharacter オプションもあります。オープンソースの Foundation は、後方のツールチェーン向けの可用性マーカーの向こうで同じオプションを持っています。それがあなたの最低デプロイ目標に届くまで、4行のエクステンションこそが移植可能な答えで、構造上、あらゆるプラットフォームで動き続けます。

JSON と API:求めたことのない Base64

Codable を扱う人の中で、これが一番驚かされます。だからこそ専用のセクションをもらいました:JSONEncoder の Data プロパティへのデフォルト戦略は、すでに base64 なのです。Codable 構造体が Data フィールドを持っていれば、エンコーダーは標準 base64 で自動的にそれを詰め、JSONDecoder は帰りの道で自動的にそれを解きます。オプションも、設定も、儀式もない。

import Foundation

struct Snapshot: Codable {
  let name: String
  let icon: Data
}

let snap = Snapshot(name: "cat", icon: Data("🐱".utf8))
let json = try JSONEncoder().encode(snap)
print(String(decoding: json, as: UTF8.self))
// アイコンは「8J+QsQ==」としてワイヤーを渡った

icon プロパティが 8J+QsQ== としてワイヤーを渡ったのは、それがハウススタイルだからです。代替手段があり、実際に会うのは2つ:データとエンコーダーを手渡して、表現をあなたが決める .custom、そして新しい .deferredToData、これはデータインスタンスそのものに委ねます。API が標準ではなく base64url を望んだ瞬間、.custom が、前セクションのエクステンションを差し込む場所です:

import Foundation

extension Data {
  var base64URLEncoded: String {
    base64EncodedString()
      .replacingOccurrences(of: "+", with: "-")
      .replacingOccurrences(of: "/", with: "_")
      .replacingOccurrences(of: "=", with: "")
  }
}

struct Snapshot: Codable {
  let name: String
  let icon: Data
}

let encoder = JSONEncoder()
encoder.dataEncodingStrategy = .custom { data, enc in
  var container = enc.singleValueContainer()
  try container.encode(data.base64URLEncoded)
}
let json = try encoder.encode(Snapshot(name: "cat", icon: Data("🐱".utf8)))
print(String(decoding: json, as: UTF8.self))
// アイコンは「8J-QsQ」としてワイヤーを渡った

動作する機能と本番インシデントを分ける警告を1つ:JSON 文字列は生の改行を含めません。.lineLength オプションでペイロードをラップして、結果をエスケープもせずに JSON ドキュメントに補間したら、あなたは JSON 値を作ったのではありません。base64 アクセント付きの構文エラーを作っただけで、パーサがそれを証明してくれます。ラップされた出力の住処は、メールボディと証明書ファイルです。JSON、URL、クエリ文字列の中に住むものはすべて、素のラップなし文字列を受けます。

Data URI:文字列の中の画像

Web のお気に入りの芸は、ファイルのバイトを URL に直接埋め込むことです:data:{mime};base64,{payload}。Swift でそれを作るのは、読み取り、エンコード、文字列結合です:

import Foundation

let gif = Data(base64Encoded: "R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")!
let uri = "data:image/gif;base64," + gif.base64EncodedString()
print(uri.hasPrefix("data:image/gif;base64,R0lGODlh")) // true
print(gif.count) // 42

この例は、有名な42バイトの透過 GIF(小さい非透過のものもありますが、誰もが埋め込んでいるのはこれです)を、ブラウザが2回目のリクエストなしで描画する data URI に組み直しています。Apple プラットフォームでは、逆方向は1行です:詰めたばかりの同じ Data がそのまま UIImage(data:) や NSImage(data:) に渡せる。トレードオフはサイズで、それは複利で増えます:100キロバイトの画像は、data:image/png;base64, プレフィックスを加える前に、133,000文字超の文字列になります。Data URI はアイコン、アバター、小さなアセットで輝き、ヒーロー写真では静かに帯域を膨らませるので、小さいものだけに留めておきましょう。

JWT:最初の2つのパートを封印する

JSON Web Token のエンコード側は、2回の封印と1つの署名で、その封印はパディングを落としたあなたの base64url エクステンションです - このフォーマットがまさに要求するもの。ヘッダーもペイロードも JSON ドキュメントで、両方のパートに同じ扱いをします:

import Foundation

extension Data {
  var base64URLEncoded: String {
    base64EncodedString()
      .replacingOccurrences(of: "+", with: "-")
      .replacingOccurrences(of: "/", with: "_")
      .replacingOccurrences(of: "=", with: "")
  }
}

func seal(_ text: String) -> String {
  Data(text.utf8).base64URLEncoded
}

let header = seal(#"{"alg":"HS256","typ":"JWT"}"#)
let claims = seal(#"{"sub":"42","role":"editor"}"#)
print("\(header).\(claims).signature-here")
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsInJvbGUiOiJlZGl0b3IifQ.signature-here

2つの注意。3番目の点区切りパートは、最初の2つの上で計算される暗号署名で、トークンの中で保証を提供するのは唯一このパートです:ヘッダーもクレームもトレンチコート姿の素の JSON なので、秘密がそこにいくことは決してない。そして seal() でパディングが消えていくのに注目してください:向こう側の JWT デコーダー(姉妹記事の中のそれも含む)は、剰余の補填でそれを元に戻すので、旅の2つの方向は共通の地でお目にかかるのです。

HTTP ヘッダー:Basic と残りの仲間

古い Authorization: Basic ヘッダーは、コロンで結ばれたユーザー名とパスワードを、標準 base64 で詰めてほしいと言っています。ヘッダーの中では + と / は無害で、方言の問題は発生しないからです:

import Foundation

let credentials = "editor:s3cret"
let header = "Basic " + Data(credentials.utf8).base64EncodedString()
print("Authorization: " + header)
// Authorization: Basic ZWRpdG9yOnMzY3JldA==

どこでも同じ大きな脚注:詰め物だけではセキュリティはゼロで、ヘッダーはそれを運ぶ HTTPS 接続と同じだけ安全です。モダンな親戚の Authorization: Bearer は代わりに JWT を運びます。だからワイヤーを走る的是 JWT セクションの封印レシピです。HTTP で方言の問題が実際に発生するのは1か所だけ、クエリ文字列です:API が識別子を URL に乗せるなら、その識別子は base64url であるべきで、少なくともパーセントエンコードされた標準 base64 であり、+ がスペースとして読まれるまま放置された素の標準アルファベットであってはなりません。

メールの添付ファイル:76文字の契約

アプリが SMTP の 7 ビット出身を生き延びなければならない添付ファイルを作る場合、契約は MIME のものです:CRLF 改行で 76 文字ごとに折り返された base64、そして受信者に何を期待すべきかを教えてくれる Content-Transfer-Encoding: base64 ヘッダー。オプションのセクションではすでに綴りを見せました。ここはラップされたボディの完全な形です:

import Foundation

let attachment = Data((0..<400).map { UInt8(65 + $0 % 26) })
let body = attachment.base64EncodedString(options: [.lineLength76Characters,
  .endLineWithCarriageReturn, .endLineWithLineFeed])
let lines = body.components(separatedBy: "\r\n")
print(lines.count)                     // 8行
print(lines.map { $0.count }.max() ?? 0) // 76、最長
print(body.hasSuffix("\r\n"))           // false、最後の行は裸のまま

この方言のサイズの請求書は、有名なものです:4/3 のアルファベット税に加えて 76 文字ごとの改行が、元の約 137%まで連れて行きます。そして古いメール工学のショートカット「元を 1.37 倍して、ヘッダーの約 800 バイトを足す」は、メールクライアントで添付ファイルのサイズを目測するにはまだ機能します。これは正しい足し算を持つ伝説で、この記事の中で 33% の手数料が小数第2位を生む唯一の場所です。

設定、環境、データベース:アンダースコアを隠す

base64 の唯一の徳が、出力が小さく予測可能な文字セットであることに尽きる、静かな仕事のクラスがあります:生のテキストを望む場所に、バイナリの塊や構造化された値をしまう。シェルの設定ファイルで生き延びなければならない環境変数、blob より varchar の方が幸せなデータベースの列、base64 マーカーを持つ LDAP ファイル、ビットより文字の方が確実にスキャンされる QR コード。パターンはどこでも同じです:バイトを決める、エンコードする、文字列を保存する、向こう側でデコードする。

import Foundation

struct FeatureFlags: Codable {
  var betaToolbar: Bool
  var maxRetries: Int
}

do {
  let flags = FeatureFlags(betaToolbar: true, maxRetries: 5)
  let json = try JSONEncoder().encode(flags)
  let storable = json.base64EncodedString()
  print(storable)
  guard let packed = Data(base64Encoded: storable) else {
    print("decode failed, that is odd")
    exit(1)
  }
  let restored = try JSONDecoder().decode(FeatureFlags.self, from: packed)
  print(restored.betaToolbar, restored.maxRetries)
} catch {
  print(error)
}

2つの落とし穴がここに住んでいます。1つ目は二重ラップ:どちらも「親切に」エンコードする2つの統合レイヤー。あなたが保存する値は base64 の base64 になり、1回だけデコードする読み手は文字の壁を受けて、機能が壊れたと思う。ちょうど1回だけ、ちょうど1つの境界でエンコードし、コメントにそう書いておくこと。2つ目は環境による方言の漂流:その値が URL、フォームフィールド、あるいは + と / を壊すシェルを通過することがあるなら、代わりに base64url の綴りを保存して。文字セットこそがこのフォーマットのすべてだからです。

ファイル:.b64 のラウンドトリップ

「このファイルを .b64 テキストファイルにしよう」の仕事は、読み取り、呼び出し、書き出しです:

import Foundation

let source = URL(fileURLWithPath: "photos/cat.png")
let archive = URL(fileURLWithPath: "photos/cat.b64")
let bytes = try Data(contentsOf: source)
try Data(bytes.base64EncodedString().utf8).write(to: archive)

// 後で、おそらく別のプロセスで
let packed = try String(contentsOf: archive, encoding: .utf8)
let restored = Data(base64Encoded:
  packed.trimmingCharacters(in: .whitespacesAndNewlines))
if let restored = restored {
  try restored.write(to: URL(fileURLWithPath: "photos/cat-copy.png"))
} else {
  print("the .b64 file was not base64 after all")
}

帰り道の trimmingCharacters は、ファイルを書き出したものが改行を追加したかもしれないからそこにあります。そして厳格なデコーダーは末尾の改行を nil 判定と扱うからです。そのラウンドトリップはバイト単位で同じものに戻るので、初めて出荷するときに検証すべきです。メモリ使用量が気になるほど大きいファイルには、バッファ全体を一度にエンコードしないでください。Base64 には、ストリーミングを正確にする愛らしい性質があります:入力3バイトが4つの独立した出力文字を生むので、エンコードする各チャンクが3バイトの倍数である限り、結合された出力はファイル全体を一度にエンコードした場合と同一です。整列を崩すと出力が変わります。チャンク境界がストリームの途中で3バイトグループを分割するからです:

import Foundation

func streamEncode(_ input: InputStream, output: OutputStream, lineLength: Int = 76) throws {
  input.open()
  output.open()
  defer { input.close(); output.close() }
  var buffer = [UInt8](repeating: 0, count: 65_536)
  var pending = [UInt8]()
  var line = ""
  var lineCount = 0
  func addText(_ text: String) {
    line += text
    while line.count > lineLength {
      if lineCount > 0 { _ = output.write(Array("\r\n".utf8), maxLength: 2) }
      _ = output.write(Array(String(line.prefix(lineLength)).utf8), maxLength: lineLength)
      line = String(line.dropFirst(lineLength))
      lineCount += 1
    }
  }
  func flushGroup(_ group: [UInt8]) {
    addText(Data(group).base64EncodedString())
  }
  while input.hasBytesAvailable {
    let n = input.read(&buffer, maxLength: buffer.count)
    if n < 0 { throw CocoaError(.fileReadUnknown) }
    if n == 0 { break }
    pending.append(contentsOf: buffer[0..<n])
    let groups = pending.count / 3
    if groups > 0 {
      flushGroup(Array(pending[0..<(groups * 3)]))
      pending.removeFirst(groups * 3)
    }
  }
  if !pending.isEmpty {
    flushGroup(pending)
  }
  if !line.isEmpty {
    if lineCount > 0 { _ = output.write(Array("\r\n".utf8), maxLength: 2) }
    _ = output.write(Array(line.utf8), maxLength: line.utf8.count)
  }
}

ピークメモリは、ファイルがどれほど大きくても、読み取りバッファ1つと現在の行です。そしてラップされた出力は、一括の .lineLength76Characters 綴りと完全に一致します。同じ3の倍数ルールを役割が逆にしたものが、姉妹記事のストリーミングデコーダーが頼るもので、旅の2側が1つの算術の真実を共有しています。

大きなペイロードとメモリの請求書

次に誰かが「これ、base64 にできる?」と聞いてきたときに必要な足し算をしましょう。入力3バイトが出力4文字になるので、サイズは 4/3 倍になります:100キロバイトのファイルは 133,336 文字の文字列に、10メガバイトのファイルは 13,333,336 文字に、そして続きます。パディングは最後に最大2文字足りますが、数バイトより大きいものでは丸め誤差にすぎず、唯一の例外は空の入力で、そこでは税務署が無料パスを1枚発行し、結果は空文字列です。実践的な結果が3つ。第一に、始める前に予算を立てる:ペイロードがすでに制限の近くにあるなら(URL の約 2,000 文字の快適圏、JSON フィールドの契約、データベース列の幅)、エンコードの前に、後ではなく、制限を 1.33 で割る(ラップが関わるなら 1.37 で)。第二に、詰めながら、元のバイトと詰められた文字列を同時に手にしているので、ワーキングセットは元の約 2.33 倍になり、その数字が快適でなくなったときの抜け道が、上のストリーミング関数です。第三に、税は実際には片道です:詰めたときに払い、誰かが解いたときにあなたのバイトは帰ってきます。だから本当の問いは「base64 は高価か?」ではなく、「私がいるテキスト専用の道がそれを要求するのか?」です。

咬みつくミス

  • 失敗可能性のある文字セットステップ。 String.data(using:) は nil と答えることがあります(アクセント付き文字で .ascii を試してみてください)。それを強制アンラップすることは、悪い入力をクラッシュしたアプリに格上げる古典的な方法です。base64 の呼び出し - 簡単な方 - ではなく、変換をガードしてください。
  • CRLF のハウススタイル。 行末オプションのない .lineLength オプションは、デフォルトで CRLF を生成します。フォーマットが LF のみを望んでいてオプションを忘れたなら、出力にはあってはならないキャリッジリターンが乗っています。
  • CR のみの罠。 .endLineWithCarriageReturn 単体は、旧 Mac 風の CR のみの行末を生成します。CRLF を意味するなら(MIME の場合はそうでしょう)、両方の行末オプションを渡してください。
  • JSON の中のラップ。 JSON 文字列の中の生の改行は、完全に無効な JSON です。ドキュメントに補間されたラップ済み base64 は、base64 アクセント付きの構文エラーです。ラップされた出力はメールボディと証明書ファイルに留めておいてください。
  • BOM のヒッチハイカー。 素の .utf16 変換は2バイトの BOM を先頭に付け、それは詰められた出力の中へ旅立ち、それを期待していないデコーダーを混乱させます。タグなしの UTF-16 が必要なら .utf16LittleEndian か .utf16BigEndian を使ってください。
  • 方言の漂流。 標準と base64url は異なるアルファベットで、RFC はそれを文字通り書面で言っています。クエリ文字列まで生き延びた + はスペースになり、寛容な標準デコーダーに届いた - は削除されます。境界で方言を選び、それを保ってください。
  • 大文字小文字は文字である。 アルファベットは A と a を区別します。大文字小文字を揃えられたコピー&ペーストや、意気揚々とした大文字化の呼び出しは、両方のバージョンが依然としてすべてのアルファベットチェックを通過するため、静かにデータを壊します。Base64 はパスポート番号と同じで大文字小文字を区別します。
  • 整列ルール。 ストリーミングエンコーダーは、3バイトの倍数でチャンクを切らなければなりません。整列していないチャンクは出力を変え、その変化は静かです:文字列は依然としてデコードされます。ただし、間違ったデータに。
  • 二重ラップ。 両方がエンコードする2つのレイヤーは、base64 の base64 を生みます。1回だけデコードする読み手は、バイトであるべき場所に文字を見、インシデントは自ら書き上がります。
  • 可用性の壁。 新しいネイティブオプション(.base64URLAlphabet、.omitPaddingCharacter)は最新の SDK ベータと、オープンソース Foundation の可用性マーカーの向こうに存在しますが、あなたの CI が触れるすべてのツールチェーンにあるわけではありません。採用するなら、同じソースが古い Xcode と Linux でもビルドされるよう、可用性チェックでガードしてください。現在の安定ツールチェーンでは、4行エクステンションはオプションのない場所にどこでもコンパイルできます。
  • Base64 は暗号化ではない。 要求が機密性なら、あなたはまるごとカテゴリ違いで間違った工具を選んでいます。Base64 の仕事はバイトを旅させることで、それはまさにその仕事をやるだけです。それ以上でもありません。

送り出す方法

  • エンコードするのはバイトであって、願い事ではない。 メソッドを呼ぶ前にバイト形式を決め、デフォルトは UTF-8、そうでないときは明示的に名を上げ、失敗可能性のある data(using:) ステップをガードする。データが実際に消えるのはそこだから。
  • デフォルトはラップなし、契約でラップ。 素の1行出力は、JSON、API、大多数のデータベースで正しい。受信側のフォーマットが要求したときだけ 64/76 のラップオプションに手を伸ばし、CRLF を意味するなら両方の行末オプションを払う。
  • 境界ごとに1つの方言。 テキスト中心の行き先には標準 base64、URL やファイル名に触れるものには base64url、2つを同じドキュメントに絶対に入れない。変換は1回書いて、正直な名前を付けて、再利用する。
  • 手数料に予算をつける。 始める前に 4/3 倍で(ラップが関わるなら 1.37 倍で)、ペイロードがワーキングセットを快適でなくするほど大きいときは、3バイト整列のチャンクでストリームする。
  • 梱包テープを鍵として使わない。 要求が秘密性なら、base64 の棚の前で立ち止まり、代わりに暗号化を取る。

梱包の短い歴史

あなたが詰めに使うアルファベットと、ラップする行長は、テキスト専用の道でどれだけのバイナリが生き延びられるかという40年にわたる議論の化石で、Swift がその歴史の中で占める場所は短いですが興味深いものがあります:

  • 1980年代、同じ機械の時代。 この系列の最初のエンコーダーは、向こう側が自分たちのような機械だと仮定したシステム間で、ダイヤルアップを越えてファイルを動かすために存在していました。UNIX の uuencode は大文字・数字・記号を使い、設計者たちは計算力を節約する裏技を見つけました:アルファベットが連続した ASCII 位置に並んでいるので、エンコードは文字通りルックアップテーブル不要の「32 を足す」だったのです。1981年に TRS-80 で生まれた親戚の BinHex は Apple II に飛び移り、1984年にクラシック Macintosh のフォーマットとなり、異なる賭けをしました:その64文字は 7、O、W、g、o、そして小文字のほぼ半分を省いています。
  • 1987年、アルファベットに住所が付く。 最初の Privacy-Enhanced Mail 仕様の RFC 989 は、あなたが今日タイプしているまさにその64文字を標準化し、出力を1行64文字で折り返し、= をパディングに、* を「エンコードされたが暗号化されていない」データの印に使いました。あなたがサーバー設定に貼り付けてきたすべての PEM 風ブロックは、この文書の子孫です。
  • 1996年、寛容の時代。 MIME(RFC 2045)はアルファベットをメールの添付ファイルに持ち込み、折り返しを76文字に移し、ラップを安全に生成可能にしたルールを追加しました:デコーダーは改行を無視すべきだ。エンコーダーは折り返すことを学び、デコーダーは許すことを学びました。Swift の76文字オプションは、まさにこの議論の生きた記念品です。
  • 2003年から2006年、ルールが固まる。 RFC 3548(2003)は、パディングを省略してはならない(フォーマットが別と定めない限り)と、デコーダーはアルファベット外の文字を拒否すると宣言しました。RFC 4648(2006年10月)がこの系列を決着させ、URL 安全アルファベットを追加したのは、長い識別子が特殊文字をすべてパーセントエスケープせずに URL で暮らせるように、と明示するためです。「URL 方言にはパディングなし」という慣例は同じ文書で生まれ、URL 中のパッド文字は普通 %3D になって目的を台無しにするためです。
  • 2013年から2014年、API はもうここにいる。 Apple の NSData クラスは何年も base64 を詰めてきました。4つのラップオプションを持つオプションベースの API は iOS 7、つまり 2013 年に到着し、Swift が存在する前のできごとです。Swift 1.0 が 2014年9月9日に到着すると、4つのラップオプションを持つ全エンコーダーと、1987年からの64文字アルファベットを引き継ぎ、性格はそれ以来変わっていません。
  • 2015年12月3日、ツールチェーンがビルディングを去る。 Swift はその日にオープンソース化され、Foundation の base64 もそれに伴って Linux、そしてその後 Windows に渡りました。「Apple マシンを離れての Swift による Base64 エンコード」の歴史はまだ10年にも満たない:1987年に始まったパーティーに、まだ幼い客として来たものです。
  • 2023年から2026年、書き換えと URL 方言。 Foundation の書き換え(swift-foundation プロジェクト)は Data を純粋な Swift のコアへ移し、2025年にはコミュニティのピッチがネイティブの base64url とパディング省略オプションを追加しました。執筆時点では、最新の SDK ベータとオープンソースのツールチェーンがエンコードオプションを搭載し、系列の残りはオープンソース Foundation の可用性マーカーの向こうで成熟中であり、その間、コミュニティエクステンションが移植可能な橋渡し役のままでいます。

小さな愉しみ

  • 1メガバイトはちょうど 1,333,336 個の base64 文字に詰められます。4/3 の税にパディング文字2つ、桁単位まで。税をまったく払わない唯一の入力は空のもので、何もない入りに、何もない出です。
  • エンコーダーは、デコーダーにはない意味で全です。nil を返すこともなく、throw することもなく、拒否することもありません。パイプライン全体で唯一の失敗は上流、文字セットのステップに住んでいます。だからこそメソッドは、その親戚とは違ってずっと穏やかに感じられるのです。
  • 単語 héllo を UTF-8 でエンコードすると aMOpbGxv に、UTF-16 リトルエンディアンでエンコードすると aADpAGwAbABvAA== になります。同じ単語、2つの異なるパスポート、両方とも有効、互換は不可能。
  • base64 世界のテスト単語は foobar で、それは Zm9vYmFy に詰められます。実世界で base64 の例を見たことがあるなら、foobar が関与していた可能性は十分にあります。
  • 有名な 1x1 の透過 GIF は42バイトで、魔法の言葉 GIF89a で始まります。だからこそプレフィックス R0lGODlh は、地球上のほぼどの base64 文字列よりも多くのコードベースに顔を出しています。
  • あなたの Codable 構造体は、気づかぬうちに何年も base64 を送り続けてきたはずです:JSONEncoder のデフォルトの Data 戦略は標準 base64 で詰めるので、Data フィールドは数字の配列ではなく、パディング付きの文字列としてワイヤーを渡るのです。
  • パディングは2文字を超えません。絶対に。1バイトのペイロードは == で終わり、2バイトのペイロードは = で終わり、3バイトのペイロードはなにもなしで終わります。最後のグループの文法全体は爪に収まります。
  • Swift は、自分が詰みに使うアルファベットより27歳若いのです。言語は 2014 年に発売され、その64文字は 1987 年に標準化されてから変わっていません。

これぞ詰めもののツールボックス全体です:1つの全メソッド、それに先立つ1つの失敗可能なステップ、CRLF のハウススタイルを持つ4つのラップオプション、4行の base64url エクステンション、ストリーミングのための3バイト整列ルール、そしてテキスト専用道の入場料である 4/3 の手数料。エンコードは base64 の請求書をあなたが払う場所で、あなたは署名する前にすべての内訳を知っています。旅を反転させて、他の人が詰めたものを開き始めるとき、nil が戻り、空白の判定が、寛容なノブの盲点が舞台に上がります。関連するデコード記事は、このラウンドトリップの片側で完全なショーを回しているので、文字たちが届き始めるとき、あなたはすでにそれらを開く方法を正確に知っています。

最終更新: 2026-10-09

関連記事: Swift での Base64 デコード:完全ガイド