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

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

У вас есть то, что должно отправиться в путь, а дорога - только текстовая: JSON API, который не принимает сырые байты, почтовый канал, помнящий своё 7-битное прошлое, URL, который давится на всё, чего не может назвать, конфигурационный файл, готовый принять лишь самые простые символы. Добро пожаловать на упаковочную сторону base64, где Swift превращает ваши байты в дружелюбную стену букв одним вызовом метода, с наценкой примерно в один лишний символ на каждые три байта и несколькими опциями переноса, которые существуют потому, что у двух разных десятилетий были мнения о длине строк.

Домашняя страница этого сайта уже подробно объясняет формат (64 печатаемых символа, четыре на каждые три входных байта, до двух символов = заполнителя в последней группе), поэтому лекция о формате окончена ещё до того, как началась. Два факта, которые стоит унести в эту статью: base64 - это упаковка, а не запирание на замок, и упаковка раздувает ваши данные примерно на 33 процента, что имеет значение каждый раз, когда вы рядом с ограничением по размеру. В Swift вся работа проходит через один тип, Data, и один единственный метод, base64EncodedString(options:). Единственный реальный навык, который здесь нужен, - знать, что происходит на двух шагах вокруг этого метода, потому что сам метод никогда не падает. Падать могут шаги.

Один метод, ноль оправданий

Всё, что связано с base64 в Swift, живёт на Data из фреймворка Foundation, и живёт там с первых релизов языка (Apple указывает этот метод начиная с iOS 8.0, macOS 10.10, tvOS 9.0, watchOS 2.0 и visionOS 1.0). Конвейер всегда состоит из одних и тех же трёх шагов: положите контент в Data, вызовите метод, отправьте строку в путь.

import Foundation

let note = "Pack it, wrap it, ship it."
let packed = Data(note.utf8).base64EncodedString()
print(packed) // UGFjayBpdCwgd3JhcCBpdCwgc2hpcCBpdC4=

Две детали в этих трёх строках заслуживают пристального взгляда. Первая: Data(note.utf8) - тихий шаг. Представление utf8 по определению может представить любой Unicode-скаляр, поэтому никогда не падает, - именно поэтому это вариант по умолчанию в большинстве примеров. Сродственный ему, но способный не удалиться, note.data(using:), может и действительно отвечает nil для некоторых кодировок, и всё это решение «какие именно байты» получит собственный раздел ниже, потому что это первое место, где ваши данные могут потеряться. Вторая: сам метод полный - он всегда отвечает, у него нет случая ошибки, и единственный вопрос, который он задаёт, - какой перенос строк вам нужен. Есть и брат, base64EncodedData(options:), который возвращает упакованный результат как Data из ASCII-байтов, а не как строку, для конвейеров, где следующая остановка - бинарный API, а не текстовое поле.

И поскольку половина из вас приехала через «моему Swift-приложению нужна base64-зависимость»: устанавливать нечего. Base64 входит в Foundation, Foundation входит в тулчейн, а тулчейн приезжает одинаково на всех платформах. На macOS это Xcode или инструменты командной строки, на Linux и Windows - установщик с swift.org, где актуальная стабильная ветка на момент написания - 6.3.x, а рекомендуемая передняя дверь - менеджер версий Swiftly, плюс официальные Docker-образы покрывают контейнерное сообщество. Ваш Package.swift остаётся пустым, и так и должно быть.

Первое настоящее решение: какие байты?

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

import Foundation

let phrase = "héllo"
print(phrase.data(using: .utf8)?.count ?? -1)              // 6
print(phrase.data(using: .ascii) == nil)                   // true
print(phrase.data(using: .utf16)?.count ?? -1)             // 12
print(phrase.data(using: .utf16LittleEndian)?.count ?? -1) // 10
print(phrase.data(using: .utf8)!.base64EncodedString())
// aMOpbGxv
print(phrase.data(using: .utf16LittleEndian)!.base64EncodedString())
// aADpAGwAbABvAA==
Преобразование Байты для «héllo» Что переносит base64
.utf8 6 aMOpbGxv, написание, которого ждут современные API
.ascii неудача с nil знак с акцентом лежит выше 0x7F, и ASCII его отвергает
.utf16 12 вдвое больше, чем UTF-8, плюс двухбайтовая метка байтового порядка (BOM), едущая в самом начале
.utf16LittleEndian 10 то же слово без метки BOM: 10 байтов, и это всё равно самый тяжёлый из без-BOM вариантов, перечисленных здесь

