Кодирование Base64 в Perl: полное руководство
У вас есть данные, которым нужно пережить канал, который их не любит. Бинарная глыба, которой нужно устроиться в JSON-поле. Изображение, которому суждено жить внутри HTML-тега. Сертификат, которому место в файле конфигурации. Токен, которому предстоит путешествовать через URL, заголовки и строки запроса. Это будни Base64-кодирования: оно переписывает каждые три байта сырых данных четырьмя символами из алфавита в 64 буквы, дочиная хвост одним-двумя знаками =, так что на выходе получается обычный текст, который перенесёт что угодно, - обычно примерно на 33 процента длиннее того, с чем вы начинали. Главная страница этого сайта объясняет формат во всех подробностях, поэтому эта статья сосредоточена на том, что Perl вам даёт, что он молча решает за вас и где лежат ловушки.
Сначала хорошая новость: encode_base64 живёт в ядре Perl с 2002 года, реализован на C и вполне быстро. Интересная часть в том, что у функции есть собственные мнения. Она переносит вывод по 76 символов, дописывает завершающий перевод строки и отказывается кодировать Unicode-символы, которые вы не преобразовали в байты: выше диапазона Latin-1 он падает наповал, а ниже этой черты молча предполагает байты Latin-1. Это руководство пройдётся по каждому вкусу Base64, который Perl-разработчик реально производит: однострочник, MIME-обёрнутое тело письма, PEM-обёрнутый ключ, URL-безопасный токен и потоковая версия для файлов, которые слишком велики, чтобы держать в памяти.
Одна функция и скрытый перевод строки
Весь API, ровно так, как его представляет современная документация:
encode_base64( $bytes )
encode_base64( $bytes, $eol )
Прочитайте это ещё раз. Два аргумента, один из них необязательный, одно возвращаемое значение, без флагов. Необязательный второй аргумент - это последовательность конца строки, и по умолчанию это обычный перевод строки, - так что самый безобидный на вид вызов в Perl производит перенесённый вывод, завершённый переводом строки:
use MIME::Base64 qw(encode_base64);
my $wrapped = encode_base64("Aladdin:open sesame");
print length($wrapped), "\n"; # 29: 28 символов плюс перевод строки
my $single = encode_base64("Aladdin:open sesame", "");
print length($single), "\n"; # 28: передайте пустую строку, чтобы не было переносов
Размер вывода следует фиксированному паттерну, который можно предсказать до вызова:
| Входные байты | Символы вывода | Заполнение |
|---|---|---|
| 0 | 0 | нет |
| 1 | 4 | два = |
| 2 | 4 | один = |
| 3 | 4 | нет |
| 100,000 | 133,336 | нет, на одной строке |
| 3,000,000 | 4,000,000 | нет |
Паттерн такой: четыре символа на каждую полную группу из трёх байтов, плюс последняя неполная группа, дочиная одним-двумя знаками =. Одно следствие, которое стоит знать: и один байт, и три байта производят по четыре символа, так что закодированная длина скрывает точный размер входа. А если размер нужен без самой работы, у модуля есть функция длины с 3.10 в 2010 году. Просто по умолчанию она не экспортируется, так что зовите её через имя пакета:
use MIME::Base64 ();
my $with_wrap = MIME::Base64::encoded_base64_length($bytes); # строки по 76 символов, eol по умолчанию
my $single_line = MIME::Base64::encoded_base64_length($bytes, ""); # без переносов
my $mime_body = MIME::Base64::encoded_base64_length($bytes, "\r\n");
Есть ещё одно правило, которое стоит запомнить, - это единственный случай, когда кодер во всеуслышание заявляет: если строка, которую вы ему подаёте, содержит символы с кодом выше 255, encode_base64 погибает с ошибкой «широкий символ на входе в подпрограмму». Ниже этой черты сбой тише: символы до 255 молча понижаются до своих Latin-1 байтов, так что строка с буквами, пропустившая конвертацию, кодируется как Latin-1 вместо UTF-8, и никто вам этого не скажет. Base64-кодирование определено только для однобайтовых символов, а Perl 5.8 и новее допускают расширенные символы в строках, так что конвертация - решение, которое вы принимаете осознанно, с Encode, в самом следующем разделе.
Текст или байты? Шаг, который функция не делает за вас
Строки Perl несут тихий флаг, который говорит, держат они символы или байты, и Base64 живёт на байтовой стороне этой черты. Если ваш текст - строка символов, а так и есть, когда он пришёл из JSON-парсера, из шаблона или из литерала с буквами в UTF-8-файле исходников, кодер отказывается угадывать, какие байты вы имели в виду для символов выше диапазона Latin-1, - и говорит вам об этом. Ниже диапазона он молча гадает Latin-1. Исправление одно и то же в обоих случаях, и это одна ядерная функция из модуля Encode:
use MIME::Base64 qw(encode_base64);
use Encode qw(encode);
my $chars = "H\x{eb}llo W\x{f6}rld"; # строка символов
my $utf8 = encode("UTF-8", $chars); # теперь: байты
my $b64 = encode_base64($utf8, "");
print $b64, "\n"; # SMOrbGxvIFfDtnJsZA==
Вот тот вызов encode и есть весь танец: выберите байтовое представление, для всего современного - UTF-8, переведите символы в эти байты и только потом отдайте байты кодеру. Для устаревшего западного текста, прибывшего в Windows 1252, конвертация - та же функция с другим именем, encode("Windows-1252", $legacy), и она отдаёт вам исходную однобайтовую форму. Модуль Encode - из ядра, так что всё это стоит ничего.
Теперь ловушка. Если имеющиеся у вас байты уже UTF-8 и вы прогоните их через encode("UTF-8", ...) ещё раз, думая, что делаете их UTF-8, вы не получите копию: вы получите двойное кодирование, где каждая буква с диакритикой разбухает в два символа сама по себе. Классический симптом - текст, который читался как Hëllo, а теперь читается как Hëllo, - и каждый декодер в интернете декодирует это для вас с полной добросовестностью:
use Encode qw(encode decode);
my $right = encode("UTF-8", "H\x{eb}llo");
my $wrong = encode("UTF-8", $right); # перекодирование байтов как символов
print decode("UTF-8", $right), "\n"; # Hëllo
print decode("UTF-8", $wrong), "\n"; # Hëllo
Правило, которое это предотвращает: байты кодируются ровно один раз, а utf8::is_utf8() показывает, на какой стороне черты находится строка. Если флаг стоит, вы держите символы, и вызов encode() - правильный ход; если не стоит, вы держите байты и готовы к Base64.
Перенос строк: три диалекта, одно правило
Поскольку вывод по умолчанию перенесён и завершён переводом строки, первое решение в любой кодировочной работе - вопрос адреса: где будет жить эта строка? Объединяющий факт: декодеры вообще игнорируют переводы строк: RFC 2045 велит декодирующему программному обеспечению игнорировать все переводы строк и все символы вне алфавита, так что перенос строк - это вежливость по отношению к строковым инструментам и людям, а не смысловое отличие. Три ответа на практике:
Без переводов строк. Вывод функции с отключённым переносом, ровно одна строка. Это то, что нужно для URL, JSON-полезных нагрузок, заголовков, значений в базах данных и всего остального, где перевод строки стал бы багом. Это также то, что большинство людей имеют в виду, когда просят «просто Base64»:
my $single = encode_base64($bytes, "");
MIME: 76 символов плюс CRLF. Почтовая конвенция из RFC 2045: закодированные строки не должны превышать 76 символов, а MIME-мир говорит на CRLF. Это уже дело самого модуля, решаемое вторым аргументом:
my $mime_body = encode_base64($bytes, "\r\n");
PEM: 64 символа плюс LF. Ключи и сертификаты используют более старую конвенцию с более короткими строками в 64 символа, и сам модуль такую ширину не производит, так что четырёхстрочный хелпер заполняет пробел:
sub wrap_lines {
my ($text, $width) = @_;
return join "\n", $text =~ /(.{1,$width})/g;
}
my $pem = "-----BEGIN CERTIFICATE-----\n"
. wrap_lines(encode_base64($der, ""), 64) . "\n"
. "-----END CERTIFICATE-----\n";
Тот же хелпер служит и другим 64-символьным диалектам: текстовому кодированию PKIX из RFC 7468 и броне OpenPGP, чьи строки данных имеют ширину 64 символа, а завершающая строка контрольной суммы CRC24 добавляется не вами, а GnuPG. Одна мелкая ловушка действует для всех: encode_base64 дописывает конец строки в самом конце результата, даже когда последняя строка заполнена точно по ширину. Если потребитель ниже по цепочке спотыкается об эту завершающую пустую строку, вызов rtrim на результате исправит дело.
URL-безопасный Base64: алфавит из - и _
Стандартный алфавит включает + и /, и оба - беда вне текстового файла: + в форм-кодированной строке запроса превращается в пробел, прежде чем ваше приложение его увидит, а / - разделитель пути в URL. У имён файлов и токенов есть свои претензии. Раздел 5 RFC 4648 решает это алфавитом, безопасным для URL и имён файлов, где + становится -, / становится _, а завершающее заполнение = обычно отбрасывается. RFC настаивает, что его не следует считать идентичным base64-кодированию, так что относитесь к нему как к отдельному формату, обычно называемому base64url. Perl производит его одним вызовом с версии 3.11 в 2010 году:
use MIME::Base64 qw(encode_base64url);
my $seg = encode_base64url("sunset-42");
print $seg, "\n"; # c3Vuc2V0LTQy: без заполнения, без перевода строки
Этот один вызов делает все три изменения: смена алфавита, без заполнения, без переносов строк. Если у вас уже есть стандартный Base64, а адресат хочет URL-безопасный диалект, две строковые операции конвертируют его на месте:
sub to_urlsafe {
my ($b64) = @_;
$b64 =~ tr{+/}{-_};
return $b64 =~ s/=+\z//r;
}
my $seg = to_urlsafe(encode_base64($bytes, ""));
Когда использовать: части JSON Web Token, параметры state и nonce в OAuth, ID API, которые вы кладёте в путь URL, и непрозрачные ключи, которым нужно пережить адресную строку или имя файла, - на CPAN для этого как раз есть Data::UUID::Base64URLSafe. Когда не использовать: тела писем, броня PEM и любое место, где по ту сторону стоит потребитель стандартного алфавита, потому что - и _ не входят в его словарь. И не смешивайте два алфавита молча: значение, закодированное URL-безопасно, должно декодироваться URL-безопасно, везде и вечно. В Perl старше 3.11 отдельный модуль MIME::Base64::URLSafe 2006 года, порт urlsafe-кодека Python, даёт urlsafe_b64encode; на любом современном Perl встроенная функция - правильный инструмент.
Собираем JWT: каждая часть вручную
JSON Web Token - флагманский потребитель Base64 в современных API, и он использует URL-безопасный диалект без заполнения из раздела выше. По RFC 7515 компактный JWT - это три base64url-части, разделённые точками: защищённый заголовок, пелод и подпись. Собрать его вручную - приятный способ увидеть все подвижные части:
use MIME::Base64 qw(encode_base64url);
use JSON::PP qw(encode_json);
use Digest::SHA qw(hmac_sha256);
my $secret = "correct-horse-battery-staple";
my $head = encode_base64url(encode_json({ alg => "HS256", typ => "JWT" }));
my $claims = encode_base64url(encode_json({ sub => "homer", role => "admin" }));
my $sig = encode_base64url(hmac_sha256("$head.$claims", $secret));
my $jwt = "$head.$claims.$sig";
print $jwt, "\n"; # компактный HS256-токен; порядок ключей внутри каждой JSON-части меняется от запуска к запуску
Три детали, которые стоит заметить. Во-первых, encode_json из ядерного модуля JSON::PP выдаёт компактные UTF-8 байты без пробельных символов, - ровно то, чего внутри токена ждут JOSE-спецификации. Во-вторых, пелод читаем любым, и это по замыслу: JWT - подписанный билет, а не секрет, так что никогда не кладите конфиденциальные значения в claims. В-третьих, подпись - это base64url-кодирование сырых HMAC-байтов, поэтому hmac_sha256 идёт прямо в кодер без всякого шестнадцатеричного форматирования.
Для продакшена подпись не собирают вручную. Модуль CPAN Crypt::JWT, построенный на CryptX, реализует JWS и JWE с полным набором алгоритмов:
use Crypt::JWT qw(encode_jwt);
my $jwt = encode_jwt(
payload => { sub => "homer", role => "admin" },
alg => "HS256",
key => $secret,
);
А на принимающей стороне закрепите алгоритм через accepted_alg, чтобы атакующий не мог подменить токен на более слабый вариант: decode_jwt(token => $jwt, key => $secret, accepted_alg => "HS256") проверяет подпись и падает при неудаче. Вручную собирать можно для понимания, а для денег - библиотека.
HTTP: заголовки аутентификации, data URI и рукопожатие WebSocket
Заголовок Authorization: Basic - самый старый живой сценарий: имя пользователя и пароль, склеенные двоеточием, закодированные в одну строку и помеченные словом схемы. Пустая строка вторым аргументом здесь несущая конструкция, потому что завершающий перевод строки внутри поля заголовка - баг:
use MIME::Base64 qw(encode_base64);
my $user = "alice";
my $pass = "s3cr3t";
my $header = "Basic " . encode_base64("$user:$pass", "");
print $header, "\n"; # Basic YWxpY2U6czNjcjN0
Data URI из RFC 2397 - та же идея, применённая к изображениям: пелод сидит прямо в URL, так что второго запроса ради его получения не нужно. Бинарные медиа используют флаг ;base64, так что пелод - это ровно то, что encode_base64 произвёл с отключённым переносом:
my $uri = "data:" . $mime_type . ";base64," . encode_base64($bytes, "");
Компромиссы здесь реальны. Закодированный пелод примерно на 33 процента больше файла, что делает сам HTML-документ больше. Браузеры не кэшируют data URI так, как кэшируют URL файла, - отдельной загрузки для кэша нет, так что при каждом просмотре страницы байты снова летят как часть документа, а сам RFC говорит, что data URI полезны только для коротких значений. Используйте их для аватаров, иконок и небольших встроенных графиков; для всего остального - настоящие файлы. Есть третий HTTP-уголок, где Base64 работает тихо: рукопожатие WebSocket из RFC 6455, где клиент присылает заголовок Sec-WebSocket-Key, который есть Base64 шестнадцати случайных байтов. Фреймворки вроде Mojolicious делают это за вас, но если вы когда-нибудь увидите это в трафике, теперь вы знаете, что это:
use MIME::Base64 qw(encode_base64);
my $key = encode_base64(pack("C16", map { int(rand 256) } 1 .. 16), "");
print length($key), "\n"; # 24: шестнадцать случайных байтов, дополненных до группы в четыре символа
Файлы: чтение целиком, куски по 57 байт и командная строка
Самая бесхитростная кодировочная работа: файл становится текстом. Строки Perl - это байты, так что искать бинарный режим не нужно - слой :raw и есть всё. Открыть raw, прочитать, закодировать, записать:
use MIME::Base64 qw(encode_base64);
open my $in, "<:raw", $ARGV[0] or die $!;
local $/;
my $bytes = <$in>;
close $in;
open my $out, ">:raw", $ARGV[1] or die $!;
print {$out} encode_base64($bytes);
close $out;
Слои :raw важны. Без них Perl попытался бы интерпретировать байты как платформенный текст на входе и на выходе, а на системе с другой кодировкой по умолчанию это ровно та порча, которую не видно, пока файл не откроют где-нибудь в другом месте. И помните про смету размеров, планируя хранение: изображение в 500 КБ становится текстовым файлом в 670 КБ, а видео в 1 ГБ - 1.33 ГБ.
Для файлов, которые не помещаются в память, собственная документация модуля даёт правило: кодируйте кусками, кратными 57 байтам, потому что 57 байтов данных ровно заполняют одну 76-символьную строку, - 76 есть 57 раз 4, делённые на 3. Резьте на этой границе, и заполнения посреди потока не будет никогда:
use MIME::Base64 qw(encode_base64);
open my $in, "<:raw", $ARGV[0] or die $!;
while (read($in, my $buf, 57 * 10)) {
print encode_base64($buf);
}
close $in;
Каждый кусок ложится ровно на границы строк, последний, возможно короче, кусок несёт завершающее заполнение, и результат байт в байт совпадает с тем, что вы получили бы, прочитав файл целиком и закодировав за один раз, - только со стабильным потреблением памяти. А когда скрипт и вовсе не нужен, однострочник закрывает вопрос: -0777 читает ввод целиком, а пустая строка аргументом держит вывод на одной строке:
perl -MMIME::Base64 -0777 -ne 'print encode_base64($_, "")' < file > file.b64
Электронная почта: MIME-тела и вложения
Электронная почта - это то место, где Base64 заработал своё имя. Стандарт MIME говорит, что данные, которым небезопасно ехать сырым текстом, следует слать с Content-Transfer-Encoding: base64, строками не длиннее 76 символов. Если вы собираете почту MIME::Lite, всё это - один аргумент, и модуль делает кодирование, перенос строк и заголовок за вас:
use MIME::Lite;
my $mime = MIME::Lite->new(
From => 'me@example.com',
To => 'you@example.com',
Subject => 'A file',
Type => 'text/plain',
Data => 'The body text.',
);
$mime->attach(
Type => 'application/octet-stream',
Data => $bytes,
Encoding => 'base64',
Filename => 'hello.txt',
);
Аргумент Encoding - спусковой крючок: MIME::Lite кодирует вложение в Base64 строками по 76 символов (с голым переводом строки по умолчанию модуля; именно почтовая доставка превращает их в CRLF) и штамповает часть соответствующим заголовком Content-Transfer-Encoding. Email::MIME занимает ту же позицию и кодирует в Base64 любое вложение, которое вы ему отдаёте как строку сырых данных (из его документации: «все части, созданные таким образом, кодируются base64, просто на всякий случай»). Если вы собираете сырое MIME-сообщение вручную, эквивалент - две строки из раздела про перенос, encode_base64($bytes, "\r\n") плюс строка заголовка, и это вся история с протокольной стороны.
Базы данных, конфигурация и переменные окружения
Базы данных: бинарные данные часто ездят в колонке TEXT как Base64, потому что колонка не может пообещать, что пропустит произвольные байты без изменений. Храните однострочную форму и никогда перенесённую, иначе следующий SELECT вернёт строку с переводами строк посреди значения:
use MIME::Base64 qw(encode_base64);
# $dbh - уже подключённый DBI-дескриптор
my $stmt = $dbh->prepare(q{UPDATE photos SET data = ? WHERE id = ?});
$stmt->execute(encode_base64($bytes, ""), 42);
Файлы конфигурации - та же форма: JSON-документ, где бинарное или секретное поле - однострочная Base64-строка, - именно для этого и существует второй аргумент:
use JSON::PP qw(encode_json);
my $config = {
api_key => encode_base64($key_bytes, ""),
logo_png => encode_base64($png_bytes, ""),
};
open my $fh, ">:raw", "app.json" or die $!;
print {$fh} encode_json($config);
close $fh;
Переменные окружения заслуживают слова предостережения. Base64 годится для маленьких токенов в окружении, но закодированная форма на 33 процента больше оригинала, а операционная система ограничивает размер каждого аргумента. На Linux лимит - 128 КБ на строку, и его держит execve, - и большая глыба в переменной окружения не падает вежливо: дочерний процесс умирает с туманной ошибкой в момент запуска. Маленькие значения - в окружении, большие - в файл или в базу.
Производительность: работу делает C, а не Perl
Ядерный модуль реализован на C, и тот C происходит от кода, написанного для metamail в 1991 году, - курьёзный факт, пока вы не замечаете следствия: у кодировщика три декады настройки. На современной машине он пропускает данные с темпом в гигабайты в секунду, что быстрее диска или сети, которые он обычно питает, так что сам Base64 почти никогда не является узким местом. Узкое место - ввод-вывод.
Для редкой системы без C-компилятора чистый Perl-аналог MIME::Base64::Perl на CPAN даёт тот же базовый интерфейс, в несколько раз медленнее, но для обычных нагрузок - вполне комфортно. И две привычки держат большие работы предсказуемыми: перекачивайте кусками по 57 байтов вместо чтения целиком и меряйте буферы через encoded_base64_length до аллокации, - это снимает и гадание, и реальлокацию.
Подвохи, отсортированные по цене для вашего послеполудня
Ловушки, примерно в том порядке, в каком кусаются:
| Подвох | Что происходит | Как исправить |
|---|---|---|
| Забытый второй аргумент | вывод прибывает перенесённым по 76 символов с завершающим переводом строки, и ваш URL, JSON-поле или заголовок ломаются посреди значения | передавайте "" для однострочного вывода, а переносы оставляйте там, где их ждут |
| Перенос по неверной ширине | PEM-потребитель ожидает строки по 64 символа, а получает 76, либо MIME-тело выходит за 76-символьный лимит | совмещайте ширину с диалектом: "" - без переноса, "\r\n" - для MIME, хелпер - для PEM |
| Падение на широком символе | строка символов с кодами выше 255 погибает посреди запроса с ошибкой «широкий символ на входе в подпрограмму» | сначала прогоните символы через Encode, назвав кодировку осознанно, до вызова encode_base64 |
| Двойное кодирование | перекодирование уже UTF-8 байтов через encode("UTF-8", ...) превращает Hëllo в Hëllo |
байты кодируются ровно один раз; при сомнении проверяйте utf8::is_utf8() |
| Молчаливое смешение алфавитов | значение, закодированное с - и _, попадает в декодер стандартного алфавита и возвращается мусором |
один диалект на значение, от края до края: выбирайте base64url или стандартный на границе |
| Завершающий символ конца строки | encode_base64 дописывает eol, даже когда последняя строка заполнена точно, и строгий потребитель видит пустую строку |
chomp или rtrim результата, если потребитель привередлив |
| Перенесённые значения в базе данных | переводы строк заезжают в колонку TEXT, и следующий SELECT возвращает битый токен |
храните однострочную форму; переносите только на стороне адресата |
| Переменные окружения с большими глыбами | рост на 33 процента плюс лимит ОС на аргумент убивает дочерний процесс в момент запуска с туманной ошибкой | маленькие значения - в окружении, большие - в файл или в базу |
| Предположение, что Base64 - это защита | формат ничего не прячет, и в публичных хрониках задокументированы реальные случаи, когда пользователь вставлял IMAP-переписку и по неосторожности раскрывал пароль | относитесь к выводу как к конфиденциальному с момента, когда он произведён, и держите его подальше от логов |
| Планирование без сметы размеров | изображение в 500 КБ становится 670 КБ текста, и лимит хранения или пелода, который вы не проверили, кусает | закладывайте 4/3 от исходного размера, прежде чем решать |
История, рассказанная кодером
У кодера Base64 в Perl есть карьера, заслуживающая минуты, и она начинается в самом первом веб-инструментарии:
- Рождение в libwww-perl. Кодер начал жизнь как
LWP::Base64, написанный Мартином Костером и Йоргом Райхельтом; Гисли Аас вобрал его в libwww-perl под именемMIME::Base64; в апреле 1997 года он вырос в собственную CPAN-дистрибуцию, версия 2.00, с записью в журнале изменений, которая просто говорит, что он основан на libwww-perl 5.08. - Эра скорости. Версия 2.07 в 1998 году выкатила более быстрый и умный C-вариант декодера, примерно на 25 процентов шустрее на тогдашних современных Linux-машинах, и настройка продолжалась ещё десятилетие.
- Эра Unicode. Perl 5.8 в 2002 году принёс в обычные строки символы с кодами выше 255, и модуль отвечал поэтапно: 2.12 в 2001 году понижал UTF-8-строки до кодирования, а современное падение с ошибкой «широкий символ на входе в подпрограмму» - это способ кодера держать то обещание. Синхронизация 2.13 с ядром в том же году принесла поддержку EBCDIC, - напоминание, что Base64 в Perl до сих пор крутится на мейнфреймах.
- Эра командной строки. Релизы с 2.14 в 2003 году по 3.05 в 2004 году поставляли настоящую команду
encode-base64вместе с её decode- и quoted-printable-близнецами; 3.06 в 2005 году перенёс скрипты в отдельную дистрибуцию MIME-Base64-Scripts. - Прибытие URL-безопасного. RFC 4648 стандартизировал URL-безопасный алфавит в 2006 году, в том же году появился отдельный модуль
MIME::Base64::URLSafe, а ядерный модуль догнал его в 3.11 в 2010 году:encode_base64urlодним вызовом. - Современная ветка. Версия 3.16 в 2020 году пересобрала упаковку и подняла нижний порог до Perl 5.6; текущие ядерные Perl поставляют серию 3.16, и модуль поддерживается внутри ядерного дистрибутива, - примерно столь же безопасное жилище, каким только может быть у ядерного модуля.
Факты для любопытных, конкретно про Perl
Курьёзы, которые делают эту историю хорошей:
- Пример из POD - магическая фраза. С 1997 года собственная документация модуля кодирует
Aladdin:open sesame, так что строкаQWxhZGRpbjpvcGVuIHNlc2FtZQ==почти тридцать лет служит визитной карточкой модуля. - Перевод строки по умолчанию - не тот, которого вы, вероятно, ждали. Это обычный
\n, а не CRLF, на котором говорит MIME. Собственной конвенции RFC нужен второй аргумент, а модуль поставляется с программистским значением по умолчанию, а не протокольным. - У пустой строки есть особое правило. Закодируйте ничего - и получите ничего, без дописанного перевода строки: единственное задокументированное исключение из правила про завершающий eol, и причина, по которой пустой файл проходит круговое путешествие чисто.
- У IMAP-родственника есть запятая. Вариант для имён почтовых ящиков из RFC 3501 заменяет в алфавите
/на запятую, так что Base64-строка с IMAP-сервера может содержать букву, которую стандартный декодер считает шумом. - Происхождение из 1991 года - настоящая вещь. C-реализация происходит от metamail, почтовой программы Bellcore 1991 года, за три года до рождения Perl 5, так что каждый вызов
encode_base64отчасти код девяностых. - У чистого Perl-аналога есть своя история. Когда версия 3.00 в 2004 году выбросила чистые Perl-реализации из ядерного модуля, журнал изменений назвал их раздутостью, которая прячет настоящие проблемы в XS-реализациях, и переиздал их как
MIME::Base64::Perl, где они живут по-прежнему.
Так что в следующий раз, когда сырым байтам нужно будет путешествовать по чисто текстовому миру, вы знаете всю историю. Один вызов функции делает работу, скрытый перевод строки - решение, которое вы принимаете вторым аргументом, падение на широком символе - способ кодера держать ваш Unicode честным, URL-безопасный диалект с 2010 года делается одним вызовом, файлы перекачиваются кусками по 57 байтов, а 33-процентная смета - плата за вход. А если однажды вам нужно будет пройти путь в обратную сторону, взять строку из букв и получить обратно исходные байты, связанная ниже статья про декодирование Base64 в Perl разберёт тот ритуал с той же глубиной.
Последнее обновление: 2026-09-08
Связанная статья: Декодирование Base64 в Perl: полное руководство