Почему atob() возвращает байты, а btoa() отклоняет некоторый текст

Ознакомьтесь с двоичными строками JavaScript, ошибками диапазона символов и шагом UTF-8, необходимым для atob и btoa.

Имена atob и btoa может предложить обычное преобразование текста. Их контракт браузера уже: они преобразуют текст Base64 в строку, значения символов которой представляют собой отдельные байты. Эта строка байтов является контейнером, а не гарантией того, что результатом будет читаемый текст в формате Unicode.

Две разные струны

В строках JavaScript используются кодовые единицы UTF-16. Байтовая строка намеренно ограничивает каждое значение символа диапазоном 0–255. btoa('Hello') работает, потому что значения ASCII соответствуют этому диапазону. btoa('你好') не удается, поскольку эти символы не помещаются в один байт. Даже подходящий персонаж может ввести в заблуждение: btoa('é') кодирует один байт E9, а не байты UTF-8 C3 A9, ожидаемые многими службами.

console.log(btoa('é')); // 6Q== (byte E9)
console.log(btoa(String.fromCharCode(0xc3, 0xa9))); // w6k= (UTF-8)

Поместите преобразование UTF-8 вокруг байтовых функций.

TextEncoder производит UTF-8 байт из текста. После декодирования Base64 фатальный TextDecoder преобразует допустимые байты UTF-8 обратно в текст и отклоняет недопустимые последовательности вместо их автоматической замены. Этот небольшой пример намеренно явный, поэтому граница видна.

const text = '中文😊';
const bytes = new TextEncoder().encode(text);
let binary = '';
for (const byte of bytes) binary += String.fromCharCode(byte);
const encoded = btoa(binary);
console.log(encoded); // 5Lit5paH8J+Yig==
const decodedBytes = Uint8Array.from(atob(encoded), c => c.charCodeAt(0));
const decoded = new TextDecoder('utf-8', { fatal: true }).decode(decodedBytes);
console.log(decoded); // 中文😊

Не рассматривайте двоичный результат как неработающую строку.

atob('/w==') представляет байт FF. Сам по себе UTF-8 недействителен, но это вполне допустимые двоичные данные. Правильным следующим действием может быть загрузка байтов или шестнадцатеричное представление, а не другой трюк преобразования текста. Запуск этой байтовой строки через TextEncoder закодирует символ U+00FF и изменит байты.

Декодер браузера также не является полноценным валидатором протокола. Допустимые пробелы и пропущенные дополнения могут отличаться от строгих требований приложения. Если протокол требует канонического кодирования, проверяйте алфавит, заполнение и неиспользуемые биты отдельно. Для больших файлов избегайте распределения полного массива байтов в String.fromCharCode(...bytes); Ограничения на количество аргументов могут выйти из строя задолго до того, как файл станет необычно большим.

Прочтите контракты браузера в МДН: atob() и МДН: btoa(). Приложение должно решить, хочет ли оно разрешающее преобразование, строгий валидатор или рабочий процесс с сохранением файлов, а не предполагать, что это одна и та же задача.