В этом выводе спрятаны три урока. Форма data(using:) может не удалиться, и .ascii - главный кандидат на провал, так что принудительная распаковка - это путь, по которому безупречная фраза становится упавшим приложением. Простое преобразование .utf16 добавляет в начало двухбайтовую метку байтового порядка (FF FE на little-endian машине), и этот BOM едет в ваш упакованный вывод и сбивает с толку любой декодер, который его не ждал. А арифметика размера беспощадна: небрежный выбор кодировки обходится в base64-наценку на вдвое большие данные, поэтому вопрос никогда не в «закодируется ли это?», а в «что другой конец ожидает найти, когда распакует?». Золотое правило: оба конца пути должны договориться о байтовой форме до того, как начнётся base64, потому что у декодера нет способа угадать ваш выбор, и спрашивать он не будет.

Перенос: две привычки, один параметр

Опции метода целиком про перенос строк, и все они существуют потому, что два формата двадцатого века не могли сойтись в том, какой длины должна быть строка букв. MIME, почтовый стандарт 1996 года, переносит base64 по 76 символов с CRLF в конце строк. PEM, наследник Privacy-Enhanced Mail 1987 года, переносит по 64 символам, и именно такую форму вы найдёте внутри сертификатов и ключей, в блоках -----BEGIN CERTIFICATE-----, которые ваши серверы хранят в каталоге конфигурации.

import Foundation

let certBytes = Data((0..<300).map { UInt8($0 % 256) })
let raw = certBytes.base64EncodedString()
let pemStyle = certBytes.base64EncodedString(options: [.lineLength64Characters, .endLineWithLineFeed])
let mimeStyle = certBytes.base64EncodedString(options: [.lineLength76Characters,
  .endLineWithCarriageReturn, .endLineWithLineFeed])
print(raw.count)                                        // 400 символов на одной строке
print(pemStyle.components(separatedBy: "\n").count)    // 7 строк, не длиннее 64
print(mimeStyle.components(separatedBy: "\r\n").count) // 6 строк, не длиннее 76
Опция Работа Осторожно
.lineLength64Characters обрезает строку после 64 символов, привычка PEM конец строки - CRLF, если вы не скажете иначе
.lineLength76Characters обрезает строку после 76 символов, привычка MIME тот же CRLF по умолчанию
.endLineWithCarriageReturn добавляет возврат каретки в конец строки сам по себе это только CR, в стиле старых Mac, и редко то, что вам нужно
.endLineWithLineFeed добавляет перевод строки в конец строки для CRLF передавайте обе опции

А теперь то значение по умолчанию, которое удивляет людей: попросите любую опцию .lineLength, не выбрав конец строки, и вы получите CRLF, полную пару возврат-каретки-плюс-перевод-строки. У метода есть домашний стиль, и его домашний стиль - 1996 год. Хотите только LF? Оплатите это явно опцией .endLineWithLineFeed и ничем больше. Ещё одно домашнее правило для истории: последняя строка никогда не получает завершающий конец. Перенесённый результат заканчивается последним символом данных или своими = заполнителями, какими бы опциями вы ни воспользовались, так что вы можете склеивать и вставлять без сиротской пустой строки в конце. А вообще без опций вывод - одна неразрывная строка, и это правильная форма для JSON-тел, URL и API-нагрузок: та работа, которую обычное Swift-приложение делает чаще всего.

Base64url: строка, способная путешествовать

Стандартный алфавит - хороший гражданин JSON и ужасный гражданин URL. В строке запроса + при разборе форм читается как пробел, / - это разделитель пути, а = отделяет ключи от значений, поэтому процентное кодирование стандартного алфавита делает его длиннее и уродливее, а не короче. Раздел 5 RFC 4648 существует, чтобы исправить ровно это: «алфавит, безопасный для URL и имён файлов», где + становится -, / становится _, а заполнитель = обычно отбрасывается, потому что в URL заполнитель чаще всего превращается в %3D, сводя на нет всю идею. RFC добавляет предупреждение, достойное рамки: это кодирование «не следует считать тем же, что и base64-кодирование». Им говорят ID видео на YouTube, JWT и большинство современных API-идентификаторов, так что ждите его в работе.

