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

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

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

А вот единственный поворот, который отличает Rust от большинства языков: в стандартной библиотеке нет Base64 вообще. В std не притаился ни один base64_decode(), и ни одна use std::... не способна его призвать. Экосистема остановилась на единственном крейте с простым именем base64, и он стал несущей конструкцией: версия 0.23.1 вышла 4 августа 2026 года, с момента первого релиза в декабре 2015 года крейт опубликовал 45 версий, а его счётчик загрузок стоит у отметки в 1,5 миллиарда. Почти наверняка вы уже декодируете Base64 через этот крейт, напрямую или через что-то вроде jsonwebtoken, pem или serde_with, которые все от него зависят.

Один крейт и его круг

Если Rust ещё нет на машине, его даёт ваша операционная система: rustc и cargo на Debian и Ubuntu, пакет или установщик на macOS и Windows, либо официальный установщик, который настраивает rustup:

# Debian / Ubuntu
sudo apt install rustc cargo
# либо официальный установщик, который настраивает rustup и cargo
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Затем крейт, внутри любого cargo-проекта. Эта единственная строка и есть вся установка, и она подтягивает ровно ноль зависимостей:

cargo new my-app
cd my-app
cargo add base64

Три необязательные фичи формируют сборку. std включена по умолчанию и даёт потоковые типы std::io, стандартные реализации Error и аллокацию кучи. alloc предоставляет аллоцирующие API для встроенных no_std-сборок без полной стандартной библиотеки. simd-unsafe включена по умолчанию и управляет доступом к векторизованным движкам, с которыми вы ещё познакомитесь. Минимальная поддерживаемая версия Rust - 1.71.0, так что любой свежий Rust его потянет. Вокруг центрального крейта кружит маленький круг специалистов, каждый - за свой краевой случай, который центр намеренно оставляет вам:

Крейт Версия (2026) Что он приносит За что тянуться
base64ct 1.8 Декодирование за постоянное время от проекта RustCrypto; API кучи прячутся за фичей alloc Декодируемые вами байты могут сливать информацию через время исполнения, например ключевой материал
data-encoding 2.11 Base64 в компании base32, hex и друзей, с снисходительными MIME-вариантами и кодированием/декодированием на уровне срезов Один компонент должен разобрать грязный, перенесённый на строки или мультипротоковый ввод
base64-turbo 0.3 Помолодее кодек с пиком выше 100 ГиБ/с, с ядрами AVX512, AVX2 и NEON плюс безопасный скалярный запасной вариант Пропускная способность - вот в чём вся суть, а стандартный движок не выжимает из процессора всего

Ни один из них не заменяет повседневную работу. Для подавляющего большинства Rust-программ base64 в одиночку - правильный и полный ответ, и остальная часть этой статьи использует этот единственный крейт для самого декодирования, прибегая к специалистам круга лишь тогда, когда дело шире, чем base64.

Три строки до ваших байтов

Девяносто процентов жизни декодирования умещаются в три строки. Канонический дымовой тест использует знаменитую строку TWFu:

use base64::prelude::*;
fn main() {
  let packed = "TWFu";
  let bytes = BASE64_STANDARD.decode(packed).expect("valid base64");
  println!("{}", String::from_utf8(bytes).expect("valid utf-8"));
}

На выходе будет Man, и в этом крошечном обряде стоит запомнить три вещи. Во-первых, decode() всегда отдаёт вам Vec<u8>, никогда не строку. Это фича, а не случайность: Base64 может нести фразу, JPEG или сертификат, и ни один из них до знакомства с содержимым не должен обрабатываться иначе. Во-вторых, прыжок от байтов к тексту - отдельный, намеренный шаг через String::from_utf8(), и именно на этом шаге живёт решение про кодировку. В-третьих, модуль prelude тихо выдаёт вам две вещи разом: движок BASE64_STANDARD и трейт Engine, методы которого вы вызываете. Если предпочитаете явные импорты, use base64::engine::general_purpose::STANDARD; в паре с use base64::Engine; - та же самая дверь, только с табличкой на ней.

