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

Кодирование Base64 в Dart: полное руководство

У вас есть байты, и вам нужна строка. Нагрузка может быть файлом, учётными данными для аутентификации, токеном конфигурации или бинарным комком, едущим внутри JSON-документа, а канал принимает только текст. Base64 - это сделка, которая это решает: каждые три входных байта становятся четырьмя символами из 64-символьного алфавита, поэтому вывод всегда кратен четырём и всегда безопасен в чисто текстовых мирах. Цена фиксирована: на 33 процента символов больше, и формат прицепляет один-два заполнителя = к концу, когда последний кусок короче. Это руководство - рецепт на Dart, как заключить эту сделку правильно.

Устанавливать нечего. Base64 поставляется с dart:convert с Dart 1.13 (2015 год), и API стабильно с тех пор; оба алфавита, стандартный и URL-безопасный, доступны больше десяти лет. Домашняя страница подробно разбирает формат; здесь сторона кодирования: полный API, дисциплина «сначала байты», которая обходит самый частый баг, выбор заполнителя и алфавита, и реальные работы: JWT, data URI, загрузка файлов, HTTP-заголовки, MIME, конфигурация, потоки и командная строка. Декодирование, обратное направление, имеет своё руководство, и ссылка на него будет в конце.

Один импорт, два алфавита, одно правило заполнителя

Весь публичный интерфейс кодирования живёт в dart:convert:

Точка входа Алфавит Когда тянуться за ним
base64Encode(bytes) стандартный: A-Z a-z 0-9 + /, с заполнителем API, MIME, Basic-аутентификация, большинство потребителей
base64UrlEncode(bytes) URL-безопасный: A-Z a-z 0-9 - _, всё равно с заполнителем URL, имена файлов, JWT, ID объектов
base64.encode(bytes) стандартный, идентичен вызову верхнего уровня Преобразования потоков и кодек-конвейеры
Base64Encoder().convert(bytes) стандартный Нужен именованный экземпляр кодировщика

Два правила покрывают все четыре строки. Первое: вход - это список байтовых значений, целых чисел от 0 до 255; всё остальное, включая отрицательные и 256 и выше, бросает ArgumentError, который называет неверный индекс. Второе: вывод всегда с заполнителем: нет ни флага, ни конструктора, ни опции, которые дали бы вывод без заполнителя, потому что заполнитель формата - свойство данных, и спецификации, которым он не нужен, срезывают его отдельным, задокументированным шагом. Самый маленький возможный пример, от начала до конца:

import 'dart:convert';
void main() {
  final text = 'Dart is open source';
  final bytes = utf8.encode(text);
  final encoded = base64Encode(bytes);
  print(encoded); // RGFydCBpcyBvcGVuIHNvdXJjZQ==
}

Сначала байты: порядок, который вас спасёт

Самый частый base64-баг Dart вообще не про base64. Он про порядок операций. Строка String в Dart - это последовательность UTF-16-кодовых единиц, и вызов base64Encode(text.codeUnits) упаковывает эти 16-битные единицы, а не байты, которых ждёт получатель. Для чистого ASCII два совпадают, и именно поэтому баг прячется, пока не приедет первый акцентированный символ, эмодзи или CJK-текст. Тогда кодировщик отказывается от работы, потому что кодовая единица вроде 0x4e16 - это не байтовое значение:

import 'dart:convert';
void main() {
  final message = 'Héllo Wörld 世界';
  print(utf8.encode(message).length); // 20
  print(message.codeUnits.length); // 14
  print(base64Encode(utf8.encode(message)));
  try {
    base64Encode(message.codeUnits);
  } on ArgumentError catch (e) {
    print(e);
  }
}

ArgumentError указывает на точный виновный индекс, поэтому сбой громкий, а не тихий. Дисциплина, которую нужно держать: решите, что это за байты, до того, как заговорить с base64. Текст идёт через именованную кодировку, utf8.encode для современных данных, и именно полученный List<int> отправляется на упаковку. Байты из файла или сетевого сокета и так приезжают Uint8List, что - правильная форма для кодировщика без какой-либо конвертации.

Заполнитель: работа кодировщика

