上传文件
  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 报错退出;只包含 é 这类 Latin-1 字符的字符串则会被悄悄按 Latin-1 而不是 UTF-8 编码。

参数 '' 的作用见下一节。

行尾参数

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 和数据库字段。传入 '' 得到单行输出,或者传入 "\r\n" 以严格遵循 RFC 2045 的行尾。

对于 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 文件一致。需要单行输出时,用 encode_base64($buf, ''),块大小只要能被 3 整除即可。这个模块还能不编码就告诉你大小:MIME::Base64::encoded_base64_length('hello') 返回 9(含换行符),decoded_base64_length('aGVsbG8=') 返回 5。公式见 体积开销指南。

在命令行中,借助这个模块有两条值得记住的单行命令:

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 会加一个换行符?

第二个参数是行尾字符串,默认值为 "\n"。输出每 76 个字符换行,并且总以这个字符串结尾。传入 '' 就能得到单行输出。

Wide character in subroutine entry 是什么意思?

你把一个包含码位大于 255 的字符的字符串传给了 encode_base64,而它只接受字节。请先对文本编码:encode_base64(encode('UTF-8', $text), '')。

decode_base64 会校验输入吗?

不会。它忽略字母表之外的字符,能解码多少就返回多少,所以 decode_base64('aGk=!!') 得到的是 hi。如果输入不可信,请在解码前先用正则检查字符串。

继续阅读