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

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

Где-то между вами и исходными данными стоит стена из символов: заглавные и строчные буквы, цифры, может быть, плюс или слэш или дефис, а то и знак равенства, примостившийся в конце. Ваш редактор понятия не имеет, какой это тип файла. Ваша база данных затолкала его в текстовую колонку. Он прилетел в HTTP-заголовке, в URL, в ключе YAML или в тикете поддержки с вложением .b64. Вы узнаёте его в ту же секунду - Base64 - и теперь вам нужны байты обратно. В Ruby до них - один require и один вызов метода.

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

Хорошие новости: в каждой установке Ruby уже лежит полный набор инструментов для декодирования. Модулю Base64 нечего устанавливать, а его три декодера настолько малы, что весь их исходный код можно прочесть за один присест. Предупреждение: тот декодер, к которому вы потянетесь первым, - тот самый, который никогда не жалуется. Для электронной почты это прекрасное свойство, а для безопасности - ужасное. К концу этого руководства вы будете точно знать, что принимает каждый декодер, как превратить возвращённые им байты в текст, который Ruby допустит в оборот, и как работать с каждым из тех пелодов, которые Ruby-разработчики декодируют на самом деле: JWT, заголовки аутентификации, data URI, тела писем, PEM-броня, файлы, конфигурационные блобы и гигантские.

Знакомство с набором инструментов

Всё начинается с require. Никакой установки, никаких платформенных особенностей, никакого нативного расширения, которое нужно собирать:

require "base64"
puts Base64::VERSION
# => 0.2.0 в стандартной Ruby 3.3, например

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

Декодер Отношение к чужим символам Правила заполнения Что, когда что-то не так
Base64.decode64(str) игнорирует всё, чего нет в стандартном алфавите, включая переводы строк и пробелы какое угодно, даже неправильное заполнение ничего - он никогда не бросает исключение, а просто возвращает то, что смог декодировать
Base64.strict_decode64(str) отклоняет любой символ вне стандартного алфавита должно быть в наличии и точно верным бросает ArgumentError
Base64.urlsafe_decode64(str) принимает URL-безопасный алфавит и стандартный, всё остальное отклоняет необязательно, но если присутствует - должно быть верным бросает ArgumentError

Если вам важно знать, что делают ваши инструменты под капотом, то вся декодирующая часть модуля - это тонкая обёртка над двумя шаблонами ядрового механизма pack/unpack, который реализован на C внутри ядра Ruby:

# вся декодирующая часть модуля, сжато
def decode64(str)
  str.unpack1("m")
end
def strict_decode64(str)
  str.unpack1("m0")
end

Шаблон m - снисходительный читатель, m0 - строгий, и эта разница в один символ объясняет весь разброс характеров между двумя первыми декодерами. Поскольку тяжёлую работу выполняет ядро на своей скорости, модуль остаётся чистым Ruby, но при этом переваривает мегабайты за однозначное число миллисекунд.

decode64: хамелеон

Base64.decode64 - это декодер, который говорит да всему. Подайте ему чистый пелод - он его декодирует. Подайте MIME-подобный блоб, доверху набитый переводами строк, - он пожимает плечами. Подайте строку, которая вообще не Base64, - он отдалит то, что сумел из неё выжать, и не выдаст ни единого предупреждения:

require "base64"
Base64.decode64("aGVsbG8gd29ybGQ=")
# => "hello world"
Base64.decode64("Zm9vCmJh\ncgptYW4=\n")
# => "foo\nbar\nman"

Вторая строка примера - весь его характер в одном действии. Декодер пропускает всё, что не входит в стандартный алфавит - переводы строк, пробелы, какой-нибудь управляющий символ, - и декодирует остальное. Это ровно то поведение, каким MIME-Base64 и должен обладать, поэтому decode64 - правильный инструмент для всего, что проехало через электронную почту.

Обратная сторона - в ней и кроется опасность. Поскольку декодер никогда не жалуется, он и никогда не скажет вам, когда вход был неверным:

Base64.decode64("not base64 at all!")
# => десять байтов совершенно правдоподобного мусора
Base64.decode64("====")
# => ""

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