Base64 отображает группы по три байта в четыре символа, поэтому нагрузка, чья длина не кратна трём, оставляет неполную группу на хвосте. Формат помечает этот недобор символами =: один байт на входе становится четырьмя символами плюс два заполнителя, два байта - четырьмя символами плюс один заполнитель, три байта - ровно четырьмя символами. Кодировщик Dart делает это за вас безоговорочно:

import 'dart:convert';
void main() {
  print(base64Encode([0x41])); // QQ==
  print(base64Encode([0x41, 0x42])); // QUI=
  print(base64Encode([0x41, 0x42, 0x43])); // QUJD
}

Это безоговорочное поведение - фича: вывод всегда является законной, самоописывающейся base64-строкой. Когда спецификация требует вариант без заполнителя, а обычная причина - JWT, срезание - ваш явный видимый шаг, а не настройка библиотеки:

base64UrlEncode(bytes).replaceAll('=', '')

Поставьте срезание на границу, где этого требует спецификация, назовите его и задокументируйте. Сторона декодирования этой сделки, включая то, как чинится повреждённый или срезанный ввод, разобрана в руководстве по декодированию.

URL-безопасный Base64

Стандартный алфавит содержит +, / и =, и эти три символа сталкиваются с URL-синтаксисом: разделители запросов, разделители путей и разделители параметров. URL-безопасный алфавит, стандартизированный как base64url в RFC 4648, подменяет + на - и / на _, так что вывод можно положить в сегмент пути, значение запроса или имя файла без экранирования. Вот разница на байтах, которые задействуют оба подменённых символа:

import 'dart:convert';
void main() {
  final tricky = [0xfb, 0xff, 0xfe, 0xf9];
  print(base64Encode(tricky)); // +//++Q==
  print(base64UrlEncode(tricky)); // -__--Q==
}

Выбирайте по потребителю, а не по вкусу. Если значение будет жить в URL, JWT или имени файла, кодируйте через base64UrlEncode и срезайте заполнитель, если спецификация без него. Если значение будет MIME-телом, заголовком Basic-аутентификации или полем API-контракта, где написано «base64», используйте стандартный алфавит, потому что base64 без уточнений - это стандартный. Два алфавита не взаимозаменяемы в глазах строгих потребителей: сервер, ожидающий стандартный base64, может отклонить нагрузку с - ошибкой 400 и ничем более полезным.

Кодировки: какие байты вы упаковываете?

Когда вход - текст, шаг кодирования решает, какие байты увидит base64, а потребитель на другом конце предполагает какую-то кодировку. Если ваше допущение не совпадёт с допущением потребителя, вывод будет совершенно валидным base64 неверных байтов, худшим из видов багов, потому что ничего не бросит исключение. Для любого современного обмена UTF-8 - значение по умолчанию, а другие однобайтовые кодировки существуют для легаси-данных:

Кодировка Для чего Кодируйте через
utf8 Современный текст, JSON, всё, что в вебе utf8.encode(text)
latin1 Легаси-данные западной однобайтной кодировки latin1.encode(text)
ascii Простой 7-битный текст ascii.encode(text)
import 'dart:convert';
void main() {
  final modern = base64Encode(utf8.encode('Héllo'));
  final legacy = base64Encode(latin1.encode('Héllo'));
  print(modern); // SMOpbGxv
  print(legacy); // SOlsbG8=
}

Одно и то же слово, разные байты, разный base64. Обратите внимание на длину: UTF-8 нужно шесть байтов для Héllo, потому что акцент - это двухбайтовая последовательность, а Latin-1 помещает его в пять. Если потребитель декодирует кодировкой, которой вы не пользовались, он получит кракозябры, и это будет выглядеть так, будто данные портились в пути, тогда как на самом деле они были порчены уже на уровне намерения.

JWT: пишем токен

JSON Web Token - это три base64url-части, склеенные точками: заголовок, нагрузка, подпись. RFC 7515 фиксирует две детали: алфавит URL-безопасный, а заполнитель опускается, потому что токен задуман для жизни в URL и заголовках. Подпись алгоритма HS256 - это HMAC-SHA256 от header.payload, который сам является base64url без заполнителя. Собрать это вручную пакетом crypto - дело нескольких строк, и это прозрачнее, чем кажется:

import 'dart:convert';
import 'package:crypto/crypto.dart';
String base64UrlNoPadding(List<int> bytes) {
  return base64UrlEncode(bytes).replaceAll('=', '');
}
String createJwt(Map<String, dynamic> header, Map<String, dynamic> payload,
    List<int> secretKey) {
  final signingInput =
      '${base64UrlNoPadding(utf8.encode(jsonEncode(header)))}.'
      '${base64UrlNoPadding(utf8.encode(jsonEncode(payload)))}';
  final mac = Hmac(sha256, secretKey).convert(utf8.encode(signingInput));
  final signature = base64UrlNoPadding(mac.bytes);
  return '$signingInput.$signature';
}
void main() {
  final token = createJwt(
    {'alg': 'HS256', 'typ': 'JWT'},
    {'sub': 'user-42', 'exp': 1893456000},
    utf8.encode('a-32-byte-secret-key-0123456789'),
  );
  print(token);
}

Подпись вычисляется по тем самым байтам, которые были упакованы, так что пока вы подписываете ту же строку, которую выдаёте, проверка на другом конце - повторение тех же шагов. Три предупреждения. Старый пакет jwt на pub.dev из 2014 года и старше null-безопасности; рабочий ответ экосистемы - делать то, что показано здесь, с crypto. Никогда не выдавайте токен с alg: none и никогда не позволяйте клиенту выбирать алгоритм. И помните, что нагрузку может прочитать любой, поэтому включайте только то, что токен должен доказать.

Data URI: отправка файлов внутри текста

Data URI, определённый RFC 2397, - это URL, чья нагрузка есть сами данные. Бинарное содержимое внутри data URI кодируется в base64, поэтому этот формат встречается везде, где текстовым документам нужно встроить изображения, шрифты или вложения: атрибуты HTML, CSS, JSON, файлы конфигурации. Dart может собирать такие URI нативно, ни один URI-пакет не нужен:

import 'dart:convert';
import 'dart:io';
Future<void> main() async {
  final png = await File('icon.png').readAsBytes();
  final imageUri = Uri.dataFromBytes(png, mimeType: 'image/png');
  print(imageUri); // data:image/png;base64,iVBOR...
  final note = Uri.dataFromString('Hello, Dart!');
  print(note); // data:,Hello,%20Dart!
}

Uri.dataFromBytes по умолчанию кодирует в base64 (для другой формы есть опция percentEncoded: true), и для бинарника это правильная кодировка. Uri.dataFromString по умолчанию делает процентное кодирование, потому что короткий текст так короче, и принимает флаг base64: true, когда нужна байтовая форма. Практическая ловушка - масштаб: нагрузка едет внутри документа, с надбавкой 33 процента, так что data URI - для маленьких ассетов, иконок и миниатюр, а не для отправки мегабайт через CSS.

Файлы: упаковка байтов для текстовых каналов

Бытовой случай: файл, который должен пройти через JSON, файл конфигурации или любой чисто текстовый транспорт. Паттерн: прочитать байты, закодировать, встроить:

import 'dart:convert';
import 'dart:io';
Future<void> main() async {
  final image = await File('photo.jpg').readAsBytes();
  final encoded = base64Encode(image);
  final upload = jsonEncode({
    'name': 'photo.jpg',
    'size': image.length,
    'data': encoded,
  });
  print('payload ${upload.length} chars for ${image.length} bytes');
}

Число, которое нужно держать в голове, - это рост: файл в 2000 байтов становится 2668 символами base64, и чуть больше, когда подтянутся JSON-ключи. Две ловушки. Первая: проверьте, что вход ещё не закодирован: base64-кодирование строки, которая уже в base64, - это классический баг двойного кодирования, и он «успешно» декодируется в ещё одну стену base64. Вторая: если канал способен нести бинарник (для этого как раз и существует multipart/form-data), несите бинарник: он на четверть меньше, а base64-налог - чистая трата.

HTTP и API: заголовки и нагрузки

Самое знакомое кодировочное задание в HTTP - заголовок Authorization: Basic: слово Basic, пробел и base64 username:password стандартным алфавитом:

import 'dart:convert';
import 'package:http/http.dart' as http;
Future<void> main() async {
  final credentials = base64Encode(utf8.encode('octocat:secret'));
  final client = http.Client();
  final response = await client.get(
    Uri.parse('https://httpbin.org/basic-auth/octocat/secret'),
    headers: {'Authorization': 'Basic $credentials'},
  );
  print(response.statusCode);
  client.close();
}

