Приходится иметь дело с форматом Base64? Тогда этот сайт идеально вам подойдет! Воспользуйтесь нашим невероятно удобным онлайн-инструментом для кодирования или декодирования ваших данных.

Декодирование Base64 в Perl: полное руководство

Вам досталась строка из букв, цифр и изредка встречающихся + или /, и где-то в глубине души вы понимаете, что она совсем не то, чем выглядит. Возможно, это токен, который едет в заголовке Authorization, файл .b64, выкопанный из тикета поддержки, сертификат, облачённый в броню -----BEGIN, или глыба, спокойно лежащая в файле конфигурации. Вы открываете терминал, вводите perl, и один вопрос забирает всё остальное: как вернуть настоящие данные?

Ответ короткий и успокаивающий. Perl поставляет модуль Base64 вместе с самим языком с 2002 года, и один вызов функции, decode_base64, делает всю работу: нечего устанавливать и нечего настраивать. Пока варится кофе, небольшой повтор: Base64 переписывает каждые три байта данных четырьмя символами из алфавита в 64 символа, заполняя хвост одним-двумя знаками = так, чтобы результат всегда складывался в кратное четырём число, - именно поэтому закодированная форма обычно примерно на 33 процента больше исходной. Главная страница этого сайта объясняет формат во всех подробностях, так что это руководство тратит всё своё время на Perl-сторону: правила декодера, диалекты и реальные форматы, с которыми вы действительно столкнётесь.

Набор инструментов: пять функций, ноль установок

Каждый нужный вызов живёт в MIME::Base64, который входит в ядро Perl с версии 5.8, так что он есть на любой серьёзной установке: от той, что вшита в прошивку роутера, до той, что на сервере базы данных. Проверка - одна строка:

perl -MMIME::Base64 -e 'print $MIME::Base64::VERSION, "\n"'
# 3.16_01

Вот декодирующая сторона модуля, целиком:

Функция Что делает Примечания
decode_base64($str) звезда этой статьи: превращает Base64-глыбу в сырые байты вечно молча игнорирует каждый символ вне алфавита
MIME::Base64::decode($str) тот же декодер, вызванный без импорта форма, которую вы встретите во множестве старых скриптов
decode_base64url($str) декодирует URL-безопасный диалект с - и _, с заполнением или без добавлен в 3.11 в 2010 году; именно она читает JWT
MIME::Base64::decoded_base64_length($str) говорит, насколько большими будут декодированные данные, без самого декодирования по умолчанию не экспортируется, удобен для предварительного расчёта размера буферов
unpack("u", $data) декодирует uuencoded-данные, формат, предшествовавший Base64 встроен сам в Perl, модуль не нужен

Карта версий этих функций, на случай если вы поддерживаете парк старых машин:

Возможность Доступна с
decode_base64() с быстрым C-путём Perl 5.8 в 2002 году, когда модуль вошёл в ядро
decoded_base64_length() модуль 3.10 в 2010 году
decode_base64url() модуль 3.11 в 2010 году
Тихое декодирование, без предупреждений на подозрительный ввод модуль 3.11 в 2010 году
Текущая ветка 3.16 2020 год, требуется Perl 5.6 или новее

Если в вашем системном Perl по какой-то причине нет модуля, хотя его быть должно, исправление - одна из двух строк: пакет дистрибутива libmime-base64-perl на Debian и Ubuntu или cpanm MIME::Base64, чтобы взять текущий выпуск с CPAN, где модуль живёт как пакет двойного жизненного цикла со времён ядра. Для редкой машины без C-компилятора чистый Perl-аналог MIME::Base64::Perl на CPAN даёт тот же базовый интерфейс, в несколько раз медленнее, но достаточно для любой работы, кроме объёмной. Это и есть вся история зависимостей: больше ничего.

Декодер, который никогда не отказывает

