Закодировать и декодировать строку
Buffer.from(string, "utf8") превращает текст в байты UTF-8, а .toString("base64") их кодирует. Декодирование – те же два вызова в обратном порядке.
const enc = Buffer.from("héllo wörld", "utf8").toString("base64");
console.log(enc); // aMOpbGxvIHfDtnJsZA==
console.log(Buffer.from(enc, "base64").toString("utf8")); // héllo wörld
"utf8" – значение по умолчанию в обоих вызовах, но если написать его явно, намерение сразу видно. Buffer – подкласс Uint8Array, так что обычный Uint8Array нужно обернуть: u8.toString("base64") игнорирует аргумент и возвращает "104,105", а Buffer.from(u8).toString("base64") возвращает aGk=.
URL-safe Base64: кодировка base64url
Для URL-safe алфавита в Node есть отдельная кодировка. Дополнение она никогда не пишет:
Buffer.from("<<???>>").toString("base64"); // PDw/Pz8+Pg==
Buffer.from("<<???>>").toString("base64url"); // PDw_Pz8-Pg
Buffer.from("PDw_Pz8-Pg", "base64url").toString(); // <<???>>
Если получателю нужен URL-safe Base64 с дополнением, допишите его сами: s + "=".repeat((4 - s.length % 4) % 4).
При декодировании Node снисходителен: и "base64", и "base64url" принимают любой из двух алфавитов и не обращают внимания на дополнение.
Buffer.from("PDw_Pz8-Pg==", "base64").toString(); // <<???>>
Buffer.from("PDw/Pz8+Pg==", "base64url").toString(); // <<???>>
Для JWT и токенов это удобно (попробуйте декодер JWT), но прочитайте раздел о проверке ниже. Подробно о двух алфавитах рассказано в статье Base64 и Base64URL.
Закодировать и декодировать файл
fs принимает "base64" в качестве кодировки, и это экономит шаг в обе стороны:
import { readFileSync, writeFileSync } from "node:fs";
readFileSync("hello.txt").toString("base64"); // aGVsbG8K
readFileSync("hello.txt", "base64"); // aGVsbG8K (same thing)
writeFileSync("copy.txt", Buffer.from("aGVsbG8K", "base64"));
writeFileSync("copy2.txt", "aGVsbG8K", "base64"); // decodes while writing
Чтобы встроить файл в HTML или CSS, добавьте к результату префикс: `data:image/png;base64,${b64}`. Генератор data URI собирает такую же строку в браузере.
Большие файлы потоком
Читать файл на 2 ГБ в память ради кодирования – плохая идея. Поток Transform может кодировать по частям, если кодирует только целые группы по 3 байта, а остаток переносит в следующую часть:
import { createReadStream, createWriteStream } from "node:fs";
import { pipeline } from "node:stream/promises";
import { Transform } from "node:stream";
class Base64Encode extends Transform {
constructor() { super(); this.rest = Buffer.alloc(0); }
_transform(chunk, _enc, cb) {
const buf = Buffer.concat([this.rest, chunk]);
const cut = buf.length - (buf.length % 3);
this.rest = buf.subarray(cut);
cb(null, buf.subarray(0, cut).toString("base64"));
}
_flush(cb) { cb(null, this.rest.toString("base64")); }
}
await pipeline(createReadStream("big.bin"), new Base64Encode(),
createWriteStream("big.b64"));
На случайном файле в 1 МБ результат в точности совпал с readFileSync("big.bin").toString("base64"). Без переноса остатка каждая граница частей, не кратная 3, вставляла бы дополнение = в середину вывода. Почему так, объяснено в статье как работает Base64.
Чтобы узнать размер декодированных данных, не декодируя их, вызовите Buffer.byteLength("aGVsbG8K", "base64") – он вернёт 6.
Проверка входных данных
Buffer.from(s, "base64") никогда не бросает исключение из-за плохого содержимого. Он отбрасывает символы, которых нет ни в одном алфавите, и возвращает то, что осталось:
Buffer.from("aGk=!!", "base64"); // <Buffer 68 69>
Buffer.from("not base64 at all", "base64"); // <Buffer 9e 8b 5b 6a c7 ba e1 ab 5a 96>
Buffer.from("aGk", "base64").toString(); // hi (missing padding is fine)
Для недоверенных данных проверьте алфавит и длину, затем закодируйте результат заново и сравните. Такая проверка туда и обратно ловит ненулевые биты дополнения, например aGl=: она тоже декодируется в hi, но каноническим Base64 не считается.
function isBase64(s) {
return /^[A-Za-z0-9+/]*={0,2}$/.test(s) && s.length % 4 === 0 &&
Buffer.from(s, "base64").toString("base64") === s;
}
isBase64("aGk="); // true
isBase64("aGk"); // false (no padding)
isBase64("aGl="); // false (non-canonical)
isBase64("not base64"); // false
Для base64url замените регулярное выражение на [A-Za-z0-9_-] и уберите проверку длины. Валидатор Base64 делает те же проверки интерактивно.
Подводные камни и сообщения об ошибках
- Аргумент не того типа.
Buffer.from(123, "base64")бросаетTypeError [ERR_INVALID_ARG_TYPE]: The first argument must be of type string or an instance of Buffer, ArrayBuffer, or Array or an Array-like Object. Received type number (123). Обычно этоundefinedили число, проскочившее из JSON. new Buffer(). Конструктор устарел (DEP0005) из соображений безопасности. ИспользуйтеBuffer.fromилиBuffer.alloc.- Декодирование двоичных данных как текста.
.toString("utf8")на байтах картинки без всякой ошибки выдаёт символы замены (U+FFFD), и записанный обратно файл оказывается испорчен. Храните двоичные данные вBuffer. - Префикс data URI.
Buffer.from("data:image/png;base64,iVBOR...", "base64")не упадёт, а декодирует префикс как мусор. Сначала разделите строку по запятой. - Переносы строк. При декодировании Node игнорирует
\nи\r, так что строки с переносами в стиле MIME работают, но кодировщик сам строки не переносит. Если нужны строки MIME, используйтеb64.match(/.{1,76}/g).join("\r\n").
Версии
- Кодировка
base64url: Node.js 15.7.0 и 14.18.0. - Глобальные
btoaиatob: Node.js 16.0, помечены как устаревшие. Uint8Array.prototype.toBase64иUint8Array.fromBase64в Node.js 24 недоступны (typeofвозвращает"undefined"). Они придут с более новыми версиями V8, а пока остаётсяBuffer.Buffer.from(string, "base64")во всех версиях игнорировал пробелы и недопустимые символы, так что для проверки на него не полагайтесь.
Вопросы и ответы
Бросает ли Buffer.from(str, 'base64') исключение на некорректных данных?
Нет. Node пропускает символы вне алфавита и останавливается на первом =, так что мусор без всякой ошибки декодируется в какие-то байты. Если входным данным нельзя доверять, проверяйте строку сами.
В какой версии Node.js появился base64url?
Кодировка base64url появилась в Node.js 15.7.0 и была перенесена в 14.18.0. В любой актуальной версии на неё можно рассчитывать.
Стоит ли использовать btoa в Node.js?
Node помечает btoa и atob как устаревшие и рекомендует Buffer. Они существуют ради совместимости с браузером и страдают тем же ограничением Latin-1, что описано на странице Base64 в JavaScript.