Ещё одна особенность, о которой стоит знать, потому что это тот самый случай, который прячется в продакшене месяцами: декодирование останавливается на первом символе =. Всё, что стоит после заполнения, - не ошибка; это просто никогда не читается:

Base64.decode64("aGVsbG8=Zm9vYmFy")
# => "hello"   часть «Zm9vYmFy» невидима для декодера

strict_decode64: смотритель

Base64.strict_decode64 - это декодер с клипбордом. Он принимает только стандартный алфавит (A-Z, a-z, 0-9, плюс, слэш), требует, чтобы заполнение было точно верным, и отказывается выдавать хоть один байт, если нарушено хоть какое-то правило:

Base64.strict_decode64("aGVsbG8gd29ybGQ=")
# => "hello world"
Base64.strict_decode64("aGVsbG8gd29ybGQ")
# => вызывает ArgumentError
Base64.strict_decode64("Zm9vCmJh\ncgptYW4=")
# => вызывает ArgumentError

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

begin
  Base64.strict_decode64("aGVsbG8")
rescue ArgumentError => e
  puts e.message
end
# => invalid base64

Смотритель следит даже за уголками формата, которые вы бы и не подумали проверить. Когда строка Base64 заканчивается заполнением, часть битов последнего символа никогда не используется, и RFC предписывает корректному кодировщику обнулять эти биты. Ruby проверяет:

Base64.strict_decode64("QQ==")
# => "A"
Base64.strict_decode64("QR==")
# => вызывает ArgumentError (биты дополнения не обнулены)

Вторая строка дала бы тот же байт, что и первая, будь декодер небрежным. Ruby небрежным не является. На практике это делает strict_decode64 правильным дефолтом для любого входа, который вы не закодировали сами: опечатки, обрезка и неверный алфавит превращаются в громкие, ловимые ошибки вместо тихой порчи.

urlsafe_decode64: дипломат

Base64.urlsafe_decode64 существует для пелодов, которым предстоит путешествовать по местам, где + и / - зарезервированные символы: URL, токены, идентификаторы баз данных. Внутри метод переводит URL-безопасный алфавит (дефис и подчёркивание) обратно в стандартный, нормализует заполнение и передаёт результат строгому декодеру:

Base64.urlsafe_decode64("SGVsbG8gd29ybGQ")
# => "Hello world"
Base64.urlsafe_decode64("SGVsbG8gd29ybGQ==")
# => вызывает ArgumentError (пятнадцать символов требуют один знак дополнения, а не два)

Первый пример показывает его самую полезную черту: ввод без заполнения вполне годится. Если строка без заполнения и её длина не кратна четырём, декодер сам дописывает недостающие знаки = - именно так выглядят JSON Web Tokens, главный потребитель URL-безопасного Base64. Если же заполнение присутствует, оно должно быть корректным, как и у строгого декодера.

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

Base64.urlsafe_decode64("aGVsbG8=")
# => "hello"   стандартный алфавит тоже принимается

Эта снисходительность удобна, но означает, что по этому методу нельзя определить, из какого алфавита пришёл пелод. Если это важно, сначала сами проверьте символы, а потом декодируйте.

И в отличие от decode64, дипломат не прощает пробельных символов. Перевод строки где угодно в URL-безопасном пелоде бросает ArgumentError, поэтому, если вход пришёл из файла с переносами, сначала удалите переводы строк.

Байты - не текст: шаг с кодировкой

Вот тот самый шаг, на котором спотыкаются даже опытные разработчики, потому что Ruby делает его видимым. Декодированная Base64-строка всегда помечена кодировкой ASCII-8BIT (также известной как BINARY), вне зависимости от того, были ли исходные данные PNG, пелод JWT или любовное письмо в UTF-8:

bin = Base64.decode64(Base64.strict_encode64("h\u{e9}llo"))
puts bin.encoding
# => ASCII-8BIT
puts bin.bytes
# => [104, 195, 169, 108, 108, 111]

Если пелод двоичный - картинка, zip-файл, хеш, - вы оставляете его как есть и пишете его через File.binwrite. Без преобразований и лишних вопросов. Если пелод текстовый, байты почти наверняка UTF-8, и нужно сказать Ruby об этом:

text = Base64.decode64(payload)
text.force_encoding("UTF-8")
if text.valid_encoding?
  puts text
else
  puts "not valid UTF-8 after all"
end

Два вызова делают разную работу. force_encoding лишь перевешивает ярлык на байтах, valid_encoding? уже проверяет, что они образуют настоящий UTF-8. Выполняйте их в этом порядке, потому что валидировать BINARY-строку первым делом - значит не иметь для проверки ничего. И одна маленькая ловушка со сравнением, которую стоит запомнить на всю жизнь: Ruby считает BINARY-строку равной UTF-8-строке только тогда, когда обе чистый ASCII, поэтому перевешивайте ярлык, прежде чем сравнивать декодированный текст с оригиналом:

decoded = Base64.decode64("aMOpbGxv")
puts decoded == "h\u{e9}llo"
# => false   те же байты, разные ярлыки
decoded.force_encoding("UTF-8")
puts decoded == "h\u{e9}llo"
# => true

JWT: чтение токена без ключа

JSON Web Token - это три строки Base64, скреплённые точками: заголовок, пелод, подпись. Две первые - это JSON-документы в URL-безопасном Base64 без заполнения, а значит, токен читается кем угодно, кто его видит, - в том числе вами, вообще без библиотек:

require "base64"
require "json"
token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIn0.dW5zaWduZWQ"
header_part, payload_part = token.split(".")[0, 2]
JSON.parse(Base64.urlsafe_decode64(payload_part))
# => {"sub"=>"1234567890", "name"=>"Alice"}

Для настоящих задач вы воспользуетесь пакетом jwt, который берёт на себя часть, которая вас действительно защищает, - подпись, - и проверки claim:

# В Gemfile: gem "jwt"
require "jwt"
token = JWT.encode(
  { sub: "1234567890", name: "Alice", exp: Time.now.to_i + 3600 },
  "my-secret-key",
  "HS256"
)
payload, header = JWT.decode(token, "my-secret-key", true, algorithm: "HS256")
puts payload["name"]
# => Alice

Два замечания о безопасности здесь уместны, потому что оба уже обходились людям реальными инцидентами. Во-первых, пелод не зашифрован: его декодирование - это чтение, а не взлом, и единственная защита - подпись, так что никогда не относите декодированный пелод к доверенному входу. Во-вторых, закрепите алгоритм в JWT.decode ровно так, как показано. Если опустить его, способ проверки решит собственный заголовок токена, и именно эту малость гибкости эксплуатируют знаменитые атаки на путаницу с алгоритмом JWT.

Basic auth: пароль, спрятанный на видном месте

Самый старый заголовок аутентификации в вебе - это и есть Base64. HTTP Basic auth отправляет учётные данные в виде user:password - закодированных, после слова Basic, - а заголовок едет в каждом запросе, поэтому он выплывает в каждом логе, который вам доведётся отлаживать. Декодировать такой заголовок - задача на срезание и нарезку:

require "base64"
header_value = "Basic YWxpY2U6czNjcjN0IQ=="
b64 = header_value.sub("Basic ", "")
decoded = Base64.decode64(b64)
user, password = decoded.split(":", 2)
puts user
# => alice
puts password
# => s3cr3t!

Лимит 2 в split важен: пароль вполне законно может содержать двоеточия, а резать нужно только по первому. Своя стандартная библиотека Ruby собирает этот заголовок в обратную сторону - в Net::HTTP, используя ядровой шаблон pack напрямую:

require "net/http"
request = Net::HTTP::Get.new("https://example.org/api")
request.basic_auth("alice", "s3cr3t!")
puts request["Authorization"]
# => Basic YWxpY2U6czNjcjN0IQ==

И замечание о безопасности, которое стоит произнести даже при всей его очевидности: Base64 - это переводчик, а не замок. Basic auth приемлем только поверх HTTPS. Кодирование существует для того, чтобы учётные данные могли ехать по каналу печатаемым текстом, а не для того, чтобы они оставались секретом.

Data URI: изображение, которое не файл