Контракт длиной в одну строку. Дайте ему строку - и он вернёт декодированные байты в виде обычной Perl-строки, содержащей сырые октеты. Без объектов, без исключений, без флагов. Документация формулирует два правила, определяющих его характер, одним предложением: любой символ, не входящий в 65-символьное подмножество Base64, молча игнорируется, а любой символ, оказавшийся после символа заполнения =, никогда не декодируется. Эта вежливость - главное в этой статье, поэтому позвольте ей раз для вас поработать:

use MIME::Base64 qw(decode_base64);
print decode_base64("TWFu!"),   "\n";  # Man - восклицательный знак исчезает без следа
print decode_base64("TWFu=XX"), "\n";  # Man - всё после = пропускается
print decode_base64("TQ"),      "\n";  # M - без предупреждений, без комментариев
print decode_base64("T"),       "\n";  # пустая строка, и тут тоже без нареканий

Последние две строки - снисходительность в её крайней форме. TQ несёт один полный байт плюс четыре лишних бита, и декодер просто сохраняет байт и выбрасывает остаток. T не несёт даже одного полного байта, поэтому результат пуст. В модуле нет ни строгого режима, ни валидатора, которые вернули бы старомодную придирчивость: с версии 3.11 в 2010 году decode_base64 даже не предупреждает об усечённом вводе, а более старые версии под -w ворчали предупреждением «преждевременный конец base64-данных». Если глыба не верна, он всё равно её декодирует, - так что контроль качества остаётся за вами.

Вот политика снисходительности в одном месте, чтобы её можно было рассмотреть целиком:

Ввод Результат Почему
"TWFu" Man чистый ввод, счастливый путь
"TWFu!" Man восклицательный знак не входит в алфавит, поэтому он пропускается
"TWFu=XX" Man после заполнения никогда ничего не декодируется
"TWFuIFdvcmxkIQ==" Man World! пробельные символы в любом месте - бесплатно
"TQ" M один полный байт умещается, лишние биты молча выбрасываются
"T" пустая строка даже одного полного байта нет, и предупреждений тоже нет
"ab-cd_efgh" молча неверные байты URL-безопасные буквы выбрасываются как шум, классическая ловушка

Последняя строка - та, которую стоит запомнить. base64url-сегмент, отданный стандартному декодеру, не приводит к сбою: он декодируется в правдоподобно выглядящий мусор, потому что символы - и _ трактуются как чужеродный шум, а оставшиеся буквы всё ещё образуют валидные группы. Декодер - свидетель, а не охранник на входе, так что если вводу нельзя доверять, проверяйте его сами. Достаточно небольшой строгой проверки:

sub strict_base64 {
  my ($blob) = @_;
  $blob =~ s/[\r\n]//g;  # декодер их игнорирует, и мы тоже
  return 0 unless length($blob) % 4 == 0;
  return $blob =~ /\A[0-9A-Za-z+\/]+(?:={1,2})?\z/ ? 1 : 0;
}
print strict_base64("TWFu"),  "\n";  # 1
print strict_base64("TQ="),   "\n";  # 0 - неверное число знаков заполнения
print strict_base64("ab-cd"), "\n";  # 0 - URL-безопасный алфавит

Ещё одно замечание про этот регэксп - урок, добытый дорогой ценой: если подпрограмма заканчивается голым return $x =~ /.../ и результат несостоявшегося совпадения попадает прямо в printf, Perl поднимает вводящее в заблуждение предупреждение «не хватает аргумента в printf» вместо чистого нуля. Приведите результат совпадения с помощью ? 1 : 0 перед возвратом, как делает функция выше, - и этот трюк исчезнет полностью.

Сначала байты, потом символы

Вспомните, что возвращает decode_base64: сырые байты, обычная строка, в которой не стоит флаг UTF-8. Что эти байты значат - решение только за вами, и именно на этом шаге люди спотыкаются о Unicode. Perl отслеживает, содержит строка символы или байты, и length(), substr() и большинство регулярных выражений ведут себя по-разному в зависимости от ответа. Решение - осознанно назвать свою кодировку, модулем Encode, который поставляется с каждой установкой Perl:

use MIME::Base64 qw(decode_base64);
use Encode qw(decode);
my $raw  = decode_base64("SMOrbGxvIFdvcmxkIQ==");
my $text = decode("UTF-8", $raw);
print $text, "\n";            # Hëllo World!
print length($text), " chars\n";  # 12
print length($raw),  " bytes\n";  # 13

Эта пара чисел - весь урок. Глыба - 13 байтов, но только 12 символов, потому что буква с диакритикой занимает в UTF-8 два байта. Пропустите шаг с кодировкой, и байты всё равно приятно напечатаются в UTF-8-терминале, - именно поэтому ошибка остаётся невидимой, пока за них не берётся строковая функция, или пока байты не пройдут через конвейер, ожидающий символы. Если сомневаетесь, декодируйте со строгой кодировкой и позвольте исключению рассказать вам правду о байтах: передайте Encode::FB_CROAK, и decode() погибнет на невалидной последовательности, вместо того чтобы молча подставлять U+FFFD по умолчанию. Это и есть фича.

Короткий список кодировок, к которым вы действительно потянетесь:

Кодировка Когда использовать Будьте внимательны
UTF-8 допущение по умолчанию: API, JSON, веб-контент, современный текст невалидные последовательности по умолчанию становятся U+FFFD; с Encode::FB_CROAK они вызывают чистую гибель, - именно этого вы и хотите
Latin-1 устаревший западный текст, один байт на символ, никогда не может сломаться он охотно изувечит UTF-8 в двойную кодировку с кракозябрами
ASCII данные, в которых вы уверены: обычный 7-битный текст любой байт выше 127 по умолчанию становится U+FFFD (с Encode::FB_CROAK - гибель)
UTF-16 текст Windows, где порядок байтов решает метка порядка байтов BOM - единственная подсказка о порядке байтов, так что держите его в байтах

А вот и ловушка, от которой вас защищает шаг с кодировкой. Если декодированные вами байты уже UTF-8 и вы прогоните их через encode("UTF-8", ...) по пути наружу, вы не получите копию: вы получите двойное кодирование, где каждая буква с диакритикой разбухает в два символа сама по себе. Классический симптом - текст, который читался как Hëllo, а теперь читается как Hëllo, - и приёмник на другом конце провода декодирует это с полной добросовестностью. Байты на входе, байты на выходе, одна осознанно названная конвертация посередине.

base64url: алфавит для URL и токенов

Половина Base64, курсирующего по современному интернету, - это вовсе не стандартный алфавит. Символ + - это то, как браузер кодирует пробел в строке запроса, а / - разделитель пути, так что стандартные буквы в URL - катастрофа. Раздел 5 RFC 4648 определяет исправление: второй алфавит, который заменяет + и / на - и _ и, по конвенции, отбрасывает ещё и заполнение =, и переносы строк. В RFC прямо сказано, что это кодирование не следует считать идентичным base64-кодированию, и у Perl с версии 3.11 в 2010 году есть для него отдельная пара функций:

use MIME::Base64 qw(decode_base64url);
my $raw = decode_base64url("c3Vuc2V0LTQy");
print $raw, "\n";  # sunset-42

Две вещи, которые стоит знать. Во-первых, decode_base64url хорошо ладит с вводом без заполнения, - это та форма, которую вы действительно найдёте в дикой природе, так что ритуал «сначала восстановить заполнение», которого требуют другие языки, здесь не нужен; ввод с заполнением тоже работает. Во-вторых, стандартный декодер - зверь совсем другой: скормите ему base64url-сегмент, и получите молча неверные байты, потому что символы - и _ выбрасываются как шум, а остальное всё равно декодируется. Используйте правильный декодер либо нормализуйте вручную, если вас прижали к легаси-пути кода:

my $seg = "ab-cd_efgh";
$seg =~ tr{-_}{+/};                    # URL-безопасные буквы, возвращены домой
$seg .= "=" x (-length($seg) % 4);     # заполнение восстановлено для стандартного декодера
my $raw = decode_base64($seg);
print unpack("H*", $raw), "\n";  # 69bf9c77f79f82 - семь байтов, снова в стандартном алфавите

