ファイルを選択
  1. base64hub
  2. 各言語での Base64
  3. Perl
コード

Perl での Base64

MIME::Base64 は何十年も前から Perl に同梱されており、これだけで十分です。デフォルト設定がメール向けになっているため、驚かれることがあります。以下の出力は Perl 5.40 と MIME::Base64 3.16 のものです。

文字列のエンコードとデコード

use strict;
use warnings;
use utf8;
use MIME::Base64 qw(encode_base64 decode_base64);
use Encode qw(encode decode);

my $enc = encode_base64(encode('UTF-8', 'héllo wörld'), '');
print "$enc\n";                                   # aMOpbGxvIHfDtnJsZA==

print decode('UTF-8', decode_base64($enc)), "\n"; # héllo wörld

Perl は文字列とバイト列を区別しており、encode_base64 が求めるのはバイト列なので、テキストは先に Encode::encode('UTF-8', ...) を通します。U+00FF を超える文字を含む文字列は Wide character in subroutine entry で die します。é のような Latin-1 の文字だけを含む文字列は、何の警告もなく UTF-8 ではなく Latin-1 としてエンコードされます。

引数の '' については次のセクションで説明します。

行末の引数

encode_base64($bytes, $eol) は出力を 76 文字で折り返し、最後の行も含めて各行に $eol を付けます。デフォルトは "\n" です。

print encode_base64('hi');        # "aGk=\n"
print encode_base64('x' x 60);
# eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4
# eHh4

これは MIME のメール本文 には正しい動作ですが、HTTP ヘッダー、JSON、URL、データベースのフィールドなど、それ以外のほぼすべての用途では誤りです。1 行で出力するなら '' を、RFC 2045 に厳密に従った行末にするなら "\r\n" を渡してください。

HTTP ヘッダーなら次のようになります。

print "Authorization: Basic ", encode_base64('user:pass', ''), "\n";
# Authorization: Basic dXNlcjpwYXNz

デコードは行末を気にしません。decode_base64("aGVs\r\nbG8K") は "hello\n" を返すので、\n でも \r\n でも MIME の本文をそのままデコードできます。

URLセーフな Base64

MIME::Base64 は、要求すれば encode_base64url と decode_base64url をエクスポートします。エンコーダーはパディングを省き、デコーダーはパディングのない入力を受け付けます。

use MIME::Base64 qw(encode_base64url decode_base64url);

print encode_base64url('<<???>>'), "\n";   # PDw_Pz8-Pg
print decode_base64url('PDw_Pz8-Pg'), "\n"; # <<???>>

パディングを残した URLセーフな出力が必要なら、標準形を tr で置き換えます。

my $padded = encode_base64('<<???>>', '') =~ tr{+/}{-_}r;   # PDw_Pz8-Pg==

/r フラグ(Perl 5.14 以降)は変更後のコピーを返します。どのシステムがどちらのバリアントを想定しているかは Base64 と Base64URL の違い で説明しています。

ファイルのエンコードとデコード

改行や文字コードを変換するレイヤーが入らないよう、ファイルは :raw モードで開きます。

open my $in, '<:raw', 'hello.txt' or die $!;
my $data = do { local $/; <$in> };
close $in;
print encode_base64($data, ''), "\n";      # aGVsbG8K

open my $out, '>:raw', 'copy.txt' or die $!;
print {$out} decode_base64('aGVsbG8K');
close $out;

大きなファイルをチャンク単位で処理する

57 バイトの倍数で読み込みます。57 バイトはちょうど 76 文字にエンコードされるので、各チャンクが完全な MIME の行を生成し、つなげた出力はファイル全体を一度にエンコードした結果と同一になります。

open my $in,  '<:raw', 'big.bin' or die $!;
open my $out, '>:raw', 'big.b64' or die $!;
while (read($in, my $buf, 57 * 1024)) {
    print {$out} encode_base64($buf);
}
close $in;
close $out;

結果を base64 -d でデコードすると、元の 300 KB のファイルに戻りました。1 行で出力するなら、3 で割り切れる任意のチャンクサイズで encode_base64($buf, '') を使ってください。このモジュールは、エンコードせずにサイズを教えてくれます。MIME::Base64::encoded_base64_length('hello') は 9(改行を含む)を、decoded_base64_length('aGVsbG8=') は 5 を返します。計算式は サイズ増加の解説 にあります。

コマンドラインでは、このモジュールを使った次の 2 つのワンライナーを覚えておくと便利です。

perl -MMIME::Base64 -0777 -ne 'print encode_base64($_, "")' < file.bin
perl -MMIME::Base64 -0777 -ne 'print decode_base64($_)' < file.b64 > file.bin

検証:decode_base64 は文句を言わない

decode_base64 は設計上寛容です。アルファベット外の文字を捨て、パディングで止まり、手元にあるものを返します。

decode_base64('aGk=!!');        # "hi"
decode_base64('aGk');           # "hi"   (missing padding is fine)
decode_base64('aGl=');          # "hi"   (non-zero padding bits ignored)
decode_base64('a');             # ""     (too short, no warning)
decode_base64('PDw_Pz8-Pg==');  # "<<\x0f\xcf\xc3\xe0", not "<<???>>"

最後の例が危険なケースです。URLセーフな入力を標準のデコーダーに渡すと、何も言わずに誤ったバイト列を生成します。トークンには decode_base64url を使ってください。信頼できない入力では、アルファベットと長さを確認してから、往復変換で一致を確かめます。

sub is_base64 {
    my ($s) = @_;
    return 0 unless $s =~ m{\A[A-Za-z0-9+/]*={0,2}\z} && length($s) % 4 == 0;
    return encode_base64(decode_base64($s), '') eq $s ? 1 : 0;
}
# is_base64('aGk=') = 1, 'aGk' = 0, 'aGl=' = 0, 'aGk=!!' = 0

Base64 バリデーター は同じチェックをブラウザで行い、どの文字が誤っているかを教えてくれます。ログから取り出した壊れた文字列には Base64 の修復 を試してください。よくある原因は よくある Base64 のエラー で説明しています。

落とし穴

  • ヘッダーの末尾の改行。 "Authorization: Basic " . encode_base64("$user:$pass") は \n で終わるため、リクエストが壊れます。'' を渡してください。
  • 二重エンコード。 use utf8 のもとで出力レイヤーが :encoding(UTF-8) のとき、デコードしたバイト列を decode('UTF-8', ...) を通さずに出力すると、héllo のような文字化けが起きます。
  • Base64 を秘密扱いする。 誰でも元に戻せます。エンコードと暗号化の違い をご覧ください。

よくある質問

encode_base64 が改行を付けるのはなぜですか?

2 番目の引数が行末の文字列で、デフォルトが "\n" だからです。出力は 76 文字で折り返され、必ずその文字列で終わります。1 行で出力したいなら '' を渡してください。

「Wide character in subroutine entry」とはどういう意味ですか?

バイト列しか受け付けない encode_base64 に、コードポイントが 255 を超える文字列を渡したということです。先にテキストをエンコードしてください:encode_base64(encode('UTF-8', $text), '')。

decode_base64 は入力を検証しますか?

しません。アルファベット外の文字を無視し、デコードできたものを返すので、decode_base64('aGk=!!') は hi になります。信頼できない入力なら、デコードの前に正規表現で文字列をチェックしてください。

続けて読む