import Foundation

extension Data {
  var base64URLEncoded: String {
    base64EncodedString()
      .replacingOccurrences(of: "+", with: "-")
      .replacingOccurrences(of: "/", with: "_")
      .replacingOccurrences(of: "=", with: "")
  }
}

let tricky = Data("The + / and = trio goes home.".utf8)
print(tricky.base64EncodedString())
// VGhlICsgLyBhbmQgPSB0cmlvIGdvZXMgaG9tZS4=
print(tricky.base64URLEncoded)
// VGhlICsgLyBhbmQgPSB0cmlvIGdvZXMgaG9tZS4

Посмотрите на этот вывод повнимательнее: этот конкретный пелод случайно не породил ни +, ни /, поэтому два написания различаются только отброшенным заполнителем. Измените один байт - и они расходятся в алфавите, а в этом весь смысл. Два правила боя. Выберите диалект один раз, на границе, где ваши данные встречают внешний мир, и никогда не смешивайте алфавиты внутри одного документа: стандартный декодер, получивший base64url (или наоборот), либо отвергнет вход, либо в мягких режимах удалит чужие символы и выдаст вам неверные байты. И называйте свою вспомогательную функцию честно, чтобы следующий разработчик понимал: это base64url, а не опечатка. То же расширение может стать короче на новых тулчейнах: самые новые бета-SDK теперь включают нативную опцию .base64URLAlphabet, которая делает замену алфавита прямо внутри фреймворка, вместе с соответствующей опцией .omitPaddingCharacter, а open-source Foundation носит те же опции за меткой доступности для более поздних тулчейнов. Пока они не дойдут до вашей минимальной цели развёртывания, четырёхстрочное расширение - переносимый ответ, и по построению оно продолжит работать на каждой платформе.

JSON и API: тот самый base64, которого вы не просили

Этот сюрприз поражает больше всех, кто работает с Codable, поэтому он заслуживает отдельного раздела: стратегия по умолчанию JSONEncoder для свойства Data - уже base64. Если в структуре Codable есть поле Data, кодировщик автоматически упаковывает его стандартным base64, а JSONDecoder автоматически распаковывает его по дороге обратно. Без опций, без конфигурации, без церемоний.

import Foundation

struct Snapshot: Codable {
  let name: String
  let icon: Data
}

let snap = Snapshot(name: "cat", icon: Data("🐱".utf8))
let json = try JSONEncoder().encode(snap)
print(String(decoding: json, as: UTF8.self))
// иконка пересекла канал как «8J+QsQ==»

Свойство icon пересекло канал в виде 8J+QsQ==, потому что это домашний стиль. Альтернативы есть, и две, которые вы действительно встретите, - это .custom, который выдаёт вам данные и кодировщик и позволяет самим решить, каким будет представление, и поновее .deferredToData, который перекладывает решение на сам экземпляр данных. В момент, когда API хочет base64url вместо стандартного, именно в .custom вставляется ваше расширение из предыдущего раздела:

import Foundation

extension Data {
  var base64URLEncoded: String {
    base64EncodedString()
      .replacingOccurrences(of: "+", with: "-")
      .replacingOccurrences(of: "/", with: "_")
      .replacingOccurrences(of: "=", with: "")
  }
}

struct Snapshot: Codable {
  let name: String
  let icon: Data
}

let encoder = JSONEncoder()
encoder.dataEncodingStrategy = .custom { data, enc in
  var container = enc.singleValueContainer()
  try container.encode(data.base64URLEncoded)
}
let json = try encoder.encode(Snapshot(name: "cat", icon: Data("🐱".utf8)))
print(String(decoding: json, as: UTF8.self))
// иконка пересекла канал как «8J-QsQ»

Одно предупреждение, которое отделяет работающую фичу от продакшен-инцидента: в JSON-строке не может быть сырого переноса строки. Если вы перенесёте пелод опцией .lineLength и вставите результат в JSON-документ без экранирования, у вас вообще не получится JSON-значение; получится синтаксическая ошибка с base64-акцентом, и парсер это докажет. Перенесённый вывод принадлежит телам писем и файлам сертификатов. Всё, что живёт внутри JSON, URL или строк запроса, получает простую строку без переносов.