С base64url вы столкнётесь сразу же: в JWT, токенах, которые выдаёт каждый современный API, и в любом непрозрачном ID, живущем в URL: одиннадцатисимвольные ID видео, UUID, хранящиеся в URL-безопасном алфавите (на CPAN для этого как раз есть Data::UUID::Base64URLSafe), и ключи баз данных, которым нужно пережить адресную строку. А если у вас старый Perl, более ранний, чем эти функции в ядре, отдельный модуль MIME::Base64::URLSafe 2006 года, порт urlsafe-кодека Python, даёт urlsafe_b64encode и urlsafe_b64decode; на любой версии с 3.11 и дальше встроенные функции - выбор получше.

JWT: читаем заголовок и пелод

JSON Web Token - это структурно два куска JSON в маскировке плюс криптографическая квитанция. Компактная форма из RFC 7515 - это три base64url-сегмента, склеенные точками: защищённый заголовок, пелод и подпись. Разобрать и прочитать один - три строки:

use MIME::Base64 qw(decode_base64url);
use JSON::PP;
my $jwt = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJob21lciJ9.uzM6l0c4...";
my ($head_b64, $claims_b64, $sig_b64) = split /\./, $jwt, 3;
my $head   = decode_json(decode_base64url($head_b64));
my $claims = decode_json(decode_base64url($claims_b64));
print $claims->{sub}, " (", $head->{alg}, ")\n";  # homer (HS256)

Обратите внимание на разделение труда: decode_base64url превращает каждый сегмент в байты, а decode_json из ядерного модуля JSON::PP, который есть с Perl 5.14, превращает байты заголовка и пелода в Perl-структуры данных. Сегмент подписи тоже base64url, но это криптографическое хеш-значение, так что декодируют только первые два сегмента, а третий отдают правильной библиотеке.

Подвох тот, который все забывают: читаемое не значит валидное. Заголовок и пелод читаемы по замыслу, - значит, их может переписать любой, - а подпись - единственное доказательство. Всё, что по-настоящему важно, проверяйте, а не просто декодируйте. Модуль CPAN Crypt::JWT, построенный на CryptX, делает всю работу:

use Crypt::JWT qw(decode_jwt);
my $claims = decode_jwt(
  token        => $jwt,
  key          => $secret,
  accepted_alg => "HS256",
);

При плохой подписи он падает с ошибкой, а закрепление accepted_alg закрывает дыру путаницы алгоритмов, через которую атакующий подменяет токен на более слабый вариант. Рабочий процесс «декодировать и напечатать» годится, чтобы осмотреть токен во время звонка в поддержку. Но это не аутентификация.

Файлы: из .b64 обратно к оригиналу

Файлы - это там, где культура Perl-однострочников по-настоящему блестит, и вся работа влезает в одну команду. Флаг -0777 - секретный ингредиент, потому что он читает весь файл целиком в одну строку, вместо того чтобы кормить декодер построчно:

perl -MMIME::Base64 -0777 -ne 'print decode_base64($_)' < in.b64 > out

Построчная форма безопасна для одного конкретного класса файлов: тех, где каждая строка содержит кратное четырём число символов Base64, - это верно для любого правильно MIME-обёрнутого тела, поскольку 76 кратно 4. В тот самый момент, когда точки переноса становятся кривыми, - а в вручную обёрнутых файлах они почти всегда кривые, - построчное декодирование начинает производить заполнение посреди данных. Режим чтения целиком не требует таких условий, - вот почему он и есть выбор по умолчанию:

perl -MMIME::Base64 -ne 'print decode_base64($_)' < in.b64 > out

Внутри скрипта паттерн - стандартная Perl-танцовка с файлами, с одной тихой, но важной деталью: слои :raw на обоих дескрипторах, чтобы Perl никогда не пытался интерпретировать байты как платформенный текст по пути на входе и на выходе:

