Декодирование Base64 в Swift: полное руководство
Где-то в вашем конвейере данные прячутся в маскировке: токен, затолканный в HTTP-заголовок, аватар, спрятавшийся в поле JSON, файл .b64, который вы пообещали разобрать ещё на прошлой неделе, вложение в письме, пришедшее сплошной стеной букв. Снимать эти маски в Swift - одно из самых приятных занятий в языке: один фреймворк, один конструктор и сводка правил, которая умещается на стикере.
Домашняя страница этого сайта уже разбирает сам формат (64 печатаемых символа, шесть битов на символ, до двух символов = заполнителя в последней группе), поэтому рассказывать эту историю заново мы не будем. Просто держите в кармане два факта. Первый: base64 - это способ одеть байты в текст, а не замок. Второй: любое base64-путешествие в Swift проходит через единственный тип Data, и декодер живёт на нём в виде конструктора, который может вернуть nil. Именно этот факт определяет остальную часть статьи, потому что конструктор, который может вернуть nil, меняет то, как вы пишете каждую строку, что идёт за ним.
Один тип забирает всю работу
Swift не рассыпал base64-помощников по дюжине модулей и не заставляет ничего устанавливать. Декодер - это Data(base64Encoded:options:) из Foundation, и он входит в платформу с самых ранних дней фреймворка (Apple указывает этот конструктор начиная с iOS 8.0, macOS 10.10, tvOS 9.0, watchOS 2.0 и visionOS 1.0; параметры длины строки на стороне кодирования уходят корнями ещё в iOS 7.0). На Linux и Windows тот же Foundation поставляется с open-source тулчейном, поэтому код ниже ведёт себя одинаково в приложении для iPhone, серверном воркере и скрипте в вашем терминале.
Есть и брат-конструктор, Data(base64Encoded: Data, options:), на случай, если ваш base64 приходит сырыми ASCII-байтами, а не строкой. Оба принимают аргумент options со значением по умолчанию []. И оба делят одну черту характера, которая важнее любого параметра: они могут вернуть nil.
import Foundation
let packed = "SGVsbG8sIFN3aWZ0IQ=="
if let data = Data(base64Encoded: packed) {
let text = String(data: data, encoding: .utf8)
print(text ?? "not text after all")
} else {
print("that was not base64")
}
// Hello, Swift!
Документация Apple по этому конструктору прекрасна своей прямотой: он «возвращает nil, когда вход не распознаётся как корректный Base-64». Никаких исключений, ни одной брошенной ошибки, никакого спама в логах. Просто тихий nil и ваша ответственность решить, что это значит для вашего пользователя. Если вы запомните об base64 в Swift только одну вещь, пусть это будет: декодер никогда не падает и никогда не жалуется. Он просто отказывает.
Вердикт декодера: таблица «да» и «нет»
Что же значит «корректный» для этого декодера? Оказывается, это короткий список жёстких правил, и именно он разграничивает «работает в демо» и «выживает в продакшене». Каждая строка таблицы ниже - реальное поведение конструктора на актуальном тулчейне, так что её можно цитировать прямо в ваших сообщениях об ошибках:
| Вход | Вердикт | Почему |
|---|---|---|
TWFu |
Man |
полная группа из четырёх символов обходится вообще без заполнителя |
TQ== |
M |
один байт плюс два заполнителя, классический учебный случай |
SGVsbG8h |
Hello! |
восемь символов - число, кратное четырём, так что заполнители не нужны |
==== |
пустой Data |
заполнитель без ничего за ним - законная вещь, и он декодируется в ноль байтов |
| пустая строка | пустой Data |
ничего на входе, ничего на выходе, и опционал всё равно успешен |
TQ |
nil |
длина два: обещали группу из четырёх, но так и не выдали |
T |
nil |
один символ несёт шесть битов, а байту нужно восемь |
SGVsbG8hTQ |
nil |
десять символов: последняя группа болтается без своих заполнителей |
TQ=== |
nil |
три заполнителя: третьему уже нечем заполнять |
TQ==TQ |
nil |
данные после заполнителя - категорическое нет |
SGVs bG8h |
nil |
один пробел - вне алфавита, и строгий режим не проявляет милосердия |
SGVsbG8h плюс завершающий перенос строки |
nil |
перенос строки в конце только что прочитанного файла засчитывается как шум |
Тремя строкам стоит уделить второй взгляд. Строка с ==== означает, что проверка if let проходит, и ваш код плывёт дальше с нулём байтов, так что, если пустой пелод - не валидное состояние вашего приложения, проверьте счётчик сразу после декодирования. Строка с пустой строкой - тот же трюк с меньшей косметикой. А строка с завершающим переносом - самая частая причина, по которой base64-файл, идеально закодированный утром, отказывается декодироваться днём: кто-то по дороге добавил конец строки, и строгий декодер принимает это на свой счёт.
Есть ещё одно знаменитое мягкое место, которое таблица показать не может. Сравните TQ== и TS==: оба декодируются в один и тот же байт, M, потому что два младших бита этого последнего символа отбрасываются до того, как их вообще посмотрят. Попробуйте вместо этого Tg== - и получите N без споров. Декодер следит за символами, но закрывает глаза на хвостовые биты. Эта мягкость - не баг, но она означает, что две разные строки могут означать одни и те же данные, и это начинает иметь значение с момента, когда ваша система сравнивает, дедуплицирует или кэширует base64-значения (об этом больше в разделе о безопасности).
Когда вход шумит сильнее, чем вы думаете
Base64 из реального мира редко приходит одной безупречной строкой. Вложения в письмах переносятся по 76 символов с возвратом каретки и переводом строки после каждой строки - привычка, унаследованная от MIME-спецификации 1996 года, - а файлы сертификатов переносятся по 64 символа. У декодера есть ровно один параметр для работы с этим шумом, и он по-настоящему весомый:
import Foundation
let mimeBody = "SGVs\r\nbG8sIG1h\naWwgbm9pc2Uu"
if let data = Data(base64Encoded: mimeBody, options: .ignoreUnknownCharacters) {
print(String(data: data, encoding: .utf8) ?? "")
}
// Hello, mail noise.
.ignoreUnknownCharacters в документации описан как декодер, который «игнорирует неизвестные не-Base-64 байты, включая символы конца строки», и для этой работы он - правильный инструмент: шум удаляется, алфавит остаётся, и пелод выходит целым. Но у этого параметра есть слепое пятно, и именно оно больнее всего кусает разработчиков на Swift: он удаляет каждый символ вне алфавита, включая - и _ из base64url. Он не переводит их в + и /; он просто выбрасывает их. В зависимости от того, что останется после удаления, вы получите nil (когда выжившие символы уже не складываются в целые группы) или, что хуже, уверенный ответ с неверным числом байтов. 16-символьная base64url-строка, кодирующая 12 байтов, может вернуться из щадящего декодера 9 совершенно другими байтами, без ошибки и без извинений.
Правило, которое стоит держать: .ignoreUnknownCharacters нужен для транспортного шума (переносы строк, случайные пробелы от копипасты), никогда - не для различий алфавитов. Если пелод может оказаться base64url, сначала переведите символы сами, ровно как показано в следующем разделе, и подайте декодеру чистую стандартную строку.
Алфавит для URL
Раздел 5 RFC 4648 описывает двоюродного брата стандартного алфавита, с которым вы уже знакомы: base64url, где + становится -, / становится _, а заполнитель = обычно отбрасывается. Причина та же, что заставляет ваши URL оставаться честными: в строке запроса + при разборе форм читается как пробел, / - это разделитель пути, а = отделяет ключи от значений. RFC прямо говорит о связи между ними: URL-вариант «не следует считать тем же, что и base64-кодирование». JWT, сообщения Web Push, ID видео на YouTube и большинство современных API-идентификаторов говорят на base64url, так что ждите встречи с ним уже в первый день.
На стороне декодирования рецепт состоит из двух движений: переведите алфавит, а затем добейте заполнитель, потому что строгий декодер всё ещё хочет своё кратное четырём число.
import Foundation
extension String {
func dataFromBase64URL() -> Data? {
var fixed = self
.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")
let missing = fixed.count % 4
if missing > 0 {
fixed += String(repeating: "=", count: 4 - missing)
}
return Data(base64Encoded: fixed)
}
}
let tokenPart = "0S__zMWaTC-iVgJ-"
if let bytes = tokenPart.dataFromBase64URL() {
print(bytes.count) // 12
}
Строка с остатком от деления - весь трюк: base64url-нагрузки обычно приезжают без заполнителя, и один-два символа = (но никогда три) восстанавливают группу из четырёх, которую ждёт декодер. Версию этого расширения из пяти строк вы найдёте в удивительном числе Swift-кодобаз, и не зря. У неё есть одна причина стать короче в будущем: самые новые SDK от Apple (26.4 и новее, на момент написания) получили нативный параметр .base64URLAlphabet для кодировщика, тогда как соответствующие параметры декодирования всё ещё созревают в open-source Foundation за меткой доступности для более позднего тулчейна. Пока он не дойдёт до вашей минимальной версии сборки, это расширение - переносимый ответ, и по построению оно продолжит работать на любой версии.
Сначала байты, потом слова
Вот решение, которое декодер не может принять за вас: он выдаёт вам Data - мешок байтов без малейшего понятия, в какой кодировке был написан исходный пелод. Если пелод был текстом, выбор кодировки - ваша работа, и Swift даёт вам две двери из мира байтов с очень разным характером.
String(data:encoding:)- это строгая дверь. Она возвращает опционал и отвечаетnil, когда байты не валидны в той кодировке, которую вы назвали. Идеальна для проверки, опасна, если вы принудительно распакуете ответ.String(decoding:as:)- это дверь никогда не отказывает. Она всегда возвращает строку, подставляя заменяющий символ U+FFFD за всё, что не понимает. Идеальна для логов и предпросмотра, опасна, если вы сохраните результат и назовёте его данными.
import Foundation
let bytes = Data([0xC3, 0xA5]) // UTF-8 запись буквы a с кольцом сверху
print(String(data: bytes, encoding: .utf8) ?? "?") // a с кольцом сверху, прочитана правильно
print(String(data: bytes, encoding: .isoLatin1) ?? "?") // две растерянные буквы, те же байты
print(String(decoding: bytes, as: UTF8.self)) // a с кольцом сверху, и это никогда не падает
Рецепт, закрывающий почти всё: сначала пробуйте строгий UTF-8, потому что именно его почти всегда подразумевают современные API; откатывайтесь к ISO Latin-1 только тогда, когда договорённость молчит и вам лучше иметь читаемое, но неверное, чем молчание; дверь, которая никогда не отказывает, оставьте для отладочного вывода. И один невидимый незваный гость, о котором стоит помнить: если пелод начинается с UTF-8 BOM (три байта EF BB BF), строгое преобразование сохранит его, и ваша строка теперь начинается с невидимого символа U+FEFF, который тихо ломает проверки равенства и преобразования туда-обратно через JSON. Срезайте его проверкой префикса, когда спецификация не обещает его наличия.
Открытие файлов
Работа вида «есть файл .b64, вынь то, что он прячет» состоит из чтения, обрезки, декодирования и записи. Обрезка - не украшение; это разница между файлом, который открывается, и файлом, который возвращает nil, потому что инструменты, почтовые клиенты и редакторы обожают оставлять перенос строки в конце:
import Foundation
let inbox = URL(fileURLWithPath: "Downloads/avatar.b64")
let outbox = URL(fileURLWithPath: "Downloads/avatar.png")
let raw = try String(contentsOf: inbox, encoding: .utf8)
if let data = Data(base64Encoded:
raw.trimmingCharacters(in: .whitespacesAndNewlines)) {
try data.write(to: outbox)
} else {
print("the file was not base64 after all")
}
Если файл обёрнут в MIME (переносы каждые 76 символов), у вас есть два чистых выхода: декодируйте с .ignoreUnknownCharacters и позвольте параметру съесть концы строк, или срежьте их сами через replacingOccurrences перед строгим декодированием. Оба варианта - по одной строке. Для файлов, которые просто большие, декодируйте выровненными группами, а не читайте всё целиком: каждая группа из четырёх символов декодируется сама по себе, так что через границы чтения можно нести только текущую группу да небольшой остаток.
import Foundation
func decodeBase64Chunks(_ stream: InputStream, into result: inout Data) throws {
let chunkSize = 65_536
var buffer = [UInt8](repeating: 0, count: chunkSize)
var leftover = ""
result = Data()
stream.open()
defer { stream.close() }
while stream.hasBytesAvailable {
let read = stream.read(&buffer, maxLength: chunkSize)
if read < 0 { throw CocoaError(.fileReadUnknown) }
if read == 0 { break }
var text = String(decoding: buffer[0..<read], as: UTF8.self)
text = text.replacingOccurrences(of: "\r", with: "")
.replacingOccurrences(of: "\n", with: "")
text = leftover + text
if text.count % 4 != 0 {
let whole = text.count - (text.count % 4)
leftover = String(text.suffix(text.count - whole))
text = String(text.prefix(whole))
} else {
leftover = ""
}
guard !text.isEmpty else { continue }
guard let part = Data(base64Encoded: text) else {
throw CocoaError(.fileReadCorruptFile)
}
result.append(part)
}
if !leftover.isEmpty {
guard let part = Data(base64Encoded: leftover) else {
throw CocoaError(.fileReadCorruptFile)
}
result.append(part)
}
}
Память остаётся плоской, каким бы большим ни был файл: один буфер чтения, один оставшийся фрагмент и тот результат, который вы строите. Тот же цикл обрабатывает и загрузку, которая приезжает по проводам в base64, и лог-файл, который на деле - закодированный поток, и любой пелод, слишком большой, чтобы удержать его в руках.
JWT: чтение трёх точек
Компактный JSON Web Token - это три base64url-части, склеенные точками, и первые две из них - обычный JSON в пальто. Они приезжают без заполнителя, а это ровно та комбинация, которую строгий декодер отвергает с порога, поэтому ваш помощник dataFromBase64URL() из раздела про URL делает всю тяжёлую работу:
import Foundation
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
func openPart(_ part: String) -> String? {
var fixed = part
.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")
let missing = fixed.count % 4
if missing > 0 {
fixed += String(repeating: "=", count: 4 - missing)
}
guard let data = Data(base64Encoded: fixed) else { return nil }
return String(data: data, encoding: .utf8)
}
let pieces = token.split(separator: ".")
print(openPart(String(pieces[0])) ?? "?")
// {"alg":"HS256","typ":"JWT"}
print(openPart(String(pieces[1])) ?? "?")
// {"sub":"1234567890","name":"John Doe"}
Вместе с этим едут два напоминания. JWT подписан, а не зашифрован: заголовок и пелод - открытая информация, и именно поэтому пароль в него никогда не кладут (зашифрованный двоюродный брат, JWE, - это вообще другая спецификация). А третья часть, отделённая точкой, - криптографическая подпись, а не документ, так что декодируйте части первую и вторую и оставьте остальное в покое.
Data URI: файл за запятой
Веб-API любят прятать двоичные данные в тексте с помощью схемы data:: PNG в поле профиля, шрифт в CSS-блобе, QR-код в файле настроек. Формат - data:{mime};base64,{payload}, и снять пелод с него можно одним разделением:
import Foundation
let uri = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
let payload = uri.components(separatedBy: ",").last ?? ""
if let bytes = Data(base64Encoded: payload) {
print(String(decoding: bytes.prefix(6), as: UTF8.self)) // GIF89a
print(bytes.count) // 42
} else {
print("not a base64 data uri")
}
В примере используется знаменитый прозрачный GIF в 42 байта, самое маленькое изображение в этом формате, поэтому его начальные символы встречаются в кодобазах чаще, чем почти любая другая base64-строка в интернете. На платформах Apple конвейер заканчивается одной строкой: тот же самый Data, который вы только что декодировали, подаётся прямо в UIImage(data:) или NSImage(data:), поэтому «показать аватар из API» - это маленькая фича, а не проект.
HTTP: заголовок Basic и его друзья
Старый заголовок Authorization: Basic - это имя пользователя и пароль, склеенные двоеточием и упакованные в дорогу стандартным base64 (не URL-диалектом: этот живёт в заголовке, где + и / совершенно безобидны). Распаковать его - одно деление и декодирование:
import Foundation
let header = "Basic ZWRpdG9yOnMzY3JldA=="
let packed = header.replacingOccurrences(of: "Basic ", with: "")
if let creds = Data(base64Encoded: packed) {
print(String(data: creds, encoding: .utf8) ?? "") // editor:s3cret
} else {
print("malformed header")
}
Держите сноску о безопасности громкой, потому что она относится к каждому base64, с которым вы когда-либо встретитесь: это упаковка, а не защита. Basic-аутентификация приемлема только поверх HTTPS, где TLS делает реальную охрану, а base64 лишь не даёт байтам сломать грамматику заголовка. То же рассуждение объясняет токены Authorization: Bearer: сам токен - это JWT, поэтому рецепт декодирования из раздела про JWT применяется к нему без изменений.
Почта: привычка к 76 символам
Вложение в письме, закодированное в base64, переносится по 76 символов с CRLF-концами строк - ровно тот шум, для которого и существует мягкий параметр. Сырые MIME-заголовки подсказывают, какой алфавит и какой перенос использовал отправитель (Content-Transfer-Encoding: base64), и лекарство - один флаг:
import Foundation
let attachment = "VGhpcyBhdHRhY2htZW50IHN1cnZpdmVk\r\nIHRoZSA3Ni1jaGFyYWN0ZXIgaGFiaXQu"
if let data = Data(base64Encoded: attachment, options: .ignoreUnknownCharacters) {
print(String(data: data, encoding: .utf8) ?? "")
}
// Это вложение пережило привычку переноса по 76 символов.
Если вы пишете почтовую фичу, а не читаете чужую, помните, что перенос по 76 символов обходится вам дорого: с переносом каждые 76 символов закодированный текст выходит примерно 137 процентами от исходного размера, поэтому старые почтовые инженеры оценивали размеры вложений на глазок по формуле «умножьте исходник на 1.37 и добавьте около 800 байт заголовков». Сейчас это фольклор, но арифметика осталась той же арифметикой.
Дважды обёрнутый пелод
Самый частый тикет «мои данные повреждены» в base64-крае - это данные, упакованные дважды: один интеграционный слой их закодировал, а второй слой, так и не прочитавший документацию, закодировал результат. Оборонительный ход - декодировать один раз, посмотреть, что получили, и если результат сам оказывается чистой base64-похожей строкой (верная длина, верный алфавит, ничего странного), декодировать ещё раз, осознанно, и остановиться. Не пишите цикл, который декодирует, пока не провалится. Такой цикл охотно съест совершенно хороший файл, содержимое которого случайно похоже на base64, и после его прогона никто не сможет сказать, где начинались исходные данные.
import Foundation
func unwrapOnce(_ packed: String) -> Data? {
let cleaned = packed.trimmingCharacters(in: .whitespacesAndNewlines)
return Data(base64Encoded: cleaned)
}
let suspicious = "WVdKag==" // уже выглядит упакованным
if let first = unwrapOnce(suspicious) {
let inner = String(data: first, encoding: .utf8) ?? ""
if let second = unwrapOnce(inner) {
print("it was wrapped twice:", String(data: second, encoding: .utf8) ?? "?")
}
}
// it was wrapped twice: abc
Две распаковки, два осознанных решения, и пелод, который наконец снова просто abc.
Как заставить nil иметь значение
Поскольку декодер отвечает nil, а не бросает исключение, стиль обработки ошибок вашего base64-кода - это выбор, который делаете вы, и выбором, которым вы позже обрадуетесь, будет небольшая обёртка, превращающая тихий отказ в громкую конкретную ошибку:
import Foundation
enum Base64Failure: Error, CustomStringConvertible {
case notBase64(Int)
var description: String {
switch self {
case .notBase64(let length):
return "input of \(length) characters is not valid base64"
}
}
}
func decodeStrict(_ text: String) throws -> Data {
let cleaned = text.trimmingCharacters(in: .whitespacesAndNewlines)
guard let data = Data(base64Encoded: cleaned) else {
throw Base64Failure.notBase64(cleaned.count)
}
return data
}
do {
let bytes = try decodeStrict("c3ludGF4IGVycm")
print(String(data: bytes, encoding: .utf8) ?? "?")
} catch {
print(error) // input of 14 characters is not valid base64
}
Обёртка становится ещё и единственным местом, где живёт нормализация: обрезка, любой перевод алфавита, любая доводка заполнителя. Вызывающие получают одну функцию, один смысл неудачи и ни одного ! на горизонте. Принудительная распаковка Data(base64Encoded:)! - это путь, по которому плохой пелод превращается в упавшее приложение, и обёртка - дешёвая страховка от этого. Тот же паттерн работает в командной строке, где скрипт с CommandLine.arguments и записью через FileHandle превращает «декодируй этот файл из shell» в утилиту на пять строк вместо обходного пути через копипаст на сайт.
Безопасность, измеряемая в байтах
- Это не шифрование. Base64 - обратимая переупаковка, читаемая мгновенно. Если в вашей модели угроз есть человек с браузером и пятью секундами, у вас нулевая защита, и каждый JWT-заголовок доказывает это ежедневно.
- Канонизируйте перед сравнением. Поскольку
TQ==иTS==декодируются в одни и те же байты, две системы могут хранить разные написания одних и тех же данных. Статья 2022 года «Маллиабельность Base64 на практике» задокументировала, что эта сломанная гарантия уникальности делает в дикой природе: рассинхрон логов, DoS-атаки и дубли записей в базе. Если ваше Swift-приложение кэширует, дедуплицирует или сравнивает base64-значения, прогоните один канонический декод (или один канонический перекод) на входе. - Ограничивайте вход до декодирования. Декодирование N символов выделяет примерно три четверти от N байтов, пока вы ещё держите входную строку. Злонамеренный клиент может прислать 100 мегабайт буквы
Aи смотреть, как ваша память ползёт вверх, прежде чем декодер вообще скажет «нет». Сначала проверьте длину, дёшево, и отклоните то, что слишком большое. - Берегитесь мягкого параметра как фильтра.
.ignoreUnknownCharactersудаляет символы. «Очищающий» прогон через него может превратить корректную base64url-нагрузку в другие данные без единой ошибки. Это фильтр шума для переносов строк, а не валидатор. - Держите его подальше от URL, где можете. Крупные base64-нагрузки в строках запроса или путях раздувают URL далеко за комфортные пределы и ломаются прокси. Лучше положите их в тела запросов, файлы или токены.
Производительность, кратко
Декодер - это обход таблицы соответствия: каждый символ ищется в небольшой таблице, и несколько битов сдвигаются и связываются побитовым «или» в выходные байты. На актуальном тулчейне этого достаточно быстро для всего, что помещается в памяти, и число, которое стоит запомнить, - коэффициент выхода: декодированные байты - примерно три четверти длины входа, так что 4-мегабайтная строка обходится примерно в 3 мегабайта результата сверх уже удерживаемой вами строки. Если вы на пути, где сам Foundation не допускается (глубоко встроенная цель, WebAssembly-бандл), заметной альтернативой является пакет сообщества swift-extras-base64: чистый Swift без зависимости от Foundation, кодировщик и декодер, соответствующие RFC 4648, с опциями base64url и заполнителя, и бенчмарки, по которым он в несколько раз быстрее Foundation. Более ранняя реализация того же пакета даже поставляется внутри поддержки WebSocket в swift-nio, - это примерно настолько близко к уровню продакшена, насколько может приблизиться хобби-проект. Для обычного приложения или скрипта это ненужный багаж; для ограниченного угла Swift - стандартный ответ.
Десятилетие распаковки
Swift не изобрёл ничего из этого, и стоит знать, откуда взялась каждая часть этой сумки инструментов:
- 1980-е, эпоха одинаковых машин. Раннейшие кодировщики этого семейства (uuencode на UNIX, BinHex на TRS-80 и классическом Mac) перемещали файлы между машинами, которые предполагали, что на другом конце такая же машина. uuencode использовал алфавит из заглавных букв, цифр и знаков препинания, и его буквы стоят на последовательных ASCII-позициях, так что кодирование было делом прибавки 32 вообще без таблицы. Декодеры той эпохи могли позволить себе предполагать многое, и в момент, когда данные пересекали экосистемы, всё падало.
- 1987, у алфавита появляется адрес. RFC 989 (Privacy-Enhanced Mail, февраль 1987) стандартизировал 64-символьный алфавит, переносил строки ровно по 64 символа и использовал
=для заполнения и*для пометки закодированных, но незашифрованных данных. Каждый блок в стиле PEM - потомок того документа. - 1996, либеральная эпоха. MIME (RFC 2045) взял алфавит для почты, сдвинул перенос на 76 символов и велел соответствующим декодерам игнорировать любые символы вне алфавита, такие как CRLF-переносы строк. Это эпоха, которая приучила целое поколение ждать прощающих декодеров, - и чьи ожидания строгое значение по умолчанию Swift намеренно ломает.
- 2003-2006, правила твердеют. RFC 3548 (2003) нанёс первый удар по объединению семейства; RFC 4648 (октябрь 2006) завершил дело, оформил правила заполнителя и добавил URL-безопасный алфавит. Его абзац про декодер - тот самый, которому следует Swift: отклоняйте символы вне алфавита, если только формат, который вы обслуживаете, явно не велит их игнорировать, как это делает MIME.
- 2013-2014, API ждёт в резерве. Класс
NSDataот Apple уже годами упаковывал и распаковывал base64, и API на основе опций со своей опцией декодирования приземлилось в iOS 7, в 2013 году, за год до того, как Swift вообще появился. Когда Swift 1.0 вышел 9 сентября 2014 года, декодер вошёл в язык вместе с ним и с тех пор не меняет характер: строгое ядро, один мягкий рычаг, конструктор, который может вернуть nil. - 3 декабря 2015, Linux получает декодер. Swift в тот день стал open-source, и вместе с ним base64 из Foundation перешёл на Linux, а позже - на Windows. «Декодирование base64 в Swift на машине не от Apple» - едва ли не десятилетие: поздний гость на вечеринке, которая началась в 1987-м.
- 2023-2026, переписывание. Переписывание Foundation (проект swift-foundation) перевело
Dataв чисто Swift-ядро, и в 2025 году питч сообщества добавил нативные опции base64url и отбрасывания заполнителя. На момент написания самые новые бета-SDK и open-source тулчейн поставляют опции кодирования, тогда как опции декодирования всё ещё созревают в open-source тулчейне, так что написанные вручную расширения пока остаются универсальным ответом.
Маленькие чудеса
====- легальный вход. Четыре заполнителя без данных декодируются в пустойData, единственную base64-строку, всё содержимое которой - «здесь ничего нет», и Swift с этим согласен.- Символьная полиция декодера не проверяет работу битовой:
TS==иTQ==оба выдают вамM, аTg==выдаётN. Грамматика одна, биты разные, вопросов не задаётся. Dataв Swift умеет декодировать base64, пришедший байтами, а не строкой, через вариантData(base64Encoded: Data), так что пелод, пересекший провод в ASCII, может вообще пропустить круговое путешествие через строку.- Слово, которое живёт в base64 с рождения тестовых векторов, -
foobar, и оно упаковывается вZm9vYmFy. Если вы когда-либо видели base64-пример в дикой природе, есть приличный шанс, что в нём участвовал foobar. - Знаменитый прозрачный GIF 1x1 - ровно 42 байта и начинается с магического слова
GIF89a, поэтому его первые восемь закодированных символов встречаются в кодобазах чаще, чем почти любой другой base64-префикс на Земле. - Современный open-source декодер проверяет невалидные символы одним сравнением: он связывает побитовым «или» четыре значения из таблиц по позициям и сравнивает результат со значением-сентинелом, так что одна ветка решает судьбу целой группы из четырёх символов. Старая реализация делала ту же работу 128-байтовой таблицей, где любое значение от 0x80 и выше означало «не буква».
- UTF-8 BOM невидимы:
EF BB BFв начале пелода превращается в символ U+FEFF, который переживает строгое преобразование, а потом ломает проверки равенства пару строк кода ниже. - Swift на 27 лет моложе алфавита, который он декодирует. Язык вышел в 2014 году; 64 буквы, с которыми он работает, были стандартизированы в 1987-м и с тех пор не менялись.
Вот и вся сумка инструментов декодера: один конструктор, который может вернуть nil, с короткой сводкой правил, один мягкий рычаг с задокументированным слепым пятном, пятистрочный помощник для base64url, решение про кодировку, которое остаётся за вами, циклический прогон для больших файлов и обёртка, которая заставляет nil иметь значение. Декодирование - то место, где base64 кусается, и теперь вы знаете имена всех зубов. Когда работа переворачивается и вы начинаете упаковывать байты в дорогу, а не распаковывать, включается наценка примерно 33 процента и появляются опции переноса. Связанная статья про кодирование полностью покрывает эту половину кругосветки, так что заглядывайте туда, когда будете готовы отправляться в обратном направлении.
Последнее обновление: 2026-09-08
Связанная статья: Кодирование Base64 в Swift: полное руководство