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

Декодирование 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: полное руководство