Data URI: изображение в строке

Любимый трюк веба - встраивать байты файла прямо в URL: data:{mime};base64,{payload}. Собирать его в Swift - это чтение, кодирование и склейка строк:

import Foundation

let gif = Data(base64Encoded: "R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")!
let uri = "data:image/gif;base64," + gif.base64EncodedString()
print(uri.hasPrefix("data:image/gif;base64,R0lGODlh")) // true
print(gif.count) // 42

В примере знаменитый прозрачный GIF в 42 байта (непрозрачные помельче тоже существуют, но это тот, который встраивают все) собирается в data URI, который браузер отрендерит без второго запроса. На платформах Apple обратное направление - одна строка: тот же Data, который вы упаковали, подаётся прямо в UIImage(data:) или NSImage(data:). Плата за это - размер, и он складывается: 100-килобайтное изображение превращается в строку из более чем 133 000 символов, ещё до того, как вы добавите префикс data:image/png;base64,. Data URI блистают в иконках, аватарах и крошечных ассетах, но молча раздувают трафик для главных фотографий, так что оставьте их для мелких вещей.

JWT: запечатывание первых двух частей

Кодирующая сторона JSON Web Token - это две печати плюс подпись, а печать - ваше base64url-расширение с отброшенным заполнителем, что ровно то, чего требует формат. Заголовок и пелод - JSON-документы, и обе части получают одинаковое обращение:

import Foundation

extension Data {
  var base64URLEncoded: String {
    base64EncodedString()
      .replacingOccurrences(of: "+", with: "-")
      .replacingOccurrences(of: "/", with: "_")
      .replacingOccurrences(of: "=", with: "")
  }
}

func seal(_ text: String) -> String {
  Data(text.utf8).base64URLEncoded
}