А поскольку вы когда-нибудь обязательно будете декодировать то, что закодировали сами, вот круговой рейс, доказывающий, что оба направления сходятся. Кодирование имеет собственное полное руководство на сестринском сайте; здесь оно появляется лишь для производства тестовых данных:

use base64::prelude::*;
fn main() {
  let packed = BASE64_STANDARD.encode("Hello, world!");
  println!("{packed}");                             // SGVsbG8sIHdvcmxkIQ==
  let back = BASE64_STANDARD.decode(packed).unwrap();
  println!("{}", String::from_utf8(back).unwrap()); // Hello, world!
}

Держите TWFu в запасном кармане как дымовой тест для любого пути декодирования, который вы пишете: если он превращает TWFu в Man, машина честная.

Четыре способа сказать нет

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

Ввод В чём дело Точная ошибка
"SGVs bG8s" пробел просочился внутрь Invalid symbol 32, offset 4.
"SGVs\nbG8s" перенос строки просочился внутрь Invalid symbol 10, offset 4.
"SG=VsbG8="" заполнитель посреди строки Invalid symbol 61, offset 2.
"SGVsbG8sIHdvcmxkIQ==xx" мусор за хвостом заполнителя Invalid symbol 61, offset 18.
"S" одного символа не хватает, чтобы составить байт Invalid input length: 1
"SGV" три символа без заполнителя, который должен следовать за ними Invalid padding
"SGVs$bG8="" символ $ не входит в алфавит Invalid symbol 36, offset 4.

Обратите внимание, как сообщение Invalid symbol сообщает вам и значение провинившегося байта, и его смещение, так что можно сразу прыгнуть к месту преступления. Вариант InvalidLength - самый придирчивый: с версии 0.22.0 он срабатывает именно тогда, когда количество валидных символов невозможно, а это длина на один больше кратного четырём числа, тогда как прочие плохие длины проступают как ошибки заполнителя. Вот полный match на те дни, когда вы хотите обрабатывать каждый сбой по-своему:

use base64::DecodeError;
use base64::prelude::*;
fn triage(dirty: &str) {
  match BASE64_STANDARD.decode(dirty) {
    Ok(_) => println!("{dirty:?} sailed through"),
    Err(DecodeError::InvalidByte(offset, byte)) =>
      println!("{dirty:?}: symbol {byte} at {offset} is not in the alphabet"),
    Err(DecodeError::InvalidLength(symbols)) =>
      println!("{dirty:?}: {symbols} valid symbols is impossible"),
    Err(DecodeError::InvalidLastSymbol { offset, .. }) =>
      println!("{dirty:?}: trailing bits at {offset} suggest truncation"),
    Err(DecodeError::InvalidPadding) =>
      println!("{dirty:?}: padding is wrong or missing"),
  }
}

Намеренная строгость уходит корнями в сам стандарт. Раздел 12 RFC 4648 предупреждает, что символы вне алфавита могут использоваться как скрытый канал, чтобы проконтрабандировать побочную информацию или выуживать баги в небрежных парсерах, и рекомендует декодерам отвергать их. Спецификация MIME - знаменитое исключение: она прямо говорит декодерам игнорировать чужие символы, и именно такой вид ввода нижеследующий раздел о почте научит вас укрощать.

Три позиции по заполнителю

Каждая Base64-строка в дикой природе молча даёт обещание про заполнитель, и версия 0.23 позволяет выбрать, какое обещание исполнять, через перечисление DecodePaddingMode. Режимов три, и поведенческая разница стоит того, чтобы запомнить её. Вот сводная таблица для Zm8 - это слово fo без его =:

Режим "Zm8", без заполнителя "Zm8=", с заполнителем Когда применять
RequireCanonical, по умолчанию Err(Invalid padding) Ok([102, 111]) Вы сами производите и потребляете данные
Indifferent Ok([102, 111]) Ok([102, 111]) Принимаете данные из смешанных источников
RequireNone Ok([102, 111]) Err(Invalid padding) Работаете с протоколом, где заполнителя нет
use base64::engine::general_purpose::{GeneralPurpose, GeneralPurposeConfig, STANDARD_PAD_INDIFFERENT};
use base64::engine::DecodePaddingMode;
use base64::prelude::*;
let strict = BASE64_STANDARD;  // по умолчанию RequireCanonical
let flexible = STANDARD_PAD_INDIFFERENT;
let bare = GeneralPurpose::new(
  &base64::alphabet::STANDARD,
  GeneralPurposeConfig::new().with_decode_padding_mode(DecodePaddingMode::RequireNone),
);
println!("{:?}", strict.decode("Zm8"));    // Err(Invalid padding)
println!("{:?}", flexible.decode("Zm8"));  // Ok([102, 111])
println!("{:?}", flexible.decode("Zm8=")); // Ok([102, 111])
println!("{:?}", bare.decode("Zm8="));     // Err(Invalid padding)

Значения по умолчанию следуют за стандартом: раздел 3.2 RFC 4648 говорит, что реализации должны включать подходящие символы заполнения в конце закодированных данных, если окружающая спецификация не распоряжается об ином, - вот почему стандартный STANDARD-движок требует их. И выбор важен для безопасности, а не только для педантизма. Принимая и заполненную, и незаполненную записи одних и тех же данных, вы делаете Base64 податливым: одна и та же логическая нагрузка может записываться двумя способами, и любой код, предполагающий единую каноническую запись значения, может получить сюрприз. Статья 2022 года «Маллиабельность Base64 на практике» (Chatzigiannis и Chalkias, ePrint 2022/361) документирует реальные последствия, и документация самого крейта ссылается на неё. Практическое правило: выбирайте один режим на протокол и будьте строги на каждой границе, где вы не контролируете производителя.

Скрытые биты последнего символа

Вот повреждение, которое выживает после любой проверки символов. Каждый символ Base64 несёт 6 бит, и 3 входных байта (24 бита) становятся ровно 4 символами. Когда вход - всего 1 или 2 байта, у последнего символа оказываются неиспользуемые биты, и RFC это чётко проговаривает: соответствующие кодировщики обязаны обнулять эти свободные биты. Багговитый или злонамеренный кодировщик вместо этого может оставить там мусор, и результат всё равно проходит проверку алфавита, проверку длины и проверку заполнителя, тихо неся при этом повреждённый хвост. Строгий движок подстрахует - с уникально подробной ошибкой, которая даже показывает вам подозрительные биты:

use base64::engine::general_purpose::{GeneralPurpose, GeneralPurposeConfig};
use base64::prelude::*;
println!("{:?}", BASE64_STANDARD.decode("MT=="));
// Err(Invalid last symbol 0x54 ('T') at offset 1, decoded as 0b00010011.)
let lenient = GeneralPurpose::new(
  &base64::alphabet::STANDARD,
  GeneralPurposeConfig::new().with_decode_allow_trailing_bits(true),
);
println!("{}", String::from_utf8_lossy(&lenient.decode("MT==").unwrap()));
// 1

Это 0b00010011 в ошибке - декодированное значение провинившегося символа вместе с незаконными старшими битами, и версия 0.23.0 вынесла эту деталь на свет именно потому, что иначе её так трудно отлаживать. Если вы знаете, что ваши производители небрежны, with_decode_allow_trailing_bits(true) проглотит мусор вместо того, чтобы отвергнуть его. Браузеры сделали ставку на противоположное: алгоритм forgiving-base64 от WHATWG, тот, что стоит за atob() в JavaScript, намеренно снисходителен к хвостовым битам, тогда как поведение Rust по умолчанию - судебный эксперт. Знайте, за какой стороной стола вы сидите.

Base64url: алфавит, который путешествует

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

use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine;
let packed = URL_SAFE_NO_PAD.encode(b"\xfb\xef\xbe");
println!("{packed}");                            // ----
let back = URL_SAFE_NO_PAD.decode(packed).unwrap();
println!("{back:02x?}");                         // [fb, ef, be]
// стандартный движок отказывается от того же входа,
// потому что дефиса в его алфавите нет вовсе
println!("{:?}", base64::prelude::BASE64_STANDARD.decode("----"));
// Err(Invalid symbol 45, offset 0.)

