Закодировать и декодировать строку
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", так что тела MIME и с \n, и с \r\n декодируются напрямую.
URL-safe 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-safe вывод с дополнением, переведите стандартную форму через 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;
Результат декодировался обратно в исходный файл на 300 КБ через base64 -d. Для вывода в одну строку используйте 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-safe строка, переданная стандартному декодеру, молча даёт неправильные байты. Для токенов используйте 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»?
Вы передали в encode_base64, который принимает только байты, символьную строку с кодовыми точками больше 255. Сначала закодируйте текст: encode_base64(encode('UTF-8', $text), '').
Проверяет ли decode_base64 входные данные?
Нет. Он игнорирует символы вне алфавита и возвращает всё, что сумел декодировать, так что decode_base64('aGk=!!') даёт hi. Если входным данным нельзя доверять, проверьте строку регулярным выражением перед декодированием.