Data URI прячет целый файл внутри URL: тип медиа, слово base64, запятая и закодированные байты. Браузеры отрисовывают их в тегах img и в CSS, а HTML-приложения из одного файла их обожают, потому что делать второй запрос не нужно. Собрать такой URI в Ruby - одна строка:

require "base64"
png = File.binread("logo.png")
data_uri = "data:image/png;base64,#{Base64.strict_encode64(png)}"

Декодировать его - обратное дело, с двумя деталями, на которых люди спотыкаются. Запятая - разделитель, так что режьте ровно один раз; часть с типом медиа может быть чем угодно, вплоть до полного отсутствия:

data_uri = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
media_part, b64 = data_uri.split(",", 2)
puts media_part
# => data:image/png;base64
bytes = Base64.strict_decode64(b64)
File.binwrite("restored.png", bytes)

Здесь используйте strict_decode64, а не decode64: пелод data URI - одна чистая строка, и если она испорчена, вам нужна громкая ошибка. И помните о налоге на размер: каждое встраиваемое изображение растёт примерно на треть, - поэтому data URI идеальны для фавиконок, маленьких логотипов и шрифтов, и плохая идея для крупных обложечных фото.

Электронная почта: строки по шестьдесят символов и пакет mail

Base64 придумали для электронной почты, и шрамы налицо. SMTP проектировали под короткие строки из семибитного текста, поэтому MIME-Base64 нарезает свой вывод на короткие строки, и корректный декодер обязан игнорировать переводы строк. Ruby-метод decode64 ведёт себя ровно так, поэтому перенесённое MIME-тело - лёгкая добыча:

body = "Zm9vCmJh\ncgptYW4=\n"
Base64.decode64(body)
# => "foo\nbar\nman"

Написать такое вручную вам, скорее всего, не придётся. Пакет mail берёт на себя всю MIME-работу: вложения кодируются в Base64 автоматически, строки переносятся каждые 60 символов, с запасом укладываясь в лимит MIME в 76 символов, и прикладываются правильные заголовки:

# В Gemfile: gem "mail"
require "mail"
message = Mail.new do |m|
  m.from = "dev@example.org"
  m.to = "ops@example.org"
  m.subject = "Binary report"
  m.add_file("report.bin")
end
puts message.encoded
# вложение несёт Content-Transfer-Encoding: base64

Та же уловка прячется внутри заголовков почты. Тема письма в не-ASCII приходит в виде кодированного слова по RFC 2047: кодировка, буква B и Base64 между вопросительными знаками. Декодировать его вручную - небольшое упражнение в строковой хирургии:

header_value = "=?UTF-8?B?w7wgc2VjcmV0cw==?="
charset, kind, b64 = header_value.sub(/\A=\?/, "").sub(/\?=$/, "").split("?")
text = Base64.decode64(b64).force_encoding(charset)
puts text
# => ü secrets

PEM: ключи и сертификаты в броне

Ключи и сертификаты проводят большую часть жизни внутри PEM-брони: строка BEGIN, блок Base64 и строка END. Броня родом из 1980-х - Privacy-Enhanced Mail, от которой начинается вся родословная Base64, - но это всё ещё тот формат, в обёртке которого ваши файлы .crt и .key ходят по сей день.

Вручную декодировать PEM-файл - значит снять броню и позволить снисходительному декодеру переварить переводы строк:

require "base64"
pem = File.read("server.key")
body = pem.lines
  .reject { |line| line.start_with?("-----") || line.strip.empty? }
  .join
key_bytes = Base64.decode64(body)

Для реального применения вы, как правило, пропустите ручной шаг и передадите всю PEM-строку OpenSSL, который читает броню сам:

require "openssl"
key = OpenSSL::PKey.read(File.read("server.key"))
puts key.class
# => OpenSSL::PKey::RSA, или чем там окажется ключ

Единственная деталь совместимости, о которой стоит знать: строки PEM классически по 64 символа, а декодер в любом случае игнорирует переводы строк, так что обёртка по 60 символов или одна огромная строка декодируются не хуже.

Файлы и конвенция .b64