let header = seal(#"{"alg":"HS256","typ":"JWT"}"#)
let claims = seal(#"{"sub":"42","role":"editor"}"#)
print("\(header).\(claims).signature-here")
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsInJvbGUiOiJlZGl0b3IifQ.signature-here

Два напоминания. Третья часть, отделённая точкой, - криптографическая подпись, вычисленная по первым двум, и это единственная часть токена, которая даёт какую-либо гарантию: заголовок и заявления - обычный JSON в пальто, поэтому секреты туда никогда не кладут. И заметьте, как заполнитель исчезает в seal(): JWT-декодеры на другой стороне (включая тот, что в статье-сестре) возвращают его с добиванием по остатку, так что два направления пути встречаются на общей земле.

HTTP-заголовки: Basic и остальные

Старый заголовок Authorization: Basic хочет имя пользователя и пароль, склеенные двоеточием и упакованные стандартным base64, потому что в заголовке + и / безобидны, и вопрос о диалекте просто не возникает:

import Foundation

let credentials = "editor:s3cret"
let header = "Basic " + Data(credentials.utf8).base64EncodedString()
print("Authorization: " + header)
// Authorization: Basic ZWRpdG9yOnMzY3JldA==

Та же громкая сноска, что и везде: упаковка сама по себе даёт нулевую безопасность, и заголовок только настолько безопасен, насколько безопасно HTTPS-соединение, по которому он летит. Современный сородник, Authorization: Bearer, несёт вместо него JWT, поэтому по каналу там идёт рецепт запечатывания из раздела про JWT. Единственное место, где вопрос о диалекте всё-таки возникает в HTTP, - это строка запроса: если ваш API пускает идентификаторы ехать в URL, такой идентификатор должен быть base64url, или хотя бы процентно закодированным стандартным base64, но никогда не сырым стандартным алфавитом, где + остаётся на милость разбора форм и читается как пробел.

Вложения в письмах: контракт на 76 символов

Когда ваше приложение создаёт вложение, которое должно пережить 7-битное прошлое SMTP, действует контракт MIME: base64, перенесённый по 76 символов с CRLF-концами строк, и заголовок Content-Transfer-Encoding: base64, который говорит получателю, чего ждать. Раздел про опции уже показал написание; вот полная форма перенесённого тела:

import Foundation

let attachment = Data((0..<400).map { UInt8(65 + $0 % 26) })
let body = attachment.base64EncodedString(options: [.lineLength76Characters,
  .endLineWithCarriageReturn, .endLineWithLineFeed])
let lines = body.components(separatedBy: "\r\n")
print(lines.count)                     // 8 строк
print(lines.map { $0.count }.max() ?? 0) // 76, самая длинная
print(body.hasSuffix("\r\n"))           // false, последняя строка остаётся без переноса

Счёт за этот диалект - знаменитый: налог алфавита 4/3 плюс перенос каждые 76 символов приводит примерно к 137 процентам исходного размера, и старый инженерный приём почты «умножьте исходник на 1.37 и добавьте около 800 байт заголовков» всё ещё работает, чтобы оценивать размеры вложений на глазок в почтовом клиенте. Это фольклор с правильной арифметикой, и это единственное место в статье, где наценка 33 процента обретает вторую цифру после запятой.

Конфигурация, окружение и базы данных: прячем нижнее подчёркивание

Есть тихий класс работ, где единственная добродетель base64 - то, что его вывод - маленький предсказуемый набор символов: прятать бинарный блоб или структурированное значение туда, где хотят обычный текст. Переменные окружения, которые должны пережить конфигурационный файл shell, столбцы в базе данных, которой varchar приятнее, чем blob, LDAP-файл с его base64-маркером, QR-код, который сканирует буквы надёжнее, чем биты. Паттерн везде один: решите байты, закодируйте, сохраните строку, декодируйте на другом конце.

import Foundation

struct FeatureFlags: Codable {
  var betaToolbar: Bool
  var maxRetries: Int
}

do {
  let flags = FeatureFlags(betaToolbar: true, maxRetries: 5)
  let json = try JSONEncoder().encode(flags)
  let storable = json.base64EncodedString()
  print(storable)
  guard let packed = Data(base64Encoded: storable) else {
    print("decode failed, that is odd")
    exit(1)
  }
  let restored = try JSONDecoder().decode(FeatureFlags.self, from: packed)
  print(restored.betaToolbar, restored.maxRetries)
} catch {
  print(error)
}

Здесь живут две ямы. Первая - двойная обёртка: два интеграционных слоя, которые оба из лучших побуждений кодируют, и в итоге сохраняемое значение - это base64 от base64, а читатель, декодировавший один раз, получает стену букв и решает, что фича сломана. Кодируйте ровно один раз, ровно на одной границе, и скажите об этом в комментарии. Вторая - дрейф диалекта по окружению: если значение когда-нибудь проедет через URL, поле формы или shell, который кривит + и /, сохраняйте base64url-написание, потому что набор символов - и есть весь смысл формата.

Файлы: круговой путь .b64

Работа вида «преврати этот файл в текстовый файл .b64» - это чтение, вызов и запись:

import Foundation

let source = URL(fileURLWithPath: "photos/cat.png")
let archive = URL(fileURLWithPath: "photos/cat.b64")
let bytes = try Data(contentsOf: source)
try Data(bytes.base64EncodedString().utf8).write(to: archive)

// позже, возможно, в другом процессе
let packed = try String(contentsOf: archive, encoding: .utf8)
let restored = Data(base64Encoded:
  packed.trimmingCharacters(in: .whitespacesAndNewlines))
if let restored = restored {
  try restored.write(to: URL(fileURLWithPath: "photos/cat-copy.png"))
} else {
  print("the .b64 file was not base64 after all")
}

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

import Foundation

func streamEncode(_ input: InputStream, output: OutputStream, lineLength: Int = 76) throws {
  input.open()
  output.open()
  defer { input.close(); output.close() }
  var buffer = [UInt8](repeating: 0, count: 65_536)
  var pending = [UInt8]()
  var line = ""
  var lineCount = 0
  func addText(_ text: String) {
    line += text
    while line.count > lineLength {
      if lineCount > 0 { _ = output.write(Array("\r\n".utf8), maxLength: 2) }
      _ = output.write(Array(String(line.prefix(lineLength)).utf8), maxLength: lineLength)
      line = String(line.dropFirst(lineLength))
      lineCount += 1
    }
  }
  func flushGroup(_ group: [UInt8]) {
    addText(Data(group).base64EncodedString())
  }
  while input.hasBytesAvailable {
    let n = input.read(&buffer, maxLength: buffer.count)
    if n < 0 { throw CocoaError(.fileReadUnknown) }
    if n == 0 { break }
    pending.append(contentsOf: buffer[0..<n])
    let groups = pending.count / 3
    if groups > 0 {
      flushGroup(Array(pending[0..<(groups * 3)]))
      pending.removeFirst(groups * 3)
    }
  }
  if !pending.isEmpty {
    flushGroup(pending)
  }
  if !line.isEmpty {
    if lineCount > 0 { _ = output.write(Array("\r\n".utf8), maxLength: 2) }
    _ = output.write(Array(line.utf8), maxLength: line.utf8.count)
  }
}

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

Крупные пелоды и счёт за память

Сделаем арифметику, которая понадобится в следующий раз, когда кто-нибудь спросит «а можно это в base64?». Каждые три входных байта становятся четырьмя выходными символами, поэтому размер умножается на 4/3: 100-килобайтовый файл становится строкой из 133 336 символов, 10-мегабайтовый - строкой из 13 333 336 символов, и так далее. Заполнитель добавляет максимум два символа в самом конце, погрешность округления для всего, что крупнее пары байтов, и единственный побег от налога - пустой вход, где налоговая даёт один бесплатный проезд, а результатом становится пустая строка. Три практических последствия. Первое: бюджетайте до начала: если ваш пелод уже близок к лимиту (комфортная зона URL в примерно 2000 символов, контракт JSON-поля, ширина столбца базы), делите лимит на 1.33 до кодирования, а не после (и на 1.37, если в дело влезает перенос). Второе: пока вы упаковываете, вы одновременно держите исходные байты и упакованную строку, так что рабочий набор примерно в 2.33 раза больше исходного, и поточные функции выше - это шлюз на свободу, когда это число перестаёт быть комфортным. Третье: налог на практике односторонний - вы платите его при упаковке, а ваши байты возвращаются домой, когда кто-то распаковывает, поэтому настоящий вопрос никогда не в «base64 дорог?», а в «требует ли его от меня текстовая дорога, по которой я еду?».

Ошибки, которые кусаются

  • Шаг с кодировкой, который может не удалиться. String.data(using:) может ответить nil (попробуйте .ascii с акцентированным знаком), и принудительная распаковка - классический способ превратить плохой ввод в упавшее приложение. Охраняйте преобразование, а не только base64-вызов, - именно в этом и состоит лёгкая часть.
  • Домашний стиль CRLF. Опция .lineLength без опции конца строки по умолчанию даёт CRLF. Если вашему формату нужны только LF, а вы забыли опцию, ваш вывод повезёт возвраты каретки, которых ему никогда не полагалось.
  • Яма только-CR. .endLineWithCarriageReturn в одиночестве даёт только-CR концы строк в стиле старых Mac. Если вы имели в виду CRLF (а для MIME именно так), передавайте обе опции конца строки.
  • Перенос внутри JSON. Сырой перенос строки внутри JSON-строки - невалидный JSON, точка. Перенесённый base64, вставленный в документ, - синтаксическая ошибка с base64-акцентом. Держите перенесённый вывод в телах писем и файлах сертификатов.
  • BOM-автостопщик. Простое преобразование .utf16 добавляет двухбайтовый BOM, который едет в ваш упакованный вывод и сбивает с толку декодеры, которые его не ждали. Когда нужен UTF-16 без метки, используйте .utf16LittleEndian или .utf16BigEndian.
  • Дрейф диалекта. Стандартный и base64url - разные алфавиты, и об этом RFC сказано письменно. +, уцелевший в строке запроса, становится пробелом; -, добравшийся до мягкого стандартного декодера, удаляется. Выбирайте диалект на границе и держитесь его.
  • Регистр - это буква. Алфавит различает A и a. Копипаст с приведением регистра или рьяный вызов перевода в верхний регистр молча портят данные, потому что обе версии проходят любую проверку алфавита. Base64 чувствителен к регистру так же, как номер паспорта.
  • Правило выравнивания. Поточные кодировщики должны резать чанки по кратным трём числам байтов. Не выровненный чанк меняет вывод, и изменение молчаливое: строка всё равно декодируется, но в неверные данные.
  • Двойная обёртка. Два слоя, которые оба кодируют, порождают base64 от base64. Читатель, декодировавший один раз, видит буквы там, где должны быть байты, и инцидент пишет себя сам.
  • Стена доступности. Новые нативные опции (.base64URLAlphabet, .omitPaddingCharacter) существуют в самых новых бета-SDK и в open-source Foundation за меткой доступности, но не в каждом тулчейне, до которого дотянется ваш CI. Если вы их берёте, охраняйте проверками доступности, чтобы тот же исходник собирался и на старом Xcode, и на Linux. На текущем стабильном тулчейне четырёхстрочное расширение компилируется всюду, где опции не достались.
  • Base64 - не шифрование. Если требование - конфиденциальность, вы выбрали неверный инструмент на целую категорию. Работа base64 - заставлять байты путешествовать, и он делает ровно эту работу, не больше.

Как отправить

  • Кодируйте байты, а не желания. Решите байтовую форму до вызова метода: по умолчанию UTF-8, и явно называйте, когда это не он, и охраняйте шаг data(using:), который может не удалиться, потому что именно там данные реально теряются.
  • По умолчанию без переноса, по договору - с ним. Простой однострочный вывод правилен для JSON, API и большинства баз; тянитесь за опциями переноса 64/76 только тогда, когда принимающий формат требует, и при CRLF оплачивайте обе опции конца строки.
  • Один диалект на границе. Стандартный base64 для пунктов назначения, где преобладает текст, base64url для всего, что коснётся URL или имени файла, никогда не оба в одном документе. Напишите конверсию один раз, назовите честно и переиспользуйте.
  • Бюджетуйте наценку. Умножайте на 4/3 до начала (на 1.37, если в деле перенос), и прогоняйте потоком чанки, выровненные по трём байтам, когда пелод достаточно велик, чтобы рабочий набор стал некомфортным.
  • Не используйте упаковочную ленту как замок. Если требование - секретность, остановитесь на полке base64 и возьмите шифрование.

Короткая история упаковки

Алфавит, которым вы упаковываете, и длины строк, по которым вы переносите, - это окаменелости из четырёх десятилетий споров о том, сколько двоичного может пережить текстовую дорогу, и место Swift в этой истории короткое, но интересное:

  • 1980-е, эпоха одинаковых машин. Первые кодировщики этого семейства существовали, чтобы перемещать файлы по телефонным линиям между системами, которые предполагали, что на другом конце машина такая же, как их. uuencode на UNIX использовал заглавные буквы, цифры и знаки препинания, и его создатели нашли трюк, который экономил вычислительную мощность: алфавит стоит на последовательных ASCII-позициях, так что кодирование было буквально «прибавь 32» без таблиц. BinHex, сородник, родившийся на TRS-80 в 1981 году, перепрыгнул на Apple II, стал форматом классического Macintosh в 1984-м и сделал другую ставку: его 64 символа пропускают 7, O, W, g, o и почти половину строчных.
  • 1987, у алфавита появляется адрес. RFC 989, первая спецификация Privacy-Enhanced Mail, стандартизировал ровно те 64 символа, которые вы печатаете сегодня, переносил вывод по 64 символа в строке и использовал = для заполнения и * для пометки закодированных, но незашифрованных данных. Каждый блок в стиле PEM, который вы когда-либо вставляли в серверную конфигурацию, - потомок этого документа.
  • 1996, либеральная эпоха. MIME (RFC 2045) взял алфавит для почтовых вложений и сдвинул перенос на 76 символов, добавив правило, которое сделало перенос безопасным для генерации: декодеры должны игнорировать переносы строк. Кодировщики научились переносить, декодеры - прощать. Опция на 76 символов в Swift - живой сувенир именно из этого спора.
  • 2003-2006, правила твердеют. RFC 3548 (2003) провозгласил, что заполнитель не следует пропускать (если формат не говорит об обратном), и что декодеры должны отклонять символы вне алфавита; RFC 4648 (октябрь 2006) уладил семейство и добавил URL-безопасный алфавит, явно чтобы длинные идентификаторы могли жить в URL без процентного экранирования каждого спецсимвола. Конвенция «без заполнителя в URL-диалекте» родилась в том же документе, потому что символ заполнителя в URL чаще всего превращается в %3D, что сводит на нет всю идею.
  • 2013-2014, API уже здесь. Класс NSData от Apple уже годами упаковывал base64, и API на основе опций с четырьмя опциями переноса пришло в iOS 7, в 2013 году, до того, как Swift вообще существовало. Когда Swift 1.0 приехал 9 сентября 2014 года, он унаследовал полный кодировщик с четырьмя опциями переноса и 64-буквенный алфавит 1987 года, и характер с тех пор не менялся.
  • 3 декабря 2015, тулчейн покидает здание. Swift в тот день стал open-source, и base64 из Foundation вместе с ним перешёл на Linux, а позже на Windows. «Кодирование base64 в Swift вне машины Apple» - едва ли не десятилетие: очень молодой гость на вечеринке, которая началась в 1987-м.
  • 2023-2026, переписывание и URL-диалект. Переписывание Foundation (проект swift-foundation) перевело Data в чисто Swift-ядро, и в 2025 году питч сообщества добавил нативные опции base64url и отбрасывания заполнителя. На момент написания самые новые бета-SDK и open-source тулчейн поставляют опции кодирования, остальная часть семейства созревает в open-source Foundation за метками доступности, а расширение сообщества пока остаётся переносимым мостом.

Маленькие радости

  • Один мегабайт упаковывается ровно в 1 333 336 base64-символов, налог 4/3 плюс два символа заполнителя, вплоть до цифры. Единственный вход, который полностью избегает налога, - пустой: ничего на входе, ничего на выходе.
  • Кодировщик полон в том смысле, в каком не полон декодер. Он никогда не возвращает nil, никогда не бросает, никогда не отказывает. Единственная неудача во всём конвейере живёт выше, на шаге с кодировкой, - именно поэтому метод кажется намного спокойнее своего сородственника.
  • Закодируйте слово héllo в UTF-8 - и оно станет aMOpbGxv; закодируйте в UTF-16 little-endian - и оно станет aADpAGwAbABvAA==. Одно и то же слово, два разных паспорта, оба действительны, но не взаимозаменяемы.
  • Тестовое слово base64-мира - foobar, и оно упаковывается в Zm9vYmFy. Если вы когда-либо видели base64-пример в дикой природе, есть приличный шанс, что в нём участвовал foobar.
  • Знаменитый прозрачный GIF 1x1 - 42 байта и начинается с магического слова GIF89a, поэтому префикс R0lGODlh встречается в кодовых базах на Земле чаще, чем почти любая другая base64-строка.
  • Ваша структура Codable, вероятно, уже годами шлёт base64, и вы этого не замечали: стратегия по умолчанию JSONEncoder для Data упаковывает стандартным base64, поэтому поле Data пересекает канал в виде строки с заполнителем, а не массива чисел.
  • Заполнитель никогда не превышает двух символов, никогда. Пелод в 1 байт заканчивается ==, пелод в 2 байта - =, а пелод в 3 байта - ничем. Вся грамматика последней группы умещается на ногте.
  • Swift на 27 лет моложе алфавита, которым он упаковывает. Язык вышел в 2014 году; 64 буквы были стандартизированы в 1987-м и с тех пор не менялись.

Вот и вся упаковка-сумка инструментов: один полный метод, один шаг, который может не удалиться и идёт перед ним, четыре опции переноса с домашним стилем CRLF, четырёхстрочное base64url-расширение, правило выравнивания по трём байтам для потоковой обработки и наценка 4/3, которая и есть цена допуска на текстовую дорогу. Кодирование - то место, где вы оплачиваете счёт base64, и теперь вы знаете каждую строку счёта до подписи. В момент, когда вы разворачиваете путь и начинаете открывать то, что упаковали другие, nil снова в деле, вердикты по пробелам и слепое пятно мягкого рычага выходят на сцену. Связанная статья про декодирование ведёт полное представление на этой половине кругового пути, поэтому, когда буквы начнут приезжать, вы уже будете точно знать, как их открыть.

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

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