Теперь о том, почему большинство разработчиков вообще сталкивается с base64url: JSON Web Tokens. JWT состоит из трёх base64url-частей, скреплённых точками, и подсмотреть, что внутри, - дело пяти строк:

use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine;
let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
let parts: Vec<&str> = jwt.split('.').collect();
let header = String::from_utf8(URL_SAFE_NO_PAD.decode(parts[0]).unwrap()).unwrap();
let payload = String::from_utf8(URL_SAFE_NO_PAD.decode(parts[1]).unwrap()).unwrap();
println!("{header}");
println!("{payload}");
// {"alg":"HS256","typ":"JWT"}
// {"sub":"1234567890","name":"John Doe","iat":1516239022}

Две честные оговорки. Декодировать JWT - значит подсмотреть, а не доверять: третья часть - это подпись, и она ничего не значит, пока не будет проверена по ключу, а это работа крейта jsonwebtoken (версия 11 на 2026 год). У версии 11 есть один острый край: ей нужна ровно одна из фич rust_crypto или aws_lc_rs, включённая в Cargo.toml, иначе она упанится в первый же момент, когда вы подпишете или проверите токен, а её конструктор Validation по умолчанию считает клейм exp обязательным, так что токены, чеканные для других библиотек, могут потребовать подправленной валидации. И именно на декодировании выбор алфавита кусается: подайте URL-безопасную строку в BASE64_STANDARD, или наоборот, и получите отказ, потому что -, _ и отсутствующий заполнитель для другого движка всё невалидно. Связывайте движок с протоколом, каждый раз.

Байты - не слова

Каждый декодер в этой статье намеренно останавливается на байтах, и в Rust это проще, чем в большинстве языков, потому что нет скрытого шага с кодировкой, который мог бы пойти не так. Base64 - это байтовый формат, точка. Вопрос «какой это был текст?» - ваш, и ответ по умолчанию для современного веба - UTF-8, который вы пропускаете через одну строку стандартной библиотеки:

use base64::prelude::*;
let packed = "Y2Fmw6k=";   // слово cafe с акцентом, упакованное
let bytes = BASE64_STANDARD.decode(packed).unwrap();
match std::str::from_utf8(&bytes) {
  Ok(text) => println!("{text}"),
  Err(_) => eprintln!("not utf-8: {bytes:02x?}"),
}

Счастливый путь мультбайтовых символов покрывает всё, что вы встретите в кабеле:

Исходный текст Base64 Декодируется обратно
café Y2Fmw6k= да
日本語 5pel5pys6Kqe да
naïve résumé bmHDr3ZlIHLDqXN1bcOp да
😀 8J+YgA== да
π ≈ 3.14159 z4Ag4omIIDMuMTQxNTk= да

А когда данные - вовсе не текст, тот же код просто получает другой финал. Вот магическое число файла PNG: четыре байта 89 50 4E 47 плюс пара CRLF, следующая за ними:

use base64::prelude::*;
let packed = "iVBORw0KGgo=";
let bytes = BASE64_STANDARD.decode(packed).unwrap();
println!("{bytes:02x?}");                          // [89, 50, 4e, 47, 0d, 0a, 1a, 0a]
assert!(std::str::from_utf8(&bytes).is_err());
std::fs::write("sprite.png", &bytes).unwrap();     // на выходе байты, а не текст

Правило-ориентир короткое: предполагайте UTF-8, проверяйте через std::str::from_utf8() и относите всё, что не прошло проверку, к байтовым нагрузкам для fs::write, блоба в базе данных или к тому стóку, откуда данные пришли. Единственный случай, когда тянутся за настоящей кодировкой, - устаревшие данные, которые так и не перенесли. Крейт encoding_rs (версия 0.8) знает старые кодировки по именам и конвертирует:

use base64::prelude::*;
use encoding_rs::Encoding;
let packed = "Y2Fm6Q==";   // cafe с акцентом, упакованное из байтов Latin-1
let bytes = BASE64_STANDARD.decode(packed).unwrap();
let (text, _, _) = Encoding::for_label(b"windows-1252").unwrap().decode(&bytes);
println!("{text}");   // cafe с акцентом, в виде UTF-8

