Декодирование Base64 в Dart: полное руководство
Оно приезжает в ответе API, спрятанным внутри URL или вставленным в тикет поддержки: длинная полоса букв и цифр, где изредка мелькают +, /, - или _, а на хвосте, может быть, болтаются пара знаков =. Кто-то говорит, что это Base64, и вам нужно то, что внутри. Это руководство - рецепт на Dart, как вернуть всё обратно. Быстрая ориентация, потому что домашняя страница подробно разбирает формат: Base64 переписывает каждые три входных байта в четыре символа из 64-символьного алфавита и прицепляет к концу один-два заполнителя =, когда последний кусок короче. Декодирование - это сокращающееся направление этой сделки: четыре символа входят, три байта выходят, поэтому результату всегда нужно примерно на четверть меньше места, чем вводу.
А теперь хорошая новость: устанавливать нечего. Base64 поставляется с библиотекой dart:convert с версии Dart 1.13 (2015 год), и API стабильно с Dart 2.0 (2018 год). Один импорт даёт вам быстрый строгий декодер, который читает и стандартный алфавит, и URL-безопасный.
Одна честная граница: здесь мы на стороне декодера. Вы узнаете, что именно декодер принимает, а что отклоняет, как работает заполнитель, как возвращать байты в текст без кракозябр и как встречать Base64 в JWT, data URI, файлах, потоках, почте, конфигурации и командной строке. Другое направление - укладывать байты в строку - получит своё руководство, и ссылка на него будет в конце этой статьи.
Четыре двери в одну строгую машину
Вот весь публичный интерфейс, который вам понадобится, и всё это живёт в dart:convert:
| Точка входа | Что это | Когда тянуться за ним |
|---|---|---|
base64Decode(source) |
Функция верхнего уровня, декодирует в Uint8List |
Декодирование в быту, почти всегда именно она |
base64.decode(source) |
Метод декодирования кодека, поведение идентичное | Нужен кодек для fuse или преобразований потока |
base64Url.decode(source) |
Метод декодирования URL-безопасного кодека | Вход документирован как URL-безопасный (машина та же самая) |
base64Url.normalize(source) |
Проверяет и чинит строку, возвращает её с заполнителем | Во вводе может не быть заполнителя, могут смешиваться алфавиты или использоваться процентные экранирования |
Здесь два момента. Первый: все четыре дороги ведут в один и тот же декодер - одну строгую машину состояний с одной таблицей соответствия. Второй: последняя строка - вообще не декодер. Это ремонтная станция, и она окупится в первый же раз, когда появится JWT без заполнителя или наполовину вычищенное значение из конфигурации.
Ваше первое декодирование
Девяносто процентов жизни декодирования умещается в пять строк. Вот самый маленький пример, который показывает весь облик работы:
import 'dart:convert';
void main() {
final bytes = base64Decode('TWFu');
final text = utf8.decode(bytes);
print(text); // Man
}
Три предложения о том, что только что произошло. Первое: точка входа возвращает байты, а не текст: base64Decode отдаёт Uint8List, и это намеренно, потому что нагрузка может быть фразой, JPEG или хэшем, и до того, как вы поймёте, что у вас в руках, не следует обращаться со всеми ними одинаково. Второе: прыжок из байтов в текст - это отдельный, явный шаг с явной кодировкой, и именно на этом шаге «café» превращается в кракозябры, если вы невнимательны. Третье: пустая строка - значение первого класса: base64Decode('') даёт вам список нулевой длины, без исключений и без торжественности.
Что декодер принимает, а что отклоняет
Декодер Dart строгий по замыслу. RFC 4648 говорит, что реализации должны отклонять ввод с символами вне алфавита, и Dart следует этому прочтению в буквальном смысле: без пропускания пробелов, без игнорирования переводов строк, без второго шанса. Когда ввод неверен, вы получаете FormatException, который показывает ввод и указывает на точный символ. Вот поведение на классических неприятелях:
| Ввод | Что не так | Точная ошибка |
|---|---|---|
'SGVs bG8s' |
пробрался пробел | FormatException: Invalid character (at character 5) |
'SGVs\nbG8s' |
пробрался перевод строки | FormatException: Invalid character (at character 5) |
'SGVs$bG8s' |
знак доллара не входит в алфавит | FormatException: Invalid character (at character 5) |
'Zm8' |
заполнителя нет совсем | FormatException: Invalid length, must be multiple of four (at character 4) |
'Zm8==' |
два заполнителя там, где положен один | FormatException: Invalid padding character (at character 5) |
'Zm=8' |
заполнитель посреди данных | FormatException: Invalid encoding before padding (at character 3) |
'Zm8=xx' |
мусор после заполнителей | FormatException: Invalid padding character (at character 5) |
'Zé' |
символ вне ASCII | FormatException: Invalid character (at character 2) |
Позиция в сообщении - это номер символа, отсчитанный от единицы, а сам ввод печатается прямо под указателем, так что место порчи в нагрузке находится быстро делениями пополам. Одно приятное удивление прячется в строгости: декодер принимает оба алфавита. - или _ посреди стандартной строки - не проблема, и + или / в URL-безопасной строке тоже. Выбор алфавита имеет значение только когда текст производите вы, а не когда читаете его.
Заполнитель: о котором не торгуются
Вот правило, которое удивляет больше всего: декодер Dart требует правильного заполнителя. Длина ввода должна быть кратна четырём символам, и хвостовые знаки = должны быть в ровно нужном количестве. Никакого мягкого режима, ни одного флага, чтобы ослабить строгость, и ни одной настройки, чтобы поменять это. Причины веские: декодирование без заполнителя неоднозначно в крайних случаях, и RFC предупреждает, что слишком снисходительное декодирование может открыть скрытый канал, поэтому строгое прочтение - безопасное. Что это значит на практике:
| Ввод | Результат |
|---|---|
'' |
пустой Uint8List, без ошибки |
'QQ==' |
1 байт: A |
'QUI=' |
2 байта: AB |
'QUJD' |
3 байта: ABC |
'Zm8' |
FormatException: неверная длина |
'Zm8==' |
FormatException: неверный символ заполнителя |
Когда ввод приходит из системы, которая срезает заполнитель, а JWT полны значений без заполнителя, чинит всё один вызов normalize. Он проверяет строку, переводит URL-безопасные символы в стандартный алфавит и добавляет недостающие заполнители:
import 'dart:convert';
void main() {
final stripped = '-__--Q';
final repaired = base64Url.normalize(stripped);
print(repaired); // +//++Q==
final bytes = base64Decode(repaired);
print('decoded ${bytes.length} bytes'); // decoded 4 bytes
}
Сюрприз со знаком процента
Этот трюк - оригинальная разработка Dart. Когда Base64 появляется в data URI, некоторые инструменты процентно кодируют заполнитель, записывая %3D вместо =, потому что голое = в URL-синтаксисе может значить «разделитель параметров». Большинство языков заставили бы вас сначала снять экранирование. Декодер Dart - нет: его таблица соответствия принимает %3D как нативную запись символа заполнителя, так что ему можно подать сырую нагрузку:
import 'dart:convert';
void main() {
final fromDataUri = 'SGVsbG8%3D';
final bytes = base64Decode(fromDataUri);
print(utf8.decode(bytes)); // Hello
}
Экранирование принимается ровно там, где законен заполнитель, то есть в хвостовой позиции. Поставьте %3D там, где = было бы отклонено, - и он отклонится тем же самым способом, а %25 провалится уже на проверке заполнителя: % - нативный символ процентного экранирования заполнителя в Dart, так что декодер читает его как экранированный = и отклоняет 2 с ошибкой Invalid padding character. На практике это значит, что нагрузка из ;base64,, скопированная прямо из инструментов разработчика браузера, декодируется без какой-либо предобработки - маленькая, но по-настоящему удобная штука.
URL-безопасный Base64
RFC 4648 определяет второй алфавит по одной причине: в стандартном есть три символа, +, / и =, которые сталкиваются с URL-синтаксисом. URL-безопасный алфавит, который в RFC называется base64url, подменяет + на - и / на _, и часто сбрасывает и заполнитель. Это алфавит JWT, ID объектов, ссылок на шаринг и всего, что живёт внутри URL или имени файла.
Со стороны декодирования у Dart один ответ: оба алфавита читает одна и та же машина. base64Decode и base64Url.decode - два имени одного декодера, так что единственная настоящая работа - это заполнитель, потому что URL-безопасные производители очень часто отгружают без него. И именно для этого существует normalize:
import 'dart:convert';
void main() {
final bytes = [0xfb, 0xff, 0xfe, 0xf9];
final urlSafe = base64UrlEncode(bytes);
print(urlSafe); // -__--Q==
final repaired = base64Url.normalize(urlSafe.replaceAll('=', ''));
print(repaired); // +//++Q==
print(base64Decode(repaired).length); // 4
}
Оставлю вам две ловушки. Не катите собственную замену - на + до декодирования: она не нужна, а normalize и так уже делает перевод алфавита, когда это нужно. И не рассчитывайте, что URL-безопасная строка приезжает без заполнителя: некоторые производители оставляют его, и декодер принимает оба варианта, пока заполнитель корректен.
От байтов к тексту: выбор кодировки
Декодирование Base64 отдаёт вам байты. Если эти байты - текст, вам нужно выбрать кодировку, которая вернёт их обратно в String, и этот выбор вы делаете явно. Догадкой по умолчанию в современных системах является UTF-8, и utf8.decode - это рабочая лошадка:
import 'dart:convert';
void main() {
final payload = base64Encode(utf8.encode('Héllo Wörld'));
final bytes = base64Decode(payload);
print(utf8.decode(bytes)); // Héllo Wörld
final legacy = base64Encode(latin1.encode('Héllo'));
print(latin1.decode(base64Decode(legacy))); // Héllo
}
Когда байты не являются корректным UTF-8, utf8.decode бросает FormatException, и это правильное поведение, гораздо лучше, чем тихие кракозябры. Если вы знаете, что данные - это легаси-текст однобайтной кодировки, используйте соответствующую кодировку:
| Кодировка | Для чего | Декодируйте через |
|---|---|---|
utf8 |
Современный текст, JSON, всё, что в вебе | utf8.decode(bytes) |
latin1 |
Легаси-данные западной однобайтной кодировки | latin1.decode(bytes) |
ascii |
Простой 7-битный текст | ascii.decode(bytes) |
Одна ловушка заслуживает отдельного предупреждения: String.fromCharCodes - это не кодировка. Он читает байты как UTF-16-кодовые единицы, так что если подать ему UTF-8-байты Héllo, он напечатает Héllo с совершенно невозмутимым видом. Если вы видите в выводе тот самый узор кракозябр, лекарство почти всегда одно: utf8.decode.
JWT: читаем токен
JSON Web Token - это три base64url-части, склеенные точками: заголовок, нагрузка, подпись. Base64 используется здесь ради компактности и URL-безопасности, а не ради секретности. Любой, у кого есть токен, может прочитать заголовок и нагрузку, и так задумано. Вот подпись - то, что вы проверяете, общим секретом или публичным ключом выпускающего. Декодирование читаемых частей в Dart занимает несколько строк:
import 'dart:convert';
Map<String, dynamic> readJwtPayload(String token) {
final parts = token.split('.');
if (parts.length != 3) {
throw FormatException('Not a compact JWT');
}
final padded = base64Url.normalize(parts[1]);
final bytes = base64Decode(padded);
return jsonDecode(utf8.decode(bytes)) as Map<String, dynamic>;
}
void main() {
const token =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9'
'.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkRhcnQgRGV2IiwiaWF0IjoxNTE2MjM5MDIyfQ'
'.c2lnbmF0dXJl';
print(readJwtPayload(token)['name']); // Dart Dev
}
Обратите внимание на танец с заполнителем: JWT собирают без заполнителя, поэтому часть упадёт при прямом base64Decode, всякий раз когда её длина не кратна четырём. (В примере выше заголовок случайно получился длиной 36 символов и декодируется напрямую, а нагрузка - 74 символа, и напрямую она не декодируется.) Вызов normalize делает ремонт единообразным, независимо от длины. Ещё два предупреждения. Декодирование - это не проверка: сверка подписи и требования exp - отдельный обязательный шаг, обычно с пакетом crypto для HMAC-алгоритмов. И относитесь с подозрением к токенам, которые заявляют alg: none: парсер, который их принимает, - это уязвимость, а не фича.
Data URI: файлы в URL-костюме
Data URI, определённый RFC 2397, - это URL, чья нагрузка есть сами данные: data:image/png;base64, за которым следуют закодированные байты. Они существуют для того, чтобы чисто текстовые каналы - атрибуты HTML, правила CSS, JSON-документы - могли нести бинарник без отдельного файла. Base64 - формат нагрузки по выбору, потому что альтернатива, процентное кодирование, для бинарных данных заметно длиннее.
А Dart умеет парсить их нативно: поддержка data URI есть в dart:core с 2016 года, так что никакой URI-библиотеки не нужно:
import 'dart:convert';
void main() {
final uri = Uri.parse('data:image/png;base64,iVBORw0KGgo=');
final data = uri.data!;
print(data.mimeType); // image/png
print(data.isBase64); // true
print('decoded ${data.contentAsBytes().length} bytes');
final textUri = Uri.parse('data:text/plain;base64,SGVsbG8sIERhcnQh');
print(textUri.data!.contentAsString()); // Hello, Dart!
}
Объект UriData отдаёт вам MIME-тип, флаг isBase64, сырой текст нагрузки и декодированное содержимое как строку или как байты. Две ловушки: заявленный MIME-тип может врать, так что в чувствительном к безопасности коде проверяйте настоящие магические байты. И data URI - для маленьких ассетов, потому что вся нагрузка едет внутри того документа, который на неё ссылается.
Файлы: Base64 на диске
Base64-файлы появляются в экспортных форматах, provisioning-пакетах и в любом чисто текстовом переносе, которому нужно нести бинарник. Рецепт: прочитайте текст, разровняйте его, декодируйте, запишите байты:
import 'dart:convert';
import 'dart:io';
Future<void> main() async {
final encoded = await File('image.b64').readAsString();
final flat = encoded.replaceAll(RegExp(r'\s+'), '');
final bytes = base64Decode(flat);
await File('image.png').writeAsBytes(bytes);
print('wrote ${bytes.length} bytes');
}
Вот этот replaceAll делает настоящую работу. Текстовые файлы полны переводов строк, часто MIME-переносов по 76 символов, и строгий декодер их отклоняет, так что сначала разравнивайте. Регулярное выражение убирает все пробельные символы, и именно этого хочется для чистого base64-файла. Если в файле могут быть другие пометки, например PEM-заголовки, снимите их явно до декодирования, и пусть ошибки декодера выловят то, что порчено по-настоящему.
HTTP и API
Base64 в HTTP носит два костюма. Первый: ответы API, JSON-поле, которое несёт бинарник в виде строки. Второй: заголовок Authorization: Basic, где учётные данные закодированы в base64 стандартным алфавитом и с заполнителем:
import 'dart:convert';
import 'package:http/http.dart' as http;
Future<void> main() async {
final response = await http.get(
Uri.parse('https://httpbin.org/get?attachment=TWFuIGlzIGhlcmU%3D&name=man.txt'),
);
final payload = jsonDecode(response.body) as Map<String, dynamic>;
final args = payload['args'] as Map<String, dynamic>;
final bytes = base64Decode(args['attachment'] as String);
print('got ${bytes.length} bytes');
final credentials = utf8.decode(base64Decode('b2N0b2NhdDpzZWNyZXQ='));
print(credentials.split(':').first); // octocat
}
Пакет http - стандартный клиент, он в одном dart pub add http от вас. Для Basic-аутентификации декодируйте часть после префикса Basic . Две ловушки: некоторые API присылают URL-безопасные или не дополненные значения там, где в документации сказано base64, так что если прямое декодирование падает, сначала прогоните значение через base64Url.normalize. И помните, что Basic-аутентификация - это маскировка, а не защита, поэтому место её - только в TLS-соединениях.
Почта и MIME: проблема переводов строк
Почта - самый старый клиент base64. MIME переносит base64-строки по 76 символов - 76 плюс CRLF спокойно укладывается в 80-колоночный дисплей - а RFC 2045 говорит декодерам игнорировать переводы строк. Декодер Dart этого не делает, намеренно: он их отклоняет. Лечение - разровнять перед декодированием:
import 'dart:convert';
List<int> decodeMimeBody(String wrapped) {
final flat = wrapped.replaceAll(RegExp(r'\s+'), '');
return base64Decode(flat);
}
void main() {
const wrapped =
'SGVsbG8gZnJvbSBhbiBlbWFpbCBhdHRhY2htZW50LCB3cmFwcGVkIGF0IDc2IGNoYXJhY3RlcnMg'
'\r\n'
'dGhlIHdheSBNSU1FIHdhbnRzIGl0IHRvIGJlLCB3aXRoIENSTEYgYmV0d2VlbiB0aGUgbGluZXMu';
print(utf8.decode(decodeMimeBody(wrapped)));
}
Правило простое: убрать пробельные символы, и ничего больше. Не вычищайте другие символы в надежде помочь: декодер - валидатор, и вы хотите, чтобы он жаловался на настоящую порчу. Если обрабатываете почту большими объёмами, шаг разравнивания дёшев, это один проход регулярного выражения, и он держит остальной конвейер честным.
Конфигурация и переменные окружения
Токены и учётные данные, живущие в текстовой конфигурации, иногда кодируют в base64, чтобы они помещались в одну строку и выглядели как токены. Честная рамка: base64 - это маскировка, а не шифрование, так что этот паттерн - для аккуратности, никогда не для секретности. Сам паттерн тривиален:
import 'dart:convert';
import 'package:dotenv/dotenv.dart';
Future<void> main() async {
final env = DotEnv()..load();
final encoded = env['API_TOKEN_B64'];
if (encoded == null) {
return;
}
final token = utf8.decode(base64Decode(encoded));
print('loaded a ${token.length}-char token');
}
С пакетом dotenv значение сидит в файле .env в виде API_TOKEN_B64=c2stbGl2ZS1hYmMxMjM= и возвращается обычным текстом после декодирования. Та же самая форма работает с String.fromEnvironment для значений dart-define времени компиляции, с одним предупреждением: значения dart-define запекаются в скомпилированный бинарник, так что всё секретное - в конфигурации времени выполнения или в менеджере секретов, а не там.
Потоки: чанк за чанком
Когда закодированный текст приезжает кусками - сетевой поток, большой файл, читаемый блоками, - декодер справляется. Его машина состояний переносит неполные группы через границы чанков, так что чанкам не нужно выравниваться по 4-символьным границам:
import 'dart:convert';
Future<void> main() async {
final incoming = Stream.fromIterable(['TWF', 'uaGVsbG8=']);
final text = await incoming
.transform(base64.decoder)
.map(utf8.decode)
.join();
print(text); // Manhello
}
Вызов transform использует декодер как преобразователь потока. Первый чанк, три символа, паркует свои биты в состоянии декодера, а второй чанк завершает группу. Ошибки проступают как ошибки потока с теми же деталями FormatException, а пустой поток просто не производит вывода. Если вам ближе sink'и, base64.decoder.startChunkedConversion даёт вам StringConversionSink, подключённый к той же машине состояний.
Большие данные: арифметика и память
Декодирование сжимает: четыре символа становятся тремя байтами, поэтому вывод всегда чуть меньше трёх четвертей длины ввода. Это значит, что размер вывода можно узнать до декодирования, а память становится предсказуемой. Маленький помощник считает его по одной строке:
import 'dart:convert';
int decodedLength(String encoded) {
var padding = 0;
for (var i = encoded.length - 1; i >= 0 && padding < 2; i--) {
if (encoded.codeUnitAt(i) == 0x3d) {
padding++;
} else {
break;
}
}
return (encoded.length ~/ 4) * 3 - padding;
}
void main() {
print(decodedLength('QQ==')); // 1
print(decodedLength('QUI=')); // 2
print(decodedLength('QUJD')); // 3
}
Встроенный декодер быстрый: один проход по таблице соответствия без строковых аллокаций на каждый символ, так что строки в несколько мегабайт - обычное дело. Где base64 берёт налог - на стороне ввода: закодированный текст примерно на 33 процента больше данных, и это строка, которая на VM живёт как UTF-16-кодовые единицы, примерно вдвое длиннее байтовой длины закодированных символов. Для нагрузок, которые могут расти, декодируйте через поток, а не склеивайте одну большую строку.
Из командной строки
VM Dart превращает декодер в аккуратный CLI. Этот маленький инструмент читает аргумент-файл или стандартный ввод, разравнивает пробельные символы и записывает сырые байты в стандартный вывод:
import 'dart:convert';
import 'dart:io';
Future<void> main(List<String> args) async {
String encoded;
if (args.isNotEmpty) {
encoded = await File(args[0]).readAsString();
} else {
encoded = await stdin
.transform(utf8.decoder)
.join();
}
final flat = encoded.replaceAll(RegExp(r'\s+'), '');
stdout.add(base64Decode(flat));
await stdout.flush();
}
Сохраните его как bin/decode.dart и запустите dart run bin/decode.dart image.b64 > image.png, или через конвейер: cat token.b64 | dart run bin/decode.dart. Вызов stdout.add принимает Uint8List напрямую, без промежуточной строки, и именно так бинарник должен двигаться через конвейер.
Ловушки, которые кусают разработчиков Dart
- Стена заполнителя. Ввод в стиле JWT и из URL-инструментов часто приезжает без знаков
=, и декодер отказывает ему с ошибкойInvalid length, must be multiple of four. Сначала прогоняйте недоверенный ввод черезbase64Url.normalize. - Ловушка пробельных символов. Текстовые файлы, почта и копирование-вставка вносят переводы строк, и декодер их никогда не пропускает. Разравнивайте через
replaceAll(RegExp(r'\s+'), '')до декодирования. - Уверенность в алфавите. Поскольку оба алфавита декодируются везде, не стройте логику на том, какой декодер произвёл строку. Строка - это контракт, а не настройки производителя.
- String.fromCharCodes - не кодировка. Он читает UTF-16-кодовые единицы, так что превращает UTF-8-текст в кракозябры. Используйте
utf8.decodeили явную кодировку. - Два разных типа ошибок. Проблемы декодирования - это
FormatException, а кодировщик бросаетArgumentErrorдля значений вне диапазона от 0 до 255. Ловите их по отдельности, если строите границу. - Результат фиксированной длины.
Uint8Listне умеет расти, так чтоbytes.add(1)бросаетUnsupportedError. Копируйте черезList<int>.from(bytes), когда нужен расширяемый список. - Не снимайте экранирование %3D вручную. Декодер читает процентно-экранированный заполнитель нативно; преждевременный
replaceAll('%3D', '=')привязывает ваш код к детали, которой SDK уже владеет. - Декодирование JWT-нагрузки - это не её проверка. Читать требования и доверять им - это дыра в безопасности, которая ждёт упорного пользователя.
Лучшие практики, короткий список
- По умолчанию -
base64Decode; тянитесь заnormalizeтолько на границе, где ввод недоверен. - Будьте явны с кодировкой:
utf8.decode(bytes), даже когда вы предполагаете UTF-8. - Держите байты байтами, пока не поймёте, что это;
Uint8Listчисто доезжает доFile.writeAsBytesи друзей. - На границах доверия ловите
FormatExceptionи логируйте позицию ввода, которую даёт сообщение. - Всё, что может превысить несколько мегабайт, - через поток.
- Относитесь к base64 как к формату, а не к защите: он ничего не прячет от того, кто знает, что это base64.
Короткая история Base64 в Dart
Декодер, с которым вы только что познакомились, старше Dart 3, null-безопасности и эры Flutter. Короткая версия:
- 18 ноября 2015 года, Dart 1.13: Base64 приезжает в
dart:convertконстантойBASE64плюс классамиBase64Codec,Base64EncoderиBase64Decoder. До этого релиза в SDK не было base64 совсем. - 28 января 2016 года, Dart 1.14:
Base64Decoder.convertполучает параметры диапазонаstartиend, а тот же релиз добавляет вdart:coreподдержку data URI, тот самый путьUri.parse, на который опирается эта статья. - 26 апреля 2016 года, Dart 1.16: URL-безопасный алфавит подключается как
BASE64URLи конструкторBase64Codec.urlSafe. - 7 августа 2018 года, Dart 2.0: константы переименовывают в строчные
base64иbase64Url, приезжают топ-уровневыеbase64Decodeи компания, декодирование возвращаетUint8Listвместо расширяемогоList<int>, аBase64Codec.normalizeвливается в семью, превращая проверку и ремонт в шаг в один вызов. - 2021 год, Dart 2.12: выходит null-безопасность, и вся история
dart:convert, base64 включительно, становится null-безопасной. - Сегодня, Dart 3.13: классы отмечены как
final, и поведение, с которым вы познакомились выше, - та же самая строгая машина, читающая оба алфавита и понимающая проценты, которая работает с 2015 года.
Строгость - не случайность реализации. Это декодер, который следует инструкции RFC 4648: реализации должны отклонять символы вне алфавита, а MIME-снисходительность оставлена приложениям, которым она нужна, - в Dart это шаг разравнивания перед декодированием.
Весёлые факты
- Декодер читает
%3Dкак нативный заполнитель. Подайте ему сырую нагрузку data URI, со всеми экранированиями, - и он её декодирует. Совсем мало рантаймов языков умеют так без шага предобработки. base64.decoderиbase64Url.decoder- буквально один и тот же объект: оба это канонический экземплярconst Base64Decoder(). «URL-безопасный декодер» - это стандартный декодер в другом костюме.- Весь декодер умещается в одну таблицу соответствия на 128 ячеек,
Int8List, общую для интерпретатора и AOT-скомпилированного кода, где+и-оба указывают на слот 62 алфавита, а/и_- оба на 63. - Base64 в Dart и поддержка data URI приехали с разницей в два релиза, в 1.13 и 1.14, и их явно планировали парой: один читает формат, второй читает его прямо из URL.
- Пустая строка декодируется в пустой
Uint8Listбез ошибки, а пустая строка кодируется в пустую строку: base64 считает отсутствие данных совершенно валидным сообщением. - В 2018 году, когда Dart 2.0 переименовал константы,
BASE64сталbase64в рамках SDK-общего перехода к строчным именам констант, той самой волны, которая подарила вамascii,jsonиutf8.
Теперь у вас есть полный декодер: что он принимает, что отклоняет, как чинить повреждённый ввод и как встречаться с ним в JWT, data URI, файлах, потоках, почте и в командной строке. Другое направление сделки, брать байты и производить один из двух алфавитов, с решениями о заполнителе и арифметикой размера, подробно разобрано в руководстве по кодированию Base64, ссылка на которое будет в конце этой страницы.
Последнее обновление: 2026-09-08
Связанная статья: Кодирование Base64 в Dart: полное руководство