С пакетом http, одним dart pub add http от вас, заголовок - просто строка в запросе. Ловушка - в рамке безопасности: base64 здесь - маскировка, а не защита. Любой развернёт это за один шаг, и именно поэтому Basic-аутентификация принадлежит только TLS-соединениям, где защищает транспорт, а не кодировка. Для полей нагрузки API следуйте контракту: если написано base64, это стандартный алфавит с заполнителем, а URL-безопасный вариант - другая штука, которую строгие потребители отклонят.

Почта и MIME: перенос по 76

MIME, система, которая позволяет почте нести бинарник, использует base64 как кодирование пересылки содержимого, и RFC 2045 предписывает, чтобы закодированные строки не превышали 76 символов, а между ними стоял CRLF. Это ограничение - конвенция MIME: 76 плюс CRLF спокойно укладывается в 80-колоночный дисплей, - и каждый соответствующий стандарту кодировщик переносит строки. Кодировщик Dart выдаёт одну неразрывную строку, так что перенос - короткий шаг постобработки:

import 'dart:convert';
String wrapForMime(String base64Text, [int lineLength = 76]) {
  final buffer = StringBuffer();
  for (var i = 0; i < base64Text.length; i += lineLength) {
    final end = i + lineLength > base64Text.length
        ? base64Text.length
        : i + lineLength;
    buffer
      ..write(base64Text.substring(i, end))
      ..write('\r\n');
  }
  return buffer.toString();
}
void main() {
  final encoded = base64Encode(utf8.encode('Hello from an email attachment'));
  print(wrapForMime(encoded));
}

Переносите готовую строку, заполнитель включительно, и пусть последняя строка будет длинной, какой получится, до 76 символов. Единственное, чего делать не стоит, - срезать заполнитель перед переносом в надежде сэкономить символ: заполнители - часть закодированного содержимого, и потребитель, который собирает строки обратно, отклонит результат без них.

Конфигурация: секреты в одну строку

Токены, ключи и учётные данные с кавычками, переводами строк или другими неудобными символами иногда кодируют в base64, чтобы они чисто укладывались в строку конфигурации или переменную CI. Сначала честная рамка: это маскировка, а не шифрование, и всё, что когда-либо попадёт в репозиторий или лог, - публично. Используйте этот паттерн ради аккуратности, никогда ради секретности. Кодирование значения - один вызов:

import 'dart:convert';
String forEnvFile(String secret) {
  return base64Encode(utf8.encode(secret));
}
void main() {
  final line = 'API_TOKEN_B64=${forEnvFile('sk-live-abc123')}';
  print(line); // API_TOKEN_B64=c2stbGl2ZS1hYmMxMjM=
}

Затем значение сидит в файле .env, в CI-секрете или в define времени компиляции и возвращается обычным текстом после одного декодирования. Если секрет нужно защищать в пути или в хранилище, тянитесь за менеджером секретов или библиотекой шифрования; работа base64 здесь - держать текстовую обработку конвейера простой, и ничего больше.

Потоки: кодирование сквозь границы чанков

Когда байты приезжают чанками - сетевое чтение, файл, обрабатываемый блоками, - кодировщик справляется без всякого выравнивания с вашей стороны. Кодек переносит неполные группы через границы чанков, так что размер чанков не обязан быть кратным трём:

import 'dart:convert';
import 'dart:typed_data';
Future<void> main() async {
  final data = Uint8List(100000);
  for (var i = 0; i < data.length; i += 31) {
    data[i] = i % 256;
  }
  final chunks = <List<int>>[data.sublist(0, 777), data.sublist(777)];
  final encoded = await Stream.fromIterable(chunks)
      .transform(base64.encoder)
      .join();
  print('in: ${data.length}, out: ${encoded.length}'); // in: 100000, out: 133336
}

Два чанка неловких размеров, 777 и 99 223 байта, дают одну правильную строку из 133 336 символов, потому что кодировщик держит в резерве оставшиеся биты каждой неполной группы до прихода следующего чанка и выдаёт заполнитель только в самом конце. Если вам ближе sink'и, base64.encoder.startChunkedConversion даёт ту же машину состояний в виде ByteConversionSink (вы кормите его байтовыми чанками, он выдаёт строки), что естественно ложится на запись большого вывода в файл или сокет, никогда не склеивая одну большую строку.