В крейте base64 нет режима «декодировать как Latin-1», в котором можно было бы ошибиться, потому что он никогда не гадает за вас. Это та дисциплина, которую вы держите: крейт выдаёт вам байты, а вы решаете, что они значат.

Ввод, переживший почту

Base64, переживший почтовую систему, носит с собой переносы строк: MIME переносит на 76 символов в строку (PEM-блоки - на 64), а спецификация MIME прямо говорит надлежащим декодерам игнорировать символы вне алфавита, включая переносы строк. Наш движок - антипод MIME-совместимости: он отвергает самый первый перенос строки, а потоковый читатель докладывает об отказе как об ошибке ввода-вывода, обёртывающей тот же самый точный DecodeError:

use std::io::Read;
use base64::prelude::*;
use base64::read::DecoderReader;
let wrapped_in = "SGVs\nbG8s";
let mut reader = DecoderReader::new(wrapped_in.as_bytes(), &BASE64_STANDARD);
let mut out = Vec::new();
println!("{:?}", reader.read_to_end(&mut out).map(|_| out));
// Err(Custom { kind: InvalidData, error: Invalid symbol 10, offset 4. })

Обе позиции ведут к одному и тому же RFC, который оставляет выбор окружающей спецификации, и крейт base64 выбрал быть строгим. Это не первый раз, когда он передумал: версия 0.5.0 вышла со встроенным MIME-переносом строк и обработкой пробельных символов, а версия 0.10.0 убрала и то и другое, рассудив, что перенос строк - слишком категоричная позиция для общей библиотеки, да ещё и усложняет историю с no_std. Так что рецепт для реального перенесённого ввода - тот же, что советует и документация самого крейта: сначала отбросить символы вне алфавита, затем декодировать. Для строки в памяти это один фильтр:

use base64::prelude::*;
fn strip_non_b64(input: &[u8]) -> Vec<u8> {
  input.iter().copied().filter(|b| !b" \n\r\t\x0b\x0c".contains(b)).collect()
}
fn main() {
  let wrapped = "SGVs\nbG8s\r\nIHN0\nYW5kYXJk";
  let clean = strip_non_b64(wrapped.as_bytes());
  let bytes = BASE64_STANDARD.decode(clean).unwrap();
  println!("{}", String::from_utf8_lossy(&bytes));   // Hello, standard
}

Если хочется декодер, который смотрит прямо через переносы, константа BASE64_MIME_PERMISSIVE из крейта data-encoding - это ровно то: она декодирует "SGVsbG8s\r\nd29ybGQh\r\n" в Hello,world!, не требуя от вас ни единой строки. Для потоков, где весь ввод не удержать, FAQ крейта указывает на крейт iter_read для фильтрации байтового потока, либо на написание крошечной обёртки Read, которая выбрасывает нежелательные байты по мере их прихода. Одно предупреждение, прежде чем строить «снисходительный декодер» вручную: тихое игнорирование символов вне алфавита - это ровно то поведение, которое раздел 12 RFC 4648 помечает как скрытый канал, так что отбрасывайте только те пробельные символы, которых ждёте, и отвергайте всё остальное.

Когда всё сообщение перед вами, base64 делает за вас почтовый парсер. Крейт mail-parser (версия 0.11) декодирует каждую часть Content-Transfer-Encoding: base64 прямо по ходу разбора, так что вложения возвращаются сырыми байтами, уже распелёнутыми и декодированными:

use mail_parser::MessageParser;
let email = br#"From: art@vandelay.com
To: jane@example.com
Subject: gift
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="festivus"

--festivus
Content-Type: text/plain; charset="us-ascii"
Content-Transfer-Encoding: base64

SGVsbG8gZnJvbSBlbWFpbA==
--festivus
Content-Type: image/gif
Content-Transfer-Encoding: Base64
Content-Disposition: attachment; filename="tiny.gif"