Самый распространённый формат файлов в мире Base64 - обычный текстовый файл с расширением .b64 (или иногда .base64), содержащий один закодированный пелод. Прочитать один такой файл - значит пройти три шага:

require "base64"
encoded = File.read("payload.b64")
bytes = Base64.decode64(encoded)
File.binwrite("payload.bin", bytes)

На выходе используйте File.binwrite - декодированный PNG или zip - это двоичные данные, а запись в текстовом режиме испортит их на платформах, которые переводят окончания строк. Если ваш файл .b64 пришёл от инструмента, который переносил строки, decode64 разберётся с переводами строк бесплатно. Если хотите валидировать, а не прощать, читайте файл в двоичном режиме и удалите переводы строк перед строгим декодированием:

encoded = File.binread("payload.b64")
clean = encoded.delete("\r\n")
bytes = Base64.strict_decode64(clean)

Двоичное чтение важно на Windows, где текстовый режим переписывает окончания строк CRLF в LF - именно то изменение, которое вы не хотите видеть внутри строки, которую вот-вот станете валидировать.

URL-безопасный Base64: пелоды, путешествующие в ссылках

Вот декодерская сторона URL-безопасного варианта, потому что выбор, который вы здесь сделаете, меняет то, к какому из трёх декодеров вы потянетесь. URL-безопасный Base64 (RFC 4648, раздел 5) подменяет два символа, которых URL не любят: + становится -, / становится _, - и обычно сбрасывает и заполнение. В Ruby вы встретите его в параметрах запроса, значениях cookie, идентификаторах API, идентификаторах видео в стиле YouTube и, конечно, в JWT.

Вот как ведут себя три декодера на одинаковых входах - именно на этих различиях и рождаются баги:

Вход decode64 strict_decode64 urlsafe_decode64
aGVsbG8= (стандартный, с заполнением) "hello" "hello" "hello"
aGVsbG8 (без заполнения) "hello" ArgumentError "hello"
SGVsbG8gd29ybGQ- (дефис в последней группе) "Hello world" (не хватает одного байта!) ArgumentError 12 байтов - правильный ответ
aGVsbG8=\n (перевод строки в конце) "hello" ArgumentError ArgumentError
aGVs!bG8= (лишний знак восклицания) "hello" ArgumentError ArgumentError

Третья строка - та, что кусает. URL-безопасный пелод, декодированный стандартным декодером, молча теряет последний байт вместо того, чтобы бросить что-нибудь, потому что decode64 просто игнорирует дефис. Если пелод может прийти из URL, декодируйте его через urlsafe_decode64.

Практическое замечание: если вам когда-нибудь придётся перенести URL-безопасный пелод в контекст, где понимают только стандартный алфавит (библиотека, чужая система), классический трюк совместимости - перевести алфавит и дополнить самому, - занимает три строки:

def standardize_urlsafe(b64)
  b64 = b64.tr("-_", "+/")
  b64 += "=" * ((4 - b64.length % 4) % 4)
  b64
end
Base64.strict_decode64(standardize_urlsafe("SGVsbG8gd29ybGQ"))
# => "Hello world"

Понадобится он редко - urlsafe_decode64 и так дополняет за вас, - но это паттерн, который стоит узнавать в чужом коде, и паттерн, к которому стоит тянуться, когда другая сторона ожидает стандартный алфавит.

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

Base64 появляется в конфигурации всякий раз, когда двоичным данным приходится жить внутри текстового документа. Файл .env, YAML-конфиг или JSON-блоб с настройками не могут безопасно нести сырые байты, поэтому байты кодируются, и что-то в вашем приложении должно декодировать их при старте:

require "base64"
b64 = ENV.fetch("APP_LOGO")
bytes = Base64.decode64(b64)
File.binwrite("logo.png", bytes)

YAML заслуживает отдельного упоминания, потому что в формате есть нативный двоичный тег. Когда вы выгружаете BINARY-строку, Psych записывает её как скаляр !binary с Base64 внутри, и при загрузке байты возвращаются в целости - никакой ручной кодировки:

require "yaml"
yaml_text = YAML.dump({ "logo" => File.binread("logo.png") })
puts yaml_text.lines.first(2)
# => "---"
# => "logo: !binary |-"
data = YAML.load(yaml_text)
puts data["logo"].encoding
# => ASCII-8BIT