Большие данные: пропускная способность и память

Арифметика размера точная и её стоит запомнить: длина вывода - это длина входа, делённая на три, округлённая вверх, умноженная на четыре. Один, два или три байта стоят четыре символа, дальше - ровные 33 процента надбавки. Формула - на случай, когда нужно зарезервировать буферы или отчитываться о прогрессе:

import 'dart:convert';
int encodedLength(int n) => (n + 2) ~/ 3 * 4;
void main() {
  print(encodedLength(100000)); // 133336
}

Скорость не ограничение: кодировщик - одно прохождение по таблице, оно справляется с мегабайтами за миллисекунды. Ограничения - сам налог на размер, который взимается и в сети, и в памяти, и то, что закодированная форма - строка. Держите оба в уме на масштабе: для нагрузок, которые могут расти, кодируйте через поток, как показано выше, вместо накопления одного большого списка и одной большой строки, а для повторяющейся передачи одних и тех же данных спросите себя, есть ли у канала бинарный режим, потому что 33 процента - это постоянная доплата, которую не вернёт ни один алгоритм.

Кодировщик в командной строке

VM превращает кодировщик в аккуратный CLI. Этот инструмент читает аргумент-файл или стандартный ввод и печатает кодирование стандартным алфавитом:

import 'dart:convert';
import 'dart:io';
Future<void> main(List<String> args) async {
  final bytes = await _read(args);
  stdout.writeln(base64Encode(bytes));
}
Future<List<int>> _read(List<String> args) async {
  if (args.isNotEmpty) {
    return File(args[0]).readAsBytes();
  }
  final all = <int>[];
  await for (final chunk in stdin) {
    all.addAll(chunk);
  }
  return all;
}

Сохраните его как bin/encode.dart и запустите dart run bin/encode.dart photo.jpg > photo.b64, или через конвейер: cat config | dart run bin/encode.dart. Спутник, декодер, который читает и разравнивает, - первый пример в руководстве по декодированию, и вместе два скрипта - маленький, но по-настоящему полезный набор инструментов для переноса бинарника через текстовые каналы.

Ловушки, которые кусают по дороге наружу

  • Ловушка codeUnits. base64Encode(text.codeUnits) упаковывает UTF-16-единицы, а не байты; для ASCII это работает, а на первой кодовой единице выше 255 бросает ArgumentError. Всегда сначала кодируйте текст именованной кодировкой.
  • Несовпадение алфавитов. Покормить потребителя, ожидающего стандартный алфавит, URL-безопасным выводом - это готовая ошибка 400. Решайте алфавит по спецификации, кодируйте один раз и не конвертируйте задним числом.
  • Допущения о заполнителе. Dart всегда добавляет заполнитель. Если спецификация хочет без заполнителя, срезайте его через replaceAll('=', '') как явный шаг на границе и пропишите это в контракте.
  • Дрейф кодировки. Закодировать байты Latin-1 для потребителя, который декодирует UTF-8, - получить валидный base64 неверных данных. Ничего не бросит исключение; текст просто окажется неверным.
  • Двойное кодирование. Base64-кодирование значения, которое уже в base64, токена, скопированного из другой конфигурации, - классический баг «декодируется в ещё одну стену base64».
  • Иллюзия приватности. Base64 - формат, а не шифр. Если в модели угроз есть читатель, ответ - шифрование, а не кодирование.
  • Устаревшие пакеты. Долгожилый пакет jwt на pub.dev старше null-безопасности; для JWT-работы crypto плюс несколько строк выше - поддерживаемый путь.

Когда пора брать что-то другое

  • Загрузка файлов через HTTP. Используйте multipart/form-data; он несёт сырые байты, так что налог в 33 процента пропускается целиком.
  • Крупные или повторяющиеся нагрузки. Сначала сжимайте, потом кодируйте: base64 от сжатого gzip текста значительно меньше, чем base64 от самого текста, и разжимающая сторона и так знает формат.
  • Короткий текст внутри URL. Для горстки символов процентное кодирование короче и держит значение читаемым для человека; data URI делают это за вас по умолчанию.
  • Отладочный вывод и логи. Шестнадцатеричный формат на 50 процентов длиннее base64 (двойной сырой размер против 4/3 у base64), но его куда легче просматривать, диффовать и передавать коллеге; для бинарных фрагментов в логах он обычно выигрывает.