R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7
--festivus--
"#;
let message = MessageParser::default().parse(email).unwrap();
for part in message.attachments() {
  let name = part
    .headers()
    .iter()
    .find(|h| h.name().eq_ignore_ascii_case("content-disposition"))
    .and_then(|h| h.value().clone().unwrap_content_type()
      .attribute("filename").map(|n| n.to_string()));
  let bytes = part.contents().to_vec();
  println!("{name:?}: {} bytes", bytes.len());
}
// Some("tiny.gif"): 42 bytes

То GIF-вложение декодируется в 42 байта, начиная с четырёх байтов 47 49 46 38 - букв ASCII GIF8. Вы и строки кода Base64 не написали, и в этом весь смысл использования парсера: детали кодирования - проблема библиотеки.

Крупная нагрузка

Строки - легко; файлы - вот где Base64 окупает себя, и крейт отвечает той же потоковой философией, что и остальной io в Rust. read::DecoderReader оборачивает любой читатель и прозрачно выдаёт декодированные байты по мере чтения, так что многогигабайтный закодированный файл никогда не обязан целиком поместиться в памяти. Для примера ниже сохраните строку dGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw== в обычный текстовый файл под именем fox.b64:

use std::io::Read;
use base64::prelude::*;
use base64::read::DecoderReader;
fn main() {
  let packed = std::fs::read("fox.b64").unwrap();
  let mut decoder = DecoderReader::new(&packed[..], &BASE64_STANDARD);
  let mut plain = Vec::new();
  decoder.read_to_end(&mut plain).unwrap();
  println!("{}", String::from_utf8_lossy(&plain));
  // the quick brown fox jumps over the lazy dog
}

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

use std::io::Cursor;
use std::io::Read;
use base64::prelude::*;
use base64::read::DecoderReader;
fn is_valid_base64(input: &str) -> bool {
  let mut cursor = Cursor::new(input.as_bytes());
  let mut decoder = DecoderReader::new(&mut cursor, &BASE64_STANDARD);
  let mut buf = [0u8; 128];
  loop {
    match decoder.read(&mut buf) {
      Ok(0) => return true,   // дочитали до конца без ошибки
      Ok(_) => continue,
      Err(_) => return false, // что-то не было base64
    }
  }
}
fn main() {
  println!("{}", is_valid_base64("SGVsbG8sIHdvcmxkIQ=="));  // true
  println!("{}", is_valid_base64("dt=="));                  // false
}

Для нагрузок, которые большие, но всё ещё помещаются в управляемый вами буфер, срезовое API - вариант без сюрпризов: base64::decoded_len_estimate(len) даёт консервативный максимальный размер декодирования для len символов, а decode_slice() пишет прямо в ваш заранее выделенный буфер, возвращая ровно столько байтов, сколько записало:

use base64::prelude::*;
let packed = "SGVsbG8sIHdvcmxkIQ==";
let cap = base64::decoded_len_estimate(packed.len());  // 15, консервативный максимум
let mut buf = vec![0u8; cap];
let written = BASE64_STANDARD.decode_slice(packed, &mut buf).unwrap();
buf.truncate(written);
println!("{}", std::str::from_utf8(&buf).unwrap());    // Hello, world!

Если ваш буфер слишком мал, вместо паники вы получите чистый DecodeSliceError::OutputSliceTooSmall, а есть вариант decode_slice_unchecked(), который панит по замыслу, - для тех мест, где «слишком мал» - это ошибка программиста, и на ней лучше разбиться, чем таскать её по коду. С версии 0.22.0 срезовая проверка консервативна в вашу пользу: она срабатывает только когда вывод действительно не помещается, так что буфер точно в размер работает.

Куда уходят декодированные байты

Base64 встречается в Rust-проектах гораздо чаще, чем подсказывает фраза «какая-то случайная строка»:

  • Ответы API и вебхуки, встраивающие бинарные данные или вложенный JSON в виде Base64-текста в своих нагрузках, - классический паттерн «загрузка файла как JSON».
  • Осмотр JWT: декодируете заголовок и нагрузку, чтобы подсмотреть клеймы, а подпись передаёте jsonwebtoken - на ту часть, которая что-то значит по-настоящему.
  • Данные почтового облика: MIME-вложения и всё, что прошло через почтовую систему, - вот ради чего вообще существует раздел о пробельных символах.
  • PEM-блоки в сертификатах и ключах, те самые секции -----BEGIN CERTIFICATE-----, которые жуют все TLS-стеки; крейт pem парсит их за вас и, что уместно, внутри построен на крейте base64.
  • Data URI, прячущиеся в HTML и CSS, которые вы скреипите или рендерите, - те, в роде data:image/png;base64,....
  • Заголовки HTTP Basic-авторизации, где Basic TWFuOnBhc3M= - это просто Man:pass в маскировке.
  • Базы данных и файлы конфигурации, где кому-то понадобился бинарный внутри текстовой колонки или переменной окружения.
  • Меязыковая передача: служба на Python упаковывает блоб, Rust распаковывает, и обе стороны по определению говорят на одном алфавите.

Для JSON-случая есть шорткат, о котором стоит знать: крейт serde_with (версия 3) умеет размечать поле структуры так, чтобы serde сам обрабатывал Base64 в обоих направлениях, кодируя поля Vec<u8> в текст при выводе и декодируя обратно при вводе, а URL-безопасный вариант - за одним параметром:

use serde::{Deserialize, Serialize};
use serde_with::serde_as;
#[serde_as]
#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct Config {
  #[serde_as(as = "serde_with::base64::Base64")]
  blob: Vec<u8>,
}
let cfg = Config { blob: b"stored in a database".to_vec() };
let json = serde_json::to_string(&cfg).unwrap();
// {"blob":"c3RvcmVkIGluIGEgZGF0YWJhc2U="}
let back: Config = serde_json::from_str(&json).unwrap();
assert_eq!(back, cfg);

Data URI - это двухшаговая операция со строкой, за которой следует обычное декодирование: найдите запятую, оставьте всё, что после неё, и проверьте, что метаданные заканчиваются словом base64:

use base64::prelude::*;
let uri = "data:image/png;base64,iVBORw0KGgo=";
let comma = uri.find(',').unwrap();
let meta = &uri[..comma];
let payload = &uri[comma + 1..];
let is_b64 = meta.rsplit(';').next().unwrap() == "base64";
let bytes = BASE64_STANDARD.decode(payload).unwrap();
println!("{is_b64}: {} bytes from {meta}", bytes.len());
// true: 8 bytes from data:image/png;base64

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

Десятилетие аккуратных шагов

Собственная история крейта читается как медленное подкручивание гаек. Он впервые появился на crates.io в декабре 2015 года, и версия 0.5.0 с гордостью добавила поддержку MIME с настраиваемыми символами переноса и переносом строк. Затем версия 0.10.0 в 2018 году убрала и перенос, и обработку пробельных символов - библиотека рассудила, что общий крейт должен декодировать и оставить поэзию прикладному слою; тот же релиз добавил потоковый кодировщик и обнаружение невалидных хвостовых символов. Версия 0.20.0 в 2022 году ввела абстракцию движка и перевернула значение по умолчанию для заполнителя, так что стандартный движок теперь требует канонического заполнителя; 0.21.0 объявила устаревшими старые свободные функции вроде base64::decode() в пользу методов движка, с пометкой компилятора «Use Engine::decode» (они всё ещё работают, поэтому много легаси-кода компилируется с довольным лицом). В 2024 году версия 0.22.0 отточила семантику ошибок, уточнила, что значит InvalidLength, и ускорила декодирование на 5-10 процентов. А в июле 2026 года пришла версия 0.23.0 с SIMD-движками, пользовательскими символами заполнения, более ясным сообщением InvalidLastSymbol и поднятием MSRV до 1.71, а патч 0.23.1 от 4 августа починил тестовую базу для не-SIMD-архитектур. Десятилетие маленьких, аккуратных шагов, и крейт, начавшийся с «Это же base64. Чего ещё можно пожелать?», теперь отгружает векторизованные ядра.

Формату больше, чем вебу, - вот почему строгость ощущается как личная. В 1987 году протоколу Privacy-Enhanced Mail (RFC 989) нужно было нести бинарные данные по 7-битным почтовым каналам, и он стандартизировал это кодирование со строками в 64 символа; каждый блок -----BEGIN CERTIFICATE-----, которому когда-либо доверял ваш TLS-стек, - прямой потомок того решения. В 1996 году спецификация MIME (RFC 2045) приняла схему, назвала её «base64» в честь 64-символьного алфавита и установила длину строки в 76 символов, которой до сих пор переносятся ваши почтовые вложения. В 2006 году RFC 4648 стал стандартом, на который ссылаются все: таблицы алфавитов, вариант base64url в разделе 5 и правила строгости, которые этот крейт реализует с такой видимой радостью.

Весёлые факты

Потому что полное руководство должно закончиться улыбкой:

  • Слово «base64» кодируется в YmFzZTY0. Формат, описывающий сам себя, - технический эквивалент зеркала, которое говорит на морзе.
  • Пустая строка декодируется в ноль байтов, но "AA==" декодируется в один байт: NUL. В Base64 «ничто» и «ноль» - разные существа.
  • Каждый Base64-кодированный PNG, который вы когда-либо видели, начинается с iVBORw0K. Это магическое число PNG в маскировке, и один из самых узнаваемых префиксов в интернете.
  • ID видео на YouTube - это base64url без заполнителя: 8-байтовое значение даёт двенадцать символов Base64, и если отбросить хвостовой заполнитель, остаётся знакомый одиннадцатисимвольный ID, который можно вставить в любое место URL. Одно из самых зримых применений режима без заполнителя на всём интернете.
  • Bash считает в системе с основанием 64 уже много лет: арифметический литерал $((64#...)) принимает цифры в порядке 0-9, a-z, A-Z, а в конце @ и _ для значений 62 и 63, так что ваш шелл носит 64-символьный алфавит при всех.
  • Старые хеши паролей crypt(3) использовали вариант Base64, чей алфавит начинается с ./, и у него есть чудесное свойство: сортировка закодированных строк даёт тот же порядок, что и сортировка исходных байтов. Файлы генеалогии использовали тот же алфавит для встроенных мультимедиа (GEDCOM 5.5; ревизия 5.5.1 отбросила это), а крейт base64 отгружает его как alphabet::CRYPT.
  • MIME-математика, как её до сих пор считает старое правило-ориентир: перенесённая на строки почтовая нагрузка обходится примерно в 1.37 исходного размера, плюс порядка нескольких сотен байтов заголовков. Почтовая инфраструктура 1990-х правда взимала ту пошлину с каждого вложения.
  • Весь крейт - #![forbid(unsafe_code)], кроме включённой по умолчанию фичи simd-unsafe, из которой надо выходить, а не входить. Одно слово - «unsafe» - и это имя флага фичи.
  • Base64 податлив так, что это пугает исследователей безопасности: одни и те же байты можно записать с заполнителем или без, с мусором в хвостовых битах, и снисходительные декодеры этого не заметят. Статья 2022 года продемонстрировала реальные последствия, и поэтому строгое значение по умолчанию в этом крейте ощущается как телохранитель.
  • Base64 - не шифрование. Если бы оно им было, вы не смогли бы прочесть вывод любого примера в этой статье. Это место у окна, а не сейф.

Итоги

Выбирайте движок по компании, которую вы собираете: BASE64_STANDARD для всего, что вы производите и контролируете, STANDARD_PAD_INDIFFERENT для границы смешанных источников, URL_SAFE_NO_PAD для токенов и URL, а собранный вручную GeneralPurpose, когда протокол требует собственных правил. Позвольте четырём вариантам DecodeError ворчать со всей точностью, пропускайте байты через std::str::from_utf8(), прежде чем называть их текстом, гоняйте большие вещи потоком через DecoderReader, отбрасывайте только те пробельные символы, которых ждёте, и тянитесь за base64ct, когда время - угроза. Декодируйте всё, доверяйте только то, что верифицируется. А если однажды понадобится пойти в обратную сторону - не распаковывать, а упаковать байты в строку в дорогу, - сестринская статья разбирает кодирование в Rust, от арифметики размера до потокового финала.

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

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