use MIME::Base64 qw(decode_base64);
use Digest::SHA qw(sha256_hex);
open my $in, "<:raw", $ARGV[0] or die $!;
local $/;
my $blob = <$in>;
close $in;
my $decoded = decode_base64($blob);
print sha256_hex($decoded), "\n";  # сравните с контрольной суммой отправителя
open my $out, ">:raw", $ARGV[1] or die $!;
print {$out} $decoded;
close $out;

Строка с хешем делает больше, чем хвастается. Поскольку декодер принимает почти что угодно, совпадение контрольной суммы с той, что опубликовал отправитель, - единственное доказательство, что переезд прошёл байт в байт. Для по-настоящему гигантских файлов построчный цикл - альтернатива с низкой памятью, при условии что переносы ложатся на границы по четыре символа, а MIME::Base64::decoded_base64_length скажет, насколько большим будет вывод, прежде чем вы решите вопрос с буфером.

PEM-броня: снять оболочку, сохранить DER

Файлы .pem в любом стеке безопасности - это тот же Base64, облачённый в броню: строка заголовка, строка подвала и тело, перенесённое по 64 символа, по старой конвенции Privacy Enhanced Mail. Обёртка - единственная интересная часть, потому что декодер модуля вообще не заботится о длине строк:

use MIME::Base64 qw(decode_base64);
open my $fh, "<:raw", "cert.pem" or die $!;
local $/;
my $blob = <$fh>;
close $fh;
my @body = grep { !/^-----/ && /\S/ } split /\n/, $blob;
my $der = decode_base64(join "", @body);
print length($der), " bytes of DER\n";

Строки BEGIN и END счищаются, остальное склеивается в одну строку, и все переводы строк по дороге игнорируются. Для повседневной работы с сертификатами инструменты OpenSSL уже делают это за вас; восемь строк выше - тот самый паттерн, который стоит помнить, когда сырые байты DER нужны вам самим: для хеша, отпечатка или сравнения.

Data URI: изображения, которые несут свой собственный адрес

Схема data: из RFC 2397 встраивает пелод прямо в URL: data:, необязательный медиатип, необязательный флаг ;base64, запятая и данные. Бинарные медиа, такие как изображения, используют флаг, так что пелод - это стандартный алфавит с заполнением, и обычный декодер справится с ним после небольшого разреза:

use MIME::Base64 qw(decode_base64);
my $uri = "data:image/png;base64,iVBORw0KGgo...";
$uri =~ s/^data:[^,]+,// or die "not a data URI";
my $raw = decode_base64($uri);
print unpack("H8", $raw), "\n";  # 89504e47: магические байты PNG

Проверка магических байтов - правильный ход. Если эти первые восемь шестнадцатеричных символов не 89504e47, то изображение не PNG, каким бы его ни называл медиатип, а декодер, который никогда не жалуется, как раз и позволяет такую тихую ложь.

Родственники и ископаемые: uuencode и прочие алфавиты

До того как Base64 победил, классический UNIX-способ отправить бинарник по почте - это был uuencode, и вы всё ещё встретите его в старых почтовых списках и старых инструментах. Хорошая новость: в Perl есть встроенный декодер для него, без всякого модуля, - спасибо шаблону u в pack и unpack:

my $uu   = pack("u", "Hello, World!");
print $uu, "\n";  # -2&5L;&\L(%=O<FQD(0`` плюс перевод строки
my $back = unpack("u", $uu);
print $back, "\n";  # Hello, World!

Эти два вызова - точные обратные друг к другу, и это вся история, которая вам нужна, - а классическая команда uuencode из UNIX-инструментария просто оборачивает голые строки в заголовок begin и подвал end, так что пелод, который вы декодируете, - это часть между ними.

У Base64 есть и диалектные родственники, и знание того, какой декодер что ест, экономит вам сессию отладки:

Диалект Перенос Где вы его встретите Что делает decode_base64
MIME (RFC 2045) 76 символов тела электронных писем декодирует как есть: переносы строк и CRLF игнорируются
PEM (RFC 1421) 64 символа сертификаты и ключи декодирует как есть
PKIX (RFC 7468) 64 символа текстовые структуры X.509 декодирует как есть
броня OpenPGP (RFC 9580) 76 символов плюс строка CRC24 ключи и подписи PGP декодирует как есть, строку контрольной суммы просто игнорирует
IMAP (RFC 3501) без переноса имена почтовых ящиков не тот алфавит: слэш становится запятой, сначала переведите буквы

Итог: для любого варианта стандартного алфавита, который отличается только переносом строк, одного снисходительного декодера хватает на все. Переводить символы первым делом нужно лишь тогда, когда меняется сам алфавит.

Конфигурация, базы данных и переменные окружения

Контейнерные платформы, облачные консоли и удивительно много файлов конфигурации хранят учётные данные и небольшие документы как непрозрачные Base64-строки, потому что глыба букв и цифр выглядит менее опасно, чем тот пароль, которым она является. Декодирование всегда состоит из одних и тех же двух шагов: decode_base64 плюс решение о кодировке:

use MIME::Base64 qw(decode_base64);
use Encode qw(decode);
my $secret = decode("UTF-8", decode_base64($config->{api_key}));

Причина, по которой формат так популярен именно здесь, - та самая, о которой предупреждает RFC 4648: люди перестают замечать, что данные читаемы. Поэтому относитесь к декодированному выводу как к конфиденциальному с того момента, когда он возвращён, и держите и глыбу, и её результат подальше от файлов логов, оповещений и дампов отладки.

Та же форма встречается и в базах данных, где бинарные данные часто ездят в колонке TEXT как Base64, потому что колонка не может пообещать, что пропустит произвольные байты без изменений:

use MIME::Base64 qw(decode_base64);
my $icon = decode_base64($row->{icon_data});
open my $fh, ">:raw", "icon.png" or die $!;
print {$fh} $icon;
close $fh;

Электронная почта: MIME-части и вложения

Электронная почта - это то место, где Base64 получил своё имя, и снисходительность модуля спроектирована именно под этот трафик. MIME-часть с Content-Transfer-Encoding: base64 прибывает строками по 76 символов текста, завершаемого CRLF, и декодер съедает всю оболочку как есть, переносы строк включительно:

use MIME::Base64 qw(decode_base64);
my $part_body = "SGVsbG8sIHF1ZXJ5IQpUaGlzIE1JTUUgcGFydCB0cmF2ZWxsZWQgYXMgYmFzZTY0LCB3cmFwcGVk";
$part_body .= "\r\nIGF0IDc2IGNoYXJhY3RlcnMsIENSTEYgYmV0d2VlbiBsaW5lcy4=";
my $text = decode_base64($part_body);
print $text;  # исходное двухстрочное тело сообщения

Если вы собираете или разбираете почту фреймворком, вы не делаете ничего из этого вручную: MIME::Lite сам кодирует вложение в Base64, когда вы передаёте Encoding => "base64" в attach, а Email::MIME делает то же самое автоматически. Ручная версия выше - для той почты, которая прибывает сырым текстом в лог, тикет или пересланное сообщение, - а на практике это очень много почты.

Подвохи: полный сбор, отсортированный по частоте укусов

Модуль достаточно мал, чтобы запомнить его наизусть, так что вот весь список ловушек в одном месте, отсортированный примерно по тому, как часто он кусается:

Подвох Что происходит Как исправить
base64url-сегмент, скормленный стандартному декодеру - и _ выбрасываются как шум, а остальное декодируется в молча неверные байты используйте decode_base64url либо сначала переведите алфавит и восстановите заполнение
Доверие тишине на повреждённом вводе чужие символы, усечение и неверный алфавит декодируются без единого предупреждения сначала прогоните строгую проверку, а если оригинал доступен, - подтвердите хешем
Не-ASCII-символы внутри глыбы засовшаяся буква с диакритикой или вставленный Unicode-пробел молча игнорируются, сжимая результат без комментария та же строгая проверка отклоняет всё, что вне 7-битного алфавита
Отношение к результату как к тексту на байтах не стоит флаг UTF-8, так что length() считает байты, а строковые функции получают неверную картину до любой обработки текста прицепите decode("UTF-8", $raw) или выбранную вами кодировку
Двойное кодирование по пути наружу пропуск уже UTF-8 байтов через encode("UTF-8", ...) превращает Hëllo в Hëllo кодируйте символы, никогда сырые байты, - и при сомнении проверяйте флаг через utf8::is_utf8()
Построчное декодирование файла с кривыми переносами строки, которые не заканчиваются на границе в четыре символа, производят заполнение посреди вывода читайте целиком через -0777 либо гарантируйте переносы по четыре символа
Возврат несостоявшегося совпадения регулярного выражения в числовом контексте подпрограмма, заканчивающаяся return $x =~ /.../ и передающая результат в printf, поднимает вводящее в заблуждение предупреждение «не хватает аргумента в printf» приведите совпадение: return $x =~ /.../ ? 1 : 0
Старый код, который ждёт старое предупреждение скрипты до 3.11, которые опирались на ворчание «преждевременный конец base64-данных» под -w, теперь не видят ничего добавьте собственную строгую проверку; предупреждение ушло навсегда
Предположение, что декодирование - это проверка декодер принимает почти что угодно и ничего по этому поводу не говорит совпавшая контрольная сумма или проверенная подпись - единственное важное доказательство
Логирование того, что вы декодируете формат ничего не прячет, и файл лога - ровно то место, где его найдёт следующий человек держите декодированные секреты подальше от логов, оповещений и дампов отладки

Хорошие привычки

Привычки, которые не дают Base64 ни разу не перехитрить ваши скрипты:

  • Ожидайте байты, всегда. Пишите код, который знает, что decode_base64 возвращает сырые октеты, и явно прицепляйте decode своей кодировки, вместо того чтобы надеяться, что терминал сделает всё правильно.
  • Называйте свою кодировку. По умолчанию UTF-8, а меняйте только когда данные говорят иначе. Строгий сбой decode("UTF-8", ..., Encode::FB_CROAK) - это фича: она говорит вам, что байты не те, что вы предполагали.
  • Совмещайте алфавит с источником. decode_base64url для URL, токенов и ID; decode_base64 для всего остального. Два алфавита не взаимозаменяемы, и декодер не скажет вам, когда вы ошибётесь в выборе.
  • Проверяйте до декодирования. В этом модуле нет флага строгого режима, так что небольшая проверка - ваш охранник на входе.
  • По умолчанию читайте файлы целиком. -0777 или local $/ = undef убирают целый класс багов точек переноса, а цена в памяти для файлов, которые вы реально декодируете, не является проблемой.
  • Используйте :raw на каждом файловом дескрипторе. Бинарь на входе, бинарь на выходе. Текстовые слои - для людей, а не для байтов.
  • Подтверждайте хешем. Когда оригинал доступен, совпавшая контрольная сумма - единственное доказательство байт-в-байт декодирования.
  • Никогда не логгируйте то, что декодируете. Формат ничего не прячет.

Краткая история, рассказанная журналом изменений

Формат старый, а отношения Perl с ним ещё старше, чем кажется. Несколько проверенных дат, по порядку:

  • C-код старше Perl 5. Быстрый декодер внутри модуля происходит от кода в metamail, почтовой программе Bellcore, авторское право на которую оформлено в 1991 году, за три года до первого релиза Perl 5. Когда вы сегодня вызываете decode_base64, работу делает кусочек девяностых.
  • Рождение в веб-инструментах. Модуль начался как LWP::Base64 внутри libwww-perl в середине девяностых, авторство - Мартин Костер и Йорг Райхельт, - и в апреле 1997 года он вырос в собственную CPAN-дистрибуцию MIME::Base64, версия 2.00, с записью в журнале изменений based on libwww-perl-5.08.
  • Эра предупреждений. С 2.03 в 1997 году усечённый ввод вместо падения вызывал предупреждение «преждевременный конец base64-данных» под -w, а 2.11 в 1999 году поправил сборки, которые предупреждали про вполне исправные данные. Для декодеров это была более нервная декада.
  • В ядре с 2002 года. Perl 5.8 взял модуль в ядро дистрибутива, а синхронизация 2.13 с ядром в том же декабре принесла поддержку EBCDIC, - вот почему кодер и декодер до сих пор работают на мейнфреймах.
  • URL-безопасный диалект вошёл в ядро Perl в 2010 году. Версия 3.11 добавила decode_base64url и её сестру, - четыре года после того, как отдельный модуль MIME::Base64::URLSafe появился на CPAN в 2006 году, в тот самый год, когда RFC 4648 кодифицировал диалект.
  • Затихание. Тот же выпуск 3.11 убрал даже старое предупреждение об усечении, на случай если подозрительный ввод был намеренным, - и каждый релиз с тех пор, включая текущую ветку 3.16 с 2020 года, держит декодер вежливым и молчаливым.

Факты для любопытных, конкретно про Perl

Чтобы завершить экскурсию, вот курьёзы, которые делают эту историю хорошей:

  • Декодируйте имя самого формата. decode_base64("YmFzZTY0") возвращает base64. Так было с 1997 года и так будет вечно.
  • Декодер - вежливый призрак. За всю свою историю в 3.x он ни разу не поднял исключение на плохом вводе. Повреждено, усечено, неверный алфавит: он декодирует всё и ни на что не жалуется, - поведение, которое журнал изменений в 2010 году осознанно зацементировал.
  • MIME-перенос кратен четырём не случайно. Ограничение в 76 символов - это девятнадцать трёхбайтовых групп, 57 байтов всего, помноженные на четыре символа, - поэтому построчное декодирование безопасно для любого правильно обёрнутого MIME-тела и небезопасно для всего остального.
  • uuencode никогда не печатает строчную букву. Его алфавит обрывается на подчёркивании, - поэтому старые uuencoded-файлы выглядят так, будто их набирала машина с зажатой Caps Lock, и поэтому в Perl до сих пор живёт встроенный декодер для формата старше интернета.
  • Perl когда-то поставляла собственную команду decode-base64. Релизы с 2.14 в 2003 году по 3.05 в 2004 году включали скрипты encode-base64, decode-base64 и их quoted-printable-близнецов; 3.06 в 2005 году перенесла их в отдельную дистрибуцию MIME-Base64-Scripts. Если вы найдёте старую установку, где эта команда лежит на PATH, теперь вы знаете, откуда она.
  • ID видео YouTube - это base64url в маскировке. Одиннадцатисимвольный ID в вашей адресной строке - это 64-битное число в URL-безопасном алфавите со срезанным заполнением, - так что у каждого просмотренного вами видео в URL есть Base64-строка, и decode_base64url умеет её читать.
  • Снисходительность - это стандарт, а не баг. MIME-правило «будь либеральным в том, что принимаешь» - причина, по которой этот декодер пережил три декады мусорных данных, и причина, по которой RFC 4648 предупреждает, что ту же снисходительность можно обратить в скрытый канал, если доверять недоверенному вводу.

Так что в следующий раз, когда в ваш терминал приземлится строка из букв, цифр, плюсов и слэшей, вы знаете всю историю. Один вызов функции делает работу, декодер - вежливый призрак, который никогда не откажет, у base64url есть свой декодер, кодировка - решение, которое вы принимаете осознанно, файлы приходят сырыми и уходят сырыми, а хеш - единственное важное доказательство. А если однажды вам нужно будет пройти этот путь в обратную сторону, обернуть собственные сырые данные в текстовый конверт и отправить их в мир, связанная ниже статья про кодирование Base64 в Perl разберёт тот ритуал с той же глубиной.

Последнее обновление: 2026-09-08

Связанная статья: Кодирование Base64 в Perl: полное руководство