Лучшие практики: список кодировщика

  • Кодируйте байты, никогда кодовые единицы; текст сначала идёт через именованную кодировку.
  • Выбирайте алфавит по спецификации потребителя, прежде чем писать вызов.
  • Срезайте заполнитель только там, где спецификация говорит «без», как видимый шаг на границе.
  • Пропишите кодировку явно в контракте; ничего не предполагайте про другую сторону.
  • Всё, что может расти, - через поток.
  • Относитесь к base64 как к формату для чисто текстовых каналов, никогда как к защите чувствительных данных.

Краткая история двух алфавитов

Формат, которым вы только что пользовались, старше любого релиза Dart, а варианты алфавитов, доступные вам, были стандартизированы за десятилетия до появления Dart. Короткая версия:

  • 1993, RFC 1521: MIME вводит base64 как кодирование пересылки содержимого для почты, со стандартным 64-символьным алфавитом и 76-символьным лимитом строки, по которому переносит эта статья. Работа формата - нести бинарник через текстовые каналы - датируется отсюда.
  • 1996, RFC 2045: отмена MIME, сделавшая правила заполнителя и длины строки base64 прочным стандартом.
  • 2006, RFC 4648: кодирование выносят из MIME и стандартизируют самостоятельно, добавляя URL-безопасный алфавит и совет, что декодерам следует отклонять невалидный ввод. Выбор из двух алфавитов, который вы получаете в Dart, пришёл из этого документа.
  • 2015, RFC 7515: JSON Web Signatures фиксируют base64url без заполнителя, конвенцию за каждым JWT.
  • ноябрь 2015 года, Dart 1.13: base64 приезжает в dart:convert; URL-безопасный вариант следует за ним следующей весной в Dart 1.16, а топ-уровневые вызовы base64Encode и base64UrlEncode, которые вы использовали выше, приходят в Dart 2.0 в 2018 году.
  • Сегодня, Dart 3.13: оба алфавита, всегда с заполнителем, в одном импорте от вас, та же строгая и простая машина с 2015 года.

Надбавка в 33 процента с 1993 года тоже не менялась. Это свойство математики, четыре символа за три байта, и каждая реализация, которой вы когда-либо воспользуетесь, на любом языке, платит её одинаково.

Весёлые факты с кодировочного верстака

  • Кодировщика нельзя выключить: в SDK нет флага для вывода без заполнителя, поэтому «срезать заполнители» - всегда ваш код, на вашей границе, на виду у всех.
  • Один байт становится четырьмя символами: base64Encode([65]) - это QQ==. Самая короткая возможная base64-строка - длиной в четыре символа, и только первые два из них несут информацию; последние два - заполнитель.
  • Оба кодировщика Dart добавляют заполнитель, включая base64UrlEncode. «Без заполнителя» в base64url - это конвенция потребителя из RFC 7515, а не свойство алфавита.
  • Стандартный алфавит задумывался как 7-битно печатаемый и с тех пор остаётся значением по умолчанию; то, что + и / в итоге заслужили URL-безопасные замены, - знак того, каким центральным он стал, а не дефект.
  • Те же 20 байтов UTF-8 строки Héllo Wörld 世界 упаковываются в SMOpbGxvIFfDtnJsZCDkuJbnlYw=, тогда как 14 кодовых единиц той же строки роняют кодировщик на индексе 12. Одни и те же символы, два совершенно разных вывода, один из которых - ошибка.
  • PEM-файлы, блоки -----BEGIN CERTIFICATE----- в каждом TLS-сертификате, - это base64, перенесённый по 64 символа с заголовками, и этот формат датируется 1987 годом, за шесть лет до того, как MIME опубликовал base64 для почты.

Теперь у вас есть вся сторона кодирования: поверхность API, дисциплина «сначала байты», решения о заполнителе и алфавите, и рабочие паттерны для JWT, data URI, файлов, HTTP, MIME, конфигурации, потоков и командной строки. Обратное направление, разбирать одну из этих строк, со всей строгостью декодера, его сюрпризом с процентным экранированием и его инструментами ремонта, разобрано в руководстве по декодированию Base64, ссылка на которое будет прямо ниже.

Последнее обновление: 2026-09-08

Связанная статья: Декодирование Base64 в Dart: полное руководство