Кодирование Base64 в Rust: полное руководство
Вы вот-вот отправите несколько байтов через текстовую дверь, и цена входа - строка из букв, цифр, плюсов и слешей, примерно на треть длиннее того, с чем вы начинали. Добро пожаловать в Base64, платный пункт интернета. Домашняя страница этого сайта объясняет формат во всех деталях, поэтому здесь достаточно напомнить лишь форму: Base64 записывает три входных байта четырьмя символами, взятыми из 64-символьного алфавита, и хвост из одного-двух символов = говорит читателю, где закончились настоящие данные. Этот обмен четыре-за-три - вся экономика формата, и это руководство о том, как сделать его хорошо в Rust.
Первое, что нужно знать: стандартная библиотека Rust не сделает это за вас. В std не притаился ни один base64_encode(), и ни одна use std::... не изменит ваше мнение. Экосистема остановилась на единственном крейте с простым именем base64, и он стал несущей конструкцией: версия 0.23.1 вышла 4 августа 2026 года, с момента первого релиза в декабре 2015 года крейт опубликовал 45 версий, а его счётчик загрузок стоит у отметки в 1,5 миллиарда. Каждый пример кодирования ниже использует этот один крейт, плюс двух маленьких спутников для переноса строк и PEM-брони.
Инструментальная цепочка и крейт
Сначала инструментальная цепочка, по команде на каждый мир:
# 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 (включена по умолчанию; SIMD-движки, которые появятся через пару разделов). Минимальная поддерживаемая версия Rust - 1.71.0, так что любой свежий Rust его потянет. Вокруг него сидят спутники для тех работ, которые центр намеренно не делает:
- line-wrap (версия 0.2) вставляет переносы строк на 76 или 64 символа, которые требуют MIME и PEM; сам крейт
base64намеренно отказывается переносить строки, как вы скоро увидите. - pem (версия 4) собирает и разбирает блоки
-----BEGIN ...-----для сертификатов и ключей; внутри зависит отbase64и добавляет броню и перенос. - base64ct (версия 1.8) - декодер за постоянное время от проекта RustCrypto, на случай, если читающая сторона кругового рейса - чувствительная половина.
- base64-turbo (версия 0.3) - новый кодек высокой пропускной способности, дающий пик выше 100 ГиБ/с на современном железе.
Одно кодирование, четыре символа
Самый маленький из возможных обрядов выглядит так, и он уже доказывает весь круговой рейс:
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!
}
Стоит заметить две вещи. Модуль prelude тихо выдаёт вам две вещи разом: движок BASE64_STANDARD и трейт Engine, методы которого вы вызываете, - поэтому голого use base64::prelude::*; для этого примера достаточно. И encode() принимает всё, что можно прочитать как байты, благодаря ограничению AsRef<[u8]>: &str, литерал &[u8], Vec<u8> - называйте что угодно. Декодирующая половина примера есть просто для того, чтобы присматривать за кодировщиком, потому что обратное направление имеет собственное полное руководство на сестринском сайте. Если хочется дымового теста самого крейта, его документация кодирует asdf и получает обратно YXNkZg==; тот же алфавит, та же математика.
Точная цена каждого байта
Каждый существующий Base64-кодировщик взимает один и тот же налог, и как только вы видите математику, вы можете заложить её в бюджет. Каждый выходной символ несёт 6 бит, каждый входной байт - 8, и самая маленькая куча, которая и то и другое, - 24 бита: ровно 3 байта на входе, ровно 4 символа на выходе. Это соотношение и есть всё шоу, так что файл в 3 килобайта становится 4 килобайтами, а загрузка в 10 мегабайт - 13.3. Заполнитель - это округлительная ошибка, ставшая видимой: когда вход не кратен 3 байтам, у последней группы остаётся свободная ёмкость, и кодировщик заполняет её символами =, чтобы длина вывода оставалась кратной 4. Вот таблица истинности из RFC 4648, которую стандартный движок воспроизводит в точности:
| Ввод | Длина mod 3 | Кодированный | Длина вывода |
|---|---|---|---|
"" (пустой) |
0 | "" (пустой) |
0 |
f |
1 | Zg== |
4 |
fo |
2 | Zm8= |
4 |
foo |
0 | Zm9v |
4 |
foobar |
0 | Zm9vYmFy |
8 |
use base64::prelude::*;
let words: [&[u8]; 4] = [b"", b"f", b"fo", b"foo"];
for input in words {
println!("{:?} -> {:?}", String::from_utf8_lossy(input), BASE64_STANDARD.encode(input));
}
// "" -> ""
// "f" -> "Zg=="
// "fo" -> "Zm8="
// "foo" -> "Zm9v"
Прочтите первую строку дважды, потому что именно её все неправильно считают в голове: пустой вход кодируется в пустую строку, а не в AA==. Строка AA== - это кодирование ровно одного байта, NUL, что является по-настоящему другой нагрузкой. А когда нужно определить размер буфера до кодирования, крейт отдаёт вам математику в виде const fn, так что размеры массивов можно задавать уже на этапе компиляции:
let padded = base64::encoded_len(15, true).unwrap();
let slim = base64::encoded_len(15, false).unwrap();
println!("{padded} / {slim}"); // 20 / 20
println!("{:?}", base64::encoded_len(13, true)); // Some(20)
println!("{:?}", base64::encoded_len(13, false)); // Some(18)
println!("{:?}", base64::encoded_len(14, false)); // Some(19)
println!("{:?}", base64::encoded_len(100, false)); // Some(134)
Посмотрите на строки для 13 и 14 байтов, потому что именно они сбивают счёту на глаз: 13 байтам нужно 18 символов без заполнителя, но 20 с заполнителем, тогда как 14 байтам - 19 и 20. Функция возвращает Option, который становится None только когда арифметика длины переполняется, так что unwrap() безопасен для любого входа, который действительно может существовать в памяти. Для мира почтового облика у налога есть надбавка: MIME переносит строки на 76 символов, и старое правило-ориентир гласит, что перенесённый Base64 обходится примерно в 1.37 исходного размера, плюс накладные расходы заголовков. У FAQ самого крейта по поводу самого заполнителя есть менее вежливое мнение: байты = «не влияют на декодирование, кроме как дают возможность сказать «этот заполнитель неверен»», и «эксабайты хранилища и передачи, без сомнения, были потрачены впустую на бессмысленные байты =».
Заполнитель: решение о читателе
В base64 0.23 голые функции кодирования объявлены устаревшими - правильный сейчас способ - вызвать метод Engine, а движок - это политика: какой алфавит писать и какой заполнитель добавлять. Пресеты живут в base64::engine::general_purpose, а четыре популярных реэкспортированы в prelude:
| Движок | Алфавит | Добавляет заполнитель | Лучше всего для |
|---|---|---|---|
STANDARD / BASE64_STANDARD |
+ / |
да | для всего, по умолчанию |
STANDARD_NO_PAD / BASE64_STANDARD_NO_PAD |
+ / |
нет | компактные данные, которые вы сами же и читаете |
URL_SAFE / BASE64_URL_SAFE |
- _ |
да | URL-содержимое, которому всё ещё хочется заполнитель |
URL_SAFE_NO_PAD / BASE64_URL_SAFE_NO_PAD |
- _ |
нет | JWT, URL, ID объектов |
Кодирование без заполнителя - не хак, который крейт терпит; это позиция первого класса, с заранее настроенными константами конфигурации NO_PAD и PAD рядом с роднёй *_INDIFFERENT, добавленной в 0.23.0. Если пресеты не подходят, вы собираете свой движок из Alphabet и GeneralPurposeConfig с одним регулятором, а поскольку движки дёшевы в создании, результат хранят в const, вместо того чтобы пересобирать его на каждый запрос:
use base64::engine::general_purpose::{GeneralPurpose, GeneralPurposeConfig};
use base64::prelude::*;
const SLIM: GeneralPurpose = GeneralPurpose::new(
&base64::alphabet::STANDARD,
GeneralPurposeConfig::new().with_encode_padding(false),
);
fn main() {
println!("{}", SLIM.encode("fo")); // Zm8
println!("{}", BASE64_STANDARD.encode("fo")); // Zm8=
}
Теперь решение становится решением о чужих декодерах, что никогда не бывает чисто эстетическим. Правила строгости на стороне декодирования идут от DecodePaddingMode, и таблица ниже отвечает на вопрос «сможет ли другая сторона прочитать то, что я записал?»:
| Вы кодируете с помощью | Строгий STANDARD-декодер |
STANDARD_NO_PAD-декодер |
INDIFFERENT-декодер |
|---|---|---|---|
STANDARD (с заполнителем) |
читает | отказывается от = |
читает |
STANDARD_NO_PAD |
отказывается: заполнитель отсутствует | читает | читает |
URL_SAFE_NO_PAD |
отказывается: неверный алфавит | отказывается: неверный алфавит | читает только с URL-алфавитом |
Практические правила вытекают из этой таблицы. Если вы контролируете оба конца, выберите один движок и используйте его везде, и предпочитайте отсутствие заполнителя, чтобы экономить байты. Если вы потребляете данные из внешнего мира, ваш декодер получает голос в том, каким движком вам следует выпускать данные: стандартный STANDARD-декодер требует ваш заполнитель, тогда как STANDARD_PAD_INDIFFERENT-декодер принимает оба варианта. И в выборе есть и привкус безопасности. Разрешая и заполненную, и незаполненную записи одной и той же нагрузки, вы делаете Base64 податливым; статья 2022 года «Маллиабельность Base64 на практике» (Chatzigiannis и Chalkias, ePrint 2022/361), на которую ссылается документация самого крейта, показывает, почему. У протокола, где одни и те же данные можно записать двумя разными способами, есть привычка удивлять код, который считает закодированную строку идентификатором, так что когда ваш формат определяет единую каноническую запись, принудительно соблюдайте её на границе.
Base64url для токенов и ссылок
Последние две буквы алфавита стандартного Base64 - + и /, и в URL это два самых дорогих символа языка: плюс становится %2B, слеш - %2F, а заполнитель - %3D. Раздел 5 RFC 4648 чинит это безопасным для URL и имён файлов алфавитом, который меняет двух бунтарей на - и _ и обычно пропускает и заполнитель. Движки делают это различение невозможно пропустить:
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]
Три байта самого зловредного из возможных входов превращаются в четырёхсимвольную строку, которую можно вставить в URL, имя файла, куки или ключ базы данных без единого процентного экранирования. В этом алфавите живут JSON Web Tokens: JWT - это три base64url-части, скреплённые точками, и чеканка одного через крейт jsonwebtoken (версия 11 на 2026 год) выглядит так:
use serde::Serialize;
use jsonwebtoken::{EncodingKey, Header, encode};
#[derive(Debug, Serialize)]
struct Claims {
sub: String,
company: String,
exp: u64,
}
let key = b"secret";
let my_claims = Claims {
sub: "b@b.com".to_owned(),
company: "ACME".to_owned(),
exp: 19_000_000_000, // далеко в будущем
};
let token = encode(&Header::default(), &my_claims, &EncodingKey::from_secret(key)).unwrap();
println!("{token}");
// eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJiQGIuY29t...
У версии 11 есть одно требование к настройке, которое кусает новичков: крейту нужна ровно одна из фич rust_crypto или aws_lc_rs, включённая в Cargo.toml, и если ни одна не включена, он панит в первый же момент, когда вы подпишете или проверите токен. Обратите внимание на клейм exp в структуре: валидация крейта по умолчанию считает его обязательным, так что настоящие токены его всё равно несут, а base64url-алфавит в токене - целиком внутреннее дело библиотеки. Если вы только осматриваете токены, а не чеканите их, сестринская статья показывает пятистрочное подглядывание. И напоминание о золотом правиле, которое действует на токены с особой силой: все три части JWT читаются без ключа. Base64 - место у окна, а не замок.
Текст на входе, байты на выходе
Кодировщики не читают мысли, поэтому в Rust «закодируйте эту строку» всегда означает «закодируйте UTF-8-байты этой строки», потому что именно это выдаёт str::as_bytes(). Хорошая новость: современный веб почти целиком UTF-8, так что честный путь короткий и счастливый:
use base64::prelude::*;
let text = "café";
let packed = BASE64_STANDARD.encode(text.as_bytes());
println!("{packed}"); // Y2Fmw6k=
Все мультбайтовые случаи держатся:
| Исходный текст | Base64 | Круговой рейс |
|---|---|---|
café |
Y2Fmw6k= |
чисто |
日本語 |
5pel5pys6Kqe |
чисто |
😀 |
8J+YgA== |
чисто |
π ≈ 3.14159 |
z4Ag4omIIDMuMTQxNTk= |
чисто |
Единственное настоящее решение - с каких байтов вы начинаете. Если данные приходят байтами, а не текстом, файл, прочитанный с диска, или буфер из сетевого вызова, пропустите строку целиком и кодируйте Vec<u8> напрямую; это и единственно правильный ответ для не-UTF-8 нагрузок вроде PNG или protobuf. Положите любую картинку рядом с кодом и направьте чтение на неё:
use base64::prelude::*;
let file_bytes = std::fs::read("sprite.png").unwrap();
let size = file_bytes.len();
let packed = BASE64_STANDARD.encode(file_bytes);
println!("{size} bytes -> {} base64 chars", packed.len());
// каждый закодированный PNG начинается с iVBORw0K
assert!(packed.starts_with("iVBORw0K"));
Последнее утверждение - бесплатная проверка адекватности и один из самых узнаваемых префиксов в интернете. А если вы закодируете один и тот же логический текст через две разные кодировки, или закодируете байты, которые неверно прочли в другой кодировке, круговой рейс вернётся кракозябрами с каменным лицом. Кодировщик никогда не врёт; он просто кодирует те байты, которые вы ему даёте, - и это одновременно его величайшая сила и единственная ловушка.
Когда формату нужны строки
Крейт base64 намеренно не вставляет переносы строк, и это не первый раз, когда он принимает такое решение. Версия 0.5.0 вышла со встроенным MIME-переносом строк с настраиваемыми символами перевода, а версия 0.10.0 убрала его - библиотека рассудила, что перенос строк - слишком категоричная позиция для общего крейта, да ещё и усложняет историю с no_std. Если формату нужны строки, крейт line-wrap существует ровно для этого. Его единственная функция, line_wrap(), принимает ваш заранее выделенный буфер, длину входа, ограничение по колонке и символ перевода строки, и возвращает число байтов перевода строки, которое она вставила:
use base64::prelude::*;
let data = BASE64_STANDARD.encode(vec![b'a'; 300]); // 400 символов
let mut buf = vec![0u8; data.len() + 16];
buf[..data.len()].copy_from_slice(data.as_bytes());
let endings = line_wrap::line_wrap(&mut buf, data.len(), 76, &line_wrap::crlf());
buf.truncate(data.len() + endings);
let wrapped = String::from_utf8(buf).unwrap();
println!("{} chars in, {} bytes out, {} line endings", data.len(), wrapped.len(), endings);
// 400 chars in, 410 bytes out, 10 line endings (пять пар CRLF)
Определите размер буфера с запасом под переводы, вызовите функцию и обрежьте до указанного итога; пять пар CRLF - цена правила MIME о 76 колонках. Для PEM поменяйте ограничение и символ перевода, 64 колонки и line_wrap::lf(), и у вас готов текст тела брони. Затем крейт pem добавляет баннеры одним вызовом:
let pem_block = pem::encode(&pem::Pem::new("CERTIFICATE", b"0123456789abcdef"));
println!("{pem_block}");
// -----BEGIN CERTIFICATE-----
// MDEyMzQ1Njc4OWFiY2RlZg==
// -----END CERTIFICATE-----
let back = pem::parse(pem_block).unwrap();
println!("{}: {} bytes", back.tag(), back.contents().len());
// CERTIFICATE: 16 bytes
// а вот Unix-переносы строк, если потребитель придирчивый
let lf_block = pem::encode_config(
&pem::Pem::new("KEY", b"0123456789abcdef"),
pem::EncodeConfig::new().set_line_ending(pem::LineEnding::LF),
);
По умолчанию pem::encode использует CRLF, историческую конвенцию PEM; конструктор set_line_ending переключает на LF для тех инструментов, которые его ждут. Обратите внимание, чего крейт pem не делает: он никогда не вызывает base64-функцию, которую можно увидеть, потому что кодирование - его внутреннее дело. Когда формату нужны строки, архитектура - один крейт на работу.
Поток в постоянном объёме
Для данных слишком больших, чтобы держать их в одной переменной, крейт отвечает той же потоковой философией, что и остальной io в Rust: write::EncoderWriter оборачивает любой writer и кодирует в base64 всё, что вы в него пишете, в постоянном объёме. Полный обряд для буфера выглядит так, и звезда раздела - вызов finish():
use std::io::Write;
use base64::prelude::*;
use base64::write::EncoderWriter;
fn main() {
let mut encoder = EncoderWriter::new(Vec::new(), &BASE64_STANDARD);
encoder.write_all(b"the quick brown fox jumps over the lazy dog").unwrap();
let packed = encoder.finish().unwrap();
println!("{}", String::from_utf8(packed).unwrap());
// dGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw==
}
Почему finish() - звезда? Потому что это единственный вызов, который сбрасывает последнюю неполную группу и добавляет заполнитель, а у кодировщика есть родственный метод, который этого не делает. Документация самого крейта говорит прямо: finish() «кодирует оставшиеся входные байты и добавляет заполнитель, если уместно. Вызывается автоматически при освобождении (см. реализацию Drop), но любая ошибка, возникающая при обращении к базовому writer'у, будет подавлена. Если вы хотите обрабатывать такие ошибки, вызовите finish() сами.» Реализация Drop ведёт себя как BufWriter: она сбрасывает, но игнорирует ошибки во время drop. Так что последняя неполная группа не теряется, но «похоже, сработало» - не стратегия для отгрузки, потому что ошибка записи, о которой он бы вам рассказал, уже исчезла.
Тот же поток работает на уровень дальше через io::copy, когда вы хотите весь конвейер одним вызовом, а есть и бонусная обёртка для моментов «мне нужно это просто внутри строки форматирования»:
use std::io;
use base64::prelude::*;
use base64::write::EncoderWriter;
let file = b"the quick brown fox jumps over the lazy dog".to_vec();
let mut cursor = io::Cursor::new(file);
let mut encoder = EncoderWriter::new(Vec::new(), &BASE64_STANDARD);
io::copy(&mut cursor, &mut encoder).unwrap();
let packed = encoder.finish().unwrap();
println!("{}", String::from_utf8(packed).unwrap());
use base64::display::Base64Display;
use base64::prelude::*;
let value = Base64Display::new(b"\0\x01\x02\x03", &BASE64_STANDARD);
println!("base64: {value}"); // base64: AAECAw==
Та обёртка Base64Display - маленький бриллиант: она форматирует байты как Base64 внутри любой строки форматирования без единой аллокации кучи, отчего строки логов и отладочный вывод внезапно становятся приятными.
Аллокация, и её отсутствие
Удобный метод аллоцирует, и большую часть жизни это правильный обмен. Но трейт Engine открывает три вида кодирования, и таблица ниже - это вся матрица решений:
| Метод | Вывод | Аллоцирует |
|---|---|---|
encode() |
новый String |
всегда |
encode_string() |
дописывает в ваш String |
только если ему нужно расти |
encode_slice() |
пишет в ваш &[u8] |
никогда |
use base64::prelude::*;
let input = b"Hello, world!";
let mut buf = vec![0u8; base64::encoded_len(input.len(), true).unwrap()];
let written = BASE64_STANDARD.encode_slice(input, &mut buf).unwrap();
buf.truncate(written);
println!("{}", std::str::from_utf8(&buf).unwrap()); // SGVsbG8sIHdvcmxkIQ==
// либо вообще держать буфер на стеке
let mut stack = [0u8; 24];
let n = BASE64_STANDARD.encode_slice(b"abc 123", &mut stack).unwrap();
println!("{}", String::from_utf8(stack[..n].to_vec()).unwrap()); // YWJjIDEyMw==
// и если вы ошиблись с размером, получите ошибку, а не переполнение буфера
let mut tiny = [0u8; 5];
println!("{:?}", BASE64_STANDARD.encode_slice(input, &mut tiny));
// Err(OutputSliceTooSmall)
Определите размер буфера через encoded_len(), пишите через encode_slice(), и если вы ошиблись с размером, получите чистый EncodeSliceError::OutputSliceTooSmall вместо неопределённого поведения, что в системном языке - разница между скучным днём и долгим. Для встроенных задач те же функции существуют за фичей alloc, так что API можно сохранить, а кучу - выбросить.
Скорость: SIMD-движки
Версия 0.23.0, та, что вышла в июле 2026 года, принесла главную фичу: ускоренные SIMD-движки для стандартного и URL-безопасного алфавитов. Их три, и они делятся по тому, насколько сильно доверяют вашему железу:
| Движок | Определяет в рантайме | Работает в no_std |
|---|---|---|
Simd |
да, выбирает AVX2 или NEON, откатывается на скалярный движок | нет, для определения нужны std |
Avx2 |
нет, предполагает, что у CPU есть AVX2 | да, на x86_64-целях |
Neon |
нет, предполагает, что у CPU есть NEON | да, на aarch64-целях |
use base64::engine::general_purpose::GeneralPurposeConfig;
use base64::engine::{Avx2, Simd};
use base64::Engine;
let turbo = Simd::standard(GeneralPurposeConfig::new());
println!("{}", turbo.encode("simd works!"));
// c2ltZCB3b3JrcyE=
if let Some(fixed) = Avx2::standard(GeneralPurposeConfig::new()) {
println!("{}", fixed.encode("hello avx2")); // aGVsbG8gYXZ4Mg==
}
Конструктор Simd делает определение CPU один раз и возвращает лучшее ядро, которое находит, или скалярный движок, если ни одно не подошло, так что собирайте его один раз в const или при запуске и переиспользуйте; на подходящем железе он в несколько раз быстрее скалярного пути и для кодирования, и для декодирования. Одна честная сноска: SIMD-путь - единственное место в крейте, где трогают unsafe, - отсюда и имя фичи simd-unsafe. Выключите фичу, и весь крейт снова #![forbid(unsafe_code)], а скалярный движок продолжает честно работать. Если чистая пропускная способность - вот в чём вся суть, крейт base64-turbo заходит дальше, давая пик выше 100 ГиБ/с с ядрами AVX512, AVX2 и NEON за определением в рантайме, а на всём остальном - на 100% безопасный скалярный запасной вариант. Крейт base64 двойно лицензирован MIT/Apache-2.0, так что всё это бесплатно, включая скорость.
Четыре алфавита сверху
Алфавит RFC - по умолчанию, но крейт base64 отгружает ещё четыре, и каждый - маленький памятник какому-то реальному протоколу, которому нужна была собственная изюминка:
| Алфавит | Изюминка | Кто им пользуется | abc 123 кодируется в |
|---|---|---|---|
alphabet::CRYPT |
./ идут первыми, затем цифры и буквы, без заполнителя |
классические хеши паролей Unix crypt(3) | MK7X612mAk |
alphabet::BCRYPT |
./ первыми, затем буквы, затем цифры |
хеши паролей bcrypt | WUHhGBCwKu |
alphabet::IMAP_MUTF7 |
запятая подменяет слеш, без заполнителя | имена ящиков IMAP в modified UTF-7 | YWJjIDEyMw |
alphabet::BIN_HEX |
алфавит, богатый знаками препинания, который пропускает похожие буквы | BinHex 4, старая файловая обёртка Macintosh | B@*M)$%b-` |
use base64::engine::general_purpose::{GeneralPurpose, NO_PAD};
use base64::Engine;
let crypt = GeneralPurpose::new(&base64::alphabet::CRYPT, NO_PAD);
println!("{}", crypt.encode(b"abc 123")); // MK7X612mAk
let bcrypt = GeneralPurpose::new(&base64::alphabet::BCRYPT, NO_PAD);
println!("{}", bcrypt.encode(b"abc 123")); // WUHhGBCwKu
let imap = GeneralPurpose::new(&base64::alphabet::IMAP_MUTF7, NO_PAD);
println!("{}", imap.encode(b"abc 123")); // YWJjIDEyMw
Один и тот же вход, три разных вывода, все - валидный Base64 в своём диалекте. Алфавит crypt - тот, у которого есть настоящая суперсила: поскольку его символы упорядочены так, чтобы совпадать с битовыми шаблонами, сортировка закодированных строк даёт тот же порядок, что и сортировка исходных байтов, - поэтому GEDCOM 5.5 (1996) использовал его для полей мультимедиа - ревизия 5.5.1 отбросила эту фичу - а крейт до сих пор отгружает вам алфавит. А если нужный вам диалект не в крейте, вы можете определить его строкой в 64 символа, потому что Alphabet::new() строит таблицы кодирования и декодирования за вас:
use base64::alphabet::Alphabet;
use base64::engine::general_purpose::{GeneralPurpose, PAD};
use base64::Engine;
// base64 из мира перевертышей: +/ в начале, а не в конце
let alphabet = Alphabet::new(
"+/ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
).expect("a valid 64 char alphabet");
let bizarro = GeneralPurpose::new(&alphabet, PAD);
println!("{}", bizarro.encode(b"hello 99")); // YETqZE6eMRi=
// а стандартный движок говорит:
println!("{}", base64::prelude::BASE64_STANDARD.encode(b"hello 99"));
// aGVsbG8gOTk=
Одно предупреждение про путь с кастомным алфавитом: в момент, когда вы изобретаете диалект, вы становитесь единственным человеком на Земле, который может читать ваши данные, так что делайте это только когда того требует протокол, и пишите комментарий, какой именно.
Где работают кодировщики
Кодирование Base64 появляется в Rust-проектах в предсказуемом составе ситуаций:
- Загрузки файлов в JSON-API, где файл - байтовое поле в текстовом костюме, самое частое применение с огромным отрывом.
- Data URI в HTML и CSS, те, в роде
data:image/png;base64,..., чудесные для крошечных иконок, сомнительные для больших баннеров. - JWT и OAuth, где base64url - диалект, а крейт
jsonwebtoken- инструмент. - PEM-блоки для сертификатов и ключей, секции
-----BEGIN CERTIFICATE-----, которые переносят Base64 на 64 символа в строку. - Бинарные данные в XML и файлах конфигурации, паттерн
<data encoding="base64">, который до сих пор находишь в выгруженных закладках и дампках настроек. - LDAP- и LDIF-файлы, которые используют Base64, чтобы держать бинарные значения атрибутов в одной строке.
- Нагрузки QR-кодов и передачи через буфер обмена, где текст переживает дорогу, а бинарные - нет.
- Заголовки HTTP Basic-авторизации, где
Basic TWFuOnBhc3M=- пара учётных данных, и напоминание, что это задача упаковки, а не сокрытия.
И золотое правило, которому подчиняется всё это: Base64 - упаковочный скотч, а не замок. Это не шифрование и не сжатие - это противоположность сжатия, и любой, у кого есть эта статья, может развернуть всё, что он делает, в одну строку. Кодируйте свободно, но никогда не кодируйте пароль, API-ключ или секрет и не называйте это защищённым. Если нужно скрыть, используйте настоящее шифрование, а если данные большие, подумайте, не оказалась бы многочастная загрузка просто дешевле, чем этот налог.
Десятилетие маленьких шагов
Формату больше, чем вебу. В 1987 году протоколу Privacy-Enhanced Mail (RFC 989) нужно было нести бинарные данные по 7-битным почтовым каналам, и он стандартизировал это кодирование со строками ровно в 64 символа. Каждый блок -----BEGIN CERTIFICATE----- в интернете - потомок того решения, и поэтому PEM-файлы до сих пор переносятся на 64. В 1996 году спецификация MIME (RFC 2045) приняла схему, назвала её «base64» в честь 64-символьного алфавита и перенесла перенос строк на 76 символов. До всего этого Unix-машины отгружались с uuencode, а Macs - с BinHex, у каждого со своим алфавитом, и оба до сих пор всплывают в старых системах, как фоссилии с файловыми заголовками. В 2006 году RFC 4648 стал стандартом, на который ссылаются все: таблицы алфавитов, вариант base64url и канонические правила кодирования, которые реализует каждый движок в этой статье. Его раздел 3.5 требует от кодировщиков обнулять неиспользуемые хвостовые биты, и крейт так делает; если ваша нагрузка позже споткнётся о проверку InvalidLastSymbol строгого декодера, повреждение случилось выше по течению.
Собственная история крейта рифмуется. Он появился на crates.io в декабре 2015 года, и версия 0.5.0 с гордостью добавила MIME-перенос строк с настраиваемыми символами перевода. Затем версия 0.10.0 в 2018 году убрала перенос и обработку пробельных символов - библиотека рассудила, что общий крейт должен кодировать и оставить поэзию прикладному слою; тот же релиз добавил потоковый EncoderWriter. Версия 0.20.0 в 2022 году ввела абстракцию движка и сделала канонический заполнитель значением по умолчанию, а 0.21.0 объявила устаревшими старые свободные функции в пользу методов движка, с пометкой компилятора «Use Engine::encode» (они всё ещё работают, поэтому много легаси-кода компилируется с довольным лицом). В 2024 году версия 0.22.0 отточила семантику ошибок и ускорила декодирование на 5-10 процентов. А в июле 2026 года пришла версия 0.23.0 с SIMD-движками, пользовательскими символами заполнения, более ясным сообщением об ошибке и поднятием MSRV до 1.71, а патч 0.23.1 от 4 августа починил тестовую базу для не-SIMD-архитектур.
На что стоит улыбнуться
Потому что полное руководство должно закончиться улыбкой:
- Слово «base64» кодируется в
YmFzZTY0. Формат, описывающий сам себя, - технический эквивалент зеркала, которое говорит на морзе. - Пустая строка кодируется в пустую строку. Ничто - единственный вход, который ничего не стоит, и это своего рода освобождение от налога.
AA==- не кодирование ничего; это кодирование одного NUL-байта. В Base64 «ничто» и «ноль» - разные существа, и декодеры их различают.- Каждый Base64-кодированный PNG начинается с
iVBORw0K. Это магическое число PNG в его упаковочном скотче, один из самых узнаваемых префиксов в интернете. - В URL стандартным символам Base64 нужны экранирующие костюмы: плюс становится
%2B, слеш -%2F, а заполнитель -%3D. Base64url существует, чтобы символы могли носить свои собственные лица. - ID видео на YouTube - это base64url без заполнителя: восемь байтов ID становятся одиннадцатисимвольной строкой, которую можно вставить куда угодно. Одно из самых зримых применений режима без заполнителя на всём интернете.
- Старый алфавит паролей crypt(3) сортируется правильно: отсортированные закодированные строки стоят в том же порядке, что и отсортированный открытый текст. GEDCOM 5.5 (1996) использовал тот алфавит для своих полей мультимедиа, ревизия 5.5.1 отбросила фичу, а крейт до сих пор отгружает его для вас.
- BinHex, старая Macintosh-обёртка, построил свой алфавит так, чтобы исключить визуально похожие символы вроде
7,O,gиo. Кодировщик, спроектированный для человеческого глаза, в мире до проверки орфографии. - У FAQ самого крейта по поводу заполнителя есть откровенное мнение: эксабайты хранилища и передачи, без сомнения, были потрачены впустую на бессмысленные байты
=. Платный пункт собирает пошлину с 1987 года. - Base64 - не шифрование. Если бы оно им было, вы не смогли бы прочесть вывод любого примера в этой статье. Это место у окна, а не сейф.
Короткая версия
Выбирайте движок по дороге, по которой поедут данные: BASE64_STANDARD для всего, что вы сами же и декодируете, движки _NO_PAD, когда вы контролируете оба конца и хотите байты обратно, URL_SAFE_NO_PAD для токенов и URL, а кастомный Alphabet - только когда протокол настаивает. Определите размеры буферов через encoded_len(), гоняйте большие вещи потоком через EncoderWriter и всегда закрывайте finish(), переносите строки через line-wrap и pem только когда того требует формат, пусть SIMD-движки делают тяжёлую работу, когда можете, и помните, что обмен четыре-за-три - цена прохода через текстовую дверь. Кодируйте всё, защищайте только то, чему нужен настоящий замок. А когда понадобится пойти в обратную сторону - распаковать строку обратно в те байты, с которых началось путешествие, - сестринская статья разбирает декодирование в Rust, со всей сводной таблицей точных сообщений об ошибках.
Последнее обновление: 2026-09-08
Связанная статья: Декодирование Base64 в Rust: полное руководство