В базах данных правило простое: если в вашей базе есть настоящий двоичный тип, используйте его. Base64 в TEXT-колонке - это паттерн, к которому тянутся, когда слой хранения понимает только строки: некоторые документные хранилища, JSON-подобные API или устаревшая схема, которую нельзя менять, - а цена - налог в треть на размер колонки, плюс дисциплина декодировать на входе и заново кодировать на выходе на каждом рубеже.

Крупные входы, ровная память

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

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

require "base64"
def decode_in_chunks(b64)
  b64.scan(/.{1,4}/).reduce("") do |result, group|
    result + Base64.strict_decode64(group)
  end
end
restored = decode_in_chunks(Base64.strict_encode64("a" * 1_000_000))
puts restored.length
# => 1000000

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

Однострочники для терминала

Чтобы декодировать что-нибудь в shell, не нужен файл со скриптом. Ruby может подтянуть модуль на лету:

ruby -rbase64 -e 'puts Base64.decode64(ARGV[0])' "aGVsbG8gd29ybGQ="
# => hello world

А для файлов передайте путь к файлу вместо самого пелода:

ruby -rbase64 -e 'print Base64.decode64(File.read(ARGV[0]))' payload.b64 > payload.bin

Здесь живут два подводных камня. Первый: если пустите данные через echo или любую текстовую команду, прицепится конечный перевод строки, и strict_decode64 на нём бросит исключение - используйте decode64 или chomp для входа:

echo "aGVsbG8gd29ybGQ=" | ruby -rbase64 -e 'print Base64.strict_decode64(STDIN.read.chomp)'

Второй: для двоичного вывода держите print вместо puts, потому что puts добавляет собственный перевод строки и испортит конец восстановленного файла.

Ловушки, на которые реально наступают Ruby-разработчики

  • decode64 никогда не бросает исключение. Мусор на входе - мусор на выходе. Если вход недоверенный, а вы молча принимаете испорченные байты, баг проявится через несколько недель в испорченном файле, а не на строке декодирования. Для всего, что вы не закодировали сами, по умолчанию выбирайте строгий декодер.
  • strict_decode64 и перевод строки в конце. Текстовые файлы, конвейеры echo и копипаст обожают заканчиваться переводом строки, и строгий декодер на нём бросает ArgumentError. Сначала chomp входа - или читайте в двоичном режиме и удаляйте переводы строк.
  • Забывать про шаг с кодировкой. Декодированная строка остаётся BINARY, пока вы сами не скажете иначе. Назначьте UTF-8 (и проверьте валидность), прежде чем обращаться к результату как к тексту, иначе в ту же секунду, как смешаете его с UTF-8 строками, получите кракозябры и Encoding::CompatibilityError.
  • Сравнение BINARY с UTF-8. Те же байты, разные ярлыки, и == отвечает false - если только строка не оказывается чистым ASCII. Перевешивайте ярлык перед сравнением.
  • URL-безопасный вход через не тот декодер. Дефисы и подчёркивания decode64 молча выбрасывает, так что URL-безопасный пелод возвращается короче на байт и испорченным, без единой ошибки. Используйте urlsafe_decode64.
  • Данные после заполнения невидимы. decode64 останавливается на первом =. Для MIME это отлично, а вот чтобы поймать пелод, который был обрезан, а потом повторно дополнен другим инструментом, - не годится.
  • Неканоничное заполнение молча принимается. Строка вроде QR== несёт биты заполнения, которые правильный кодировщик обнулил бы; decode64 охотно её декодирует, а strict_decode64 отклоняет. Никто никогда не скажет вам, что ваш кодировщик врал.
  • Чтение файлов в текстовом режиме на Windows переписывает окончания строк раньше, чем вы их увидите. Когда собираетесь валидировать, читайте файлы .b64 в двоичном режиме.

Хорошие привычки для стороны декодирования

  • Выбирайте декодер по источнику данных: strict_decode64 для всего недоверенного (и ловите ArgumentError как ветку невалидного ввода), urlsafe_decode64 для пелодов, родившихся в URL, decode64 - только для форматов, которые подлинно снисходительные, вроде MIME-тел.
  • В ту же секунду, как байты декодированы, решите, кто они: двоичные (оставьте ASCII-8BIT, пишите через File.binwrite) или текст (force_encoding в UTF-8, затем valid_encoding? перед использованием).
  • Никогда не декодируйте и не доверяйте. Пелод JWT читается ровно потому, что он Base64; подпись решает, настоящий он или нет. Base64-строка в конфигурационном файле - это данные, а не доказательство.
  • Когда пишете валидаторы, проверяйте их на скучных случаях: пустая строка, ввод без заполнения, ввод с переносами, URL-безопасный ввод и неправильное заполнение. Именно на этих случаях расходятся три декодера.

Краткая история Base64 в Ruby

Модуль Base64 является частью стандартной библиотеки Ruby уже более пятнадцати лет, и то, как он поставляется, менялось чаще, чем можно подумать:

  • 2008, Ruby 1.8.7: модуль поставляется с encode64, decode64, плюс два метода, которые больше не существуют: b64encode (перенос по заданной длине строки) и decode_b (декодирование заголовков почты по RFC 2047). Старые книги и даже некоторые старые Ruby-пакеты всё ещё ссылаются на них, и вызов любого из них сегодня - NoMethodError.
  • 2009, линия 1.9: прибывают strict_encode64, strict_decode64, urlsafe_encode64 и urlsafe_decode64, а два устаревших метода отправляются на пенсию (1.9.1 уже вышла с обеими переменами в январе 2009 года).
  • 2015, Ruby 2.3: urlsafe_encode64 получает ключевое слово padding:, позволяющее выводить результат без заполнения для токенов и URL.
  • 2020, Ruby 3.0: base64 выносится из стандартной библиотеки в собственный Ruby-пакет, версия 0.1.0, под репозиторием ruby/base64. Он поставляется как пакет по умолчанию, поэтому require "base64" всё так же просто работает.
  • 2023, Ruby 3.3: версия 0.2.0 добавляет Base64::VERSION и заметно более богатый набор документации.
  • 2024, Ruby 3.4: Ruby-пакет переклассифицирован из пакета по умолчанию в поставляемый пакет. Практическое следствие: в проектах на Bundler в Ruby 3.4 и новее указывайте gem "base64" в Gemfile (или установите его через gem install base64).
  • 2025, Ruby 4.0: выходит версия 0.3.0, которая, помимо прочего обслуживания, добавляет RBS-подписи типов.

Посреди всего этого один факт так и не менялся: модуль - это несколько десятков строк чистого Ruby, лежащих поверх ядровых шаблонов pack и unpack. Ни C-расширения, ни зависимостей, ни сборки - а счётчик загрузок на rubygems.org исчисляется сотнями миллионов.

Ruby-факты для любопытных

  • Декодирующая часть модуля - это два однострочных тела методов, str.unpack1("m") и str.unpack1("m0"), плюс urlsafe-вариант, который поверх строгого делает замену букв иправку заполнения. Можно удалить require и написать это самому.
  • Собственный Net::HTTP Ruby и вовсе не использует модуль Base64 для Basic auth - он вызывает шаблон pack напрямую: ["user:pass"].pack("m0").
  • Подписанные и зашифрованные cookie Rails под капотом - это Base64-строки: кодек сообщений ActiveSupport выбирает strict_encode64 для обычных cookie и urlsafe_encode64 с padding: false для подписанных идентификаторов, безопасных для URL. Скорее всего, вы уже декодировали такой, даже не зная об этом.
  • У каждого digest-класса есть метод base64digest - Digest::SHA256.base64digest("hello"), - однострочник для контрольных сумм, которым приходится жить в тексте.
  • Тег !binary в YAML - это Base64. Выгрузите BINARY-строку через Psych, и формат молча закодирует её за вас.
  • decode64 не заботится, по 60, 64 или 76 символов ваши строки, или же это одна огромная строка. Шаблон m пропускает переводы строк, так что перенесённый и неперенесённый ввод декодируются одинаково.

Идём дальше

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

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

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