Base64-Kodierung in Swift: Ein vollständiger Leitfaden
Sie haben etwas, das reisen muss, und der Weg ist nur für Text: eine JSON-API, die rohe Bytes verweigert, ein E-Mail-Kanal, der an seine 7-Bit-Ursprünge erinnert, eine URL, die an allem erstickt, was sie nicht benennen kann, und eine Konfigurationsdatei, die nur die schlichtesten Zeichen annimmt. Willkommen auf der Pack-Seite von Base64, wo Swift Ihre Bytes mit einem einzigen Methodenaufruf in eine freundliche Wand aus Buchstaben verwandelt, mit einem Aufschlag von ungefähr einem zusätzlichen Zeichen pro drei Bytes und einigen Umbruch-Optionen, die existieren, weil zwei verschiedene Jahrzehnte Meinungen über Zeilenlängen hatten.
Die Startseite dieser Website erklärt das Format bereits im Detail (64 druckbare Zeichen, vier davon pro drei Eingabe-Bytes, bis zu zwei =-Zeichen Padding auf der letzten Gruppe), also ist die Format-Vorlesung vorbei, bevor sie beginnt. Zwei Fakten für diesen Artikel: Base64 ist Packen, kein Schloss, und das Packen vergrößert Ihre Daten um etwa 33 Prozent, was jedes Mal wichtig wird, wenn Sie nahe an einer Größen-Grenze sind. In Swift läuft der ganze Job über einen einzigen Typ, Data, und eine einzige total Methode, base64EncodedString(options:). Die einzige wirklich benötigte Fähigkeit ist zu wissen, was in den zwei Schritten um diese Methode herum passiert, denn die Methode selbst schlägt nie fehl. Es sind die Schritte, die es tun.
Eine Methode, null Ausreden
Alles Base64-Bezogene in Swift lebt auf Data aus dem Foundation-Framework, und dort lebt es seit den ersten Releases der Sprache (Apple listet die Methode ab iOS 8.0, macOS 10.10, tvOS 9.0, watchOS 2.0 und visionOS 1.0). Die Pipeline sind immer dieselben drei Schritte: Bringen Sie Ihren Inhalt in ein Data, rufen Sie die Methode auf, versenden Sie den String.
import Foundation
let note = "Pack it, wrap it, ship it."
let packed = Data(note.utf8).base64EncodedString()
print(packed) // UGFjayBpdCwgd3JhcCBpdCwgc2hpcCBpdC4=
Zwei Details in diesen drei Zeilen verdienen einen genaueren Blick. Erstens ist Data(note.utf8) der stille Schritt: Die utf8-View kann per Definition jeden Unicode-Scalar darstellen, daher schlägt sie nie fehl, und deshalb ist sie in den meisten Beispielen der Standard. Die failable Cousine, note.data(using:), kann und wird für manche Kodierungen mit nil antworten, und die ganze "Welche-Bytes"-Entscheidung bekommt unten ihren eigenen Abschnitt, denn dort ist der erste Ort, an dem Ihre Daten verloren gehen können. Zweitens ist die Methode selbst total: Sie antwortet immer, hat keinen Fehlerfall, und die einzige Frage, die sie Ihnen stellt, ist, welchen Zeilenumbruch Sie wollen. Es gibt auch eine Schwester, base64EncodedData(options:), die das verpackte Ergebnis als Data aus ASCII-Bytes statt als String zurückgibt, für Pipelines, in denen die nächste Haltestelle eine binäre API ist und kein Textfeld.
Und weil die Hälfte von Ihnen über "Meine Swift-App braucht eine Base64-Abhängigkeit" hierher kam: Es gibt nichts zu installieren. Base64 ist Teil von Foundation, Foundation ist Teil der Toolchain, und die Toolchain kommt auf jeder Plattform auf demselben Weg an. Auf macOS ist es Xcode oder die Command-Line-Tools; auf Linux und Windows ist es der Installer von swift.org, wo die aktuelle stabile Linie Stand dieses Artikels 6.3.x ist und der Swiftly-Versionsmanager die empfohlene Tür ist; und offizielle Docker-Images decken die Container-Szene ab. Ihr Package.swift bleibt leer, und so soll es sein.
Die erste richtige Entscheidung: Welche Bytes?
Bevor auch nur ein einzelnes Base64-Zeichen erzeugt wird, haben Sie bereits die wichtigste Entscheidung getroffen, denn Base64 packt Bytes, und ein String ist nur ein String, bis Sie seine Byte-Form wählen. UTF-8 ist der vernünftige Standard und die richtige Antwort für fast alles, aber in dem Moment, in dem Ihre Daten aus einem Legacy-System, einem binären Protokoll oder einer Ecke von Unicode kommen, wird die Wahl plötzlich sichtbar:
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==
| Konvertierung | Bytes für "héllo" | Was das Base64 trägt |
|---|---|---|
.utf8 |
6 | aMOpbGxv, die Schreibweise, die moderne APIs erwarten |
.ascii |
schlägt mit nil fehl |
das akzentuierte Zeichen liegt über 0x7F, und ASCII verweigert es |
.utf16 |
12 | die doppelte Größe von UTF-8, plus ein zwei Bytes großes Byte-Order-Mark, das vorne mitfährt |
.utf16LittleEndian |
10 | dasselbe Wort ohne das BOM-Tag: 10 Bytes, immer noch das schwerste der hier aufgeführten BOM-freien Optionen |
Drei Lektionen verstecken sich in diesem Output. Die data(using:)-Form ist failable, und .ascii ist ein Prime-Kandidat, um zu scheitern, also ist Force-Unwrapping der Weg, wie ein völlig guter Satz zu einer abgestürzten App wird. Die schlichte .utf16-Konvertierung hängt ein zwei Bytes großes Byte-Order-Mark vor (FF FE auf einer Little-Endian-Maschine), und dieses BOM reist in Ihr verpacktes Ergebnis und verwirrt jeden Dekodierer, der es nicht erwartet hat. Und die Größen-Mathematik ist unerbittlich: Eine sorglose Zeichensatz-Wahl kostet Sie den Base64-Aufschlag auf das Doppelte an Daten, also ist die Frage nie "Kodiert das hier?" sondern "Was wird das andere Ende erwarten, wenn es auspackt?" Die goldene Regel: Beide Enden der Reise müssen sich auf die Byte-Form einigen, bevor das Base64 beginnt, denn der Dekodierer hat keine Möglichkeit zu raten, was Sie gewählt haben, und er wird nicht fragen.
Umbruch: Zwei Gewohnheiten, ein Parameter
Die Optionen der Methode handeln alle von Zeilenbrüchen, und sie alle existieren, weil zwei Formate des 20. Jahrhunderts sich nicht einigen konnten, wie lang eine Zeile aus Buchstaben sein sollte. MIME, der E-Mail-Standard von 1996, bricht Base64 bei 76 Zeichen mit CRLF-Zeilenenden um. PEM, die Privacy-Enhanced-Mail-Linie aus 1987, bricht bei 64 Zeichen um, und das ist die Form, die Sie in Zertifikaten und Schlüsseln finden, die -----BEGIN CERTIFICATE------Blöcke, die Ihre Server in einem Konfigurationsverzeichnis aufbewahren.
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 Zeichen auf einer Zeile
print(pemStyle.components(separatedBy: "\n").count) // 7 Zeilen mit höchstens 64
print(mimeStyle.components(separatedBy: "\r\n").count) // 6 Zeilen mit höchstens 76
| Option | Aufgabe | Vorsicht |
|---|---|---|
.lineLength64Characters |
bricht die Zeile nach 64 Zeichen um, die PEM-Gewohnheit | das Zeilenende ist CRLF, es sei denn, Sie sagen etwas anderes |
.lineLength76Characters |
bricht die Zeile nach 76 Zeichen um, die MIME-Gewohnheit | derselbe CRLF-Standard |
.endLineWithCarriageReturn |
nimmt einen Wagenrücklauf in das Zeilenende auf | allein ist das nur CR, im Stil alter Macs, und selten das, was Sie wollen |
.endLineWithLineFeed |
nimmt einen Zeilenvorschub in das Zeilenende auf | geben Sie beide Optionen, wenn Sie CRLF meinen |
Und jetzt der Standard, der die Leute überrascht: Fordern Sie eine .lineLength-Option an, ohne ein Zeilenende zu wählen, und das Zeilenende, das Sie erhalten, ist CRLF, das volle Paar aus Wagenrücklauf plus Zeilenvorschub. Die Methode hat einen Haus-Stil, und ihr Haus-Stil ist 1996. Sie wollen nur LF? Zahlen Sie explizit mit .endLineWithLineFeed und nichts anderem. Noch eine Hausregel zur Dokumentation: Die letzte Zeile bekommt niemals ein nachfolgendes Zeilenende. Ein umgebrochenes Ergebnis endet mit seinem letzten Datenzeichen oder seinen =-Pads, egal welche Optionen Sie gewählt haben, also können Sie konkatenieren und einfügen, ohne eine verwaiste Leerzeile am Ende. Und ganz ohne Optionen ist der Output eine einzige ununterbrochene Zeile, was die richtige Form für JSON-Body, URLs und API-Payloads ist: die Arbeit, die eine moderne Swift-App tatsächlich am häufigsten macht.
Base64url: Ein String, der reisen kann
Das Standard-Alphabet ist ein guter Bürger des JSON und ein schrecklicher Bürger einer URL. Im Query-String wird + beim Form-Parsing als Leerzeichen gelesen, / ist ein Pfadtrennzeichen, und = trennt Schlüssel von Werten, und deshalb macht es das Percent-Encoding des Standard-Alphabets länger und hässlicher statt kürzer. Abschnitt 5 von RFC 4648 existiert, um genau das zu reparieren: das "URL- und Dateinamen-sichere Alphabet", bei dem aus + ein - wird, aus / ein _, und das =-Padding wird normalerweise weggelassen, weil ein Pad in einer URL typischerweise zu %3D wird, was den Zweck zunichtemacht. Der RFC fügt eine Warnung hinzu, die man einrahmen sollte: Diese Kodierung "sollte nicht als identisch mit der Base64-Kodierung betrachtet werden". YouTube-Video-IDs, JWTs und die meisten modernen API-Identifikatoren sprechen es, also rechnen Sie damit, es zu verwenden.
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
Blicken Sie genau auf diesen Output: Dieser besondere Payload hat zufällig kein + oder / erzeugt, also unterscheiden sich die beiden Schreibweisen nur durch das weggelassene Padding. Ändern Sie ein Byte, und sie werden sich im Alphabet voneinander unterscheiden, was der ganze Punkt ist. Zwei Einsatzregeln. Wählen Sie den Dialekt einmal, an der Grenze, wo Ihre Daten auf die Außenwelt treffen, und mischen Sie niemals Alphabete innerhalb desselben Dokuments: Ein Standard-Dekodierer, der Base64url empfängt (oder umgekehrt), wird die Eingabe entweder verwerfen oder, in nachsichtigen Modi, die fremden Zeichen löschen und Ihnen falsche Bytes reichen. Und nennen Sie Ihren Helfer ehrlich, damit der nächste Entwickler weiß, dass der String Base64url ist und kein Tippfehler. Dieselbe Extension kann auf neueren Toolchains kürzer werden: Die neuesten SDK-Betas enthalten jetzt eine native .base64URLAlphabet-Option, die den Alphabet-Tausch innerhalb des Frameworks erledigt, mit einer passenden .omitPaddingCharacter-Option, und open-source Foundation trägt dieselben Optionen hinter einem Verfügbarkeits-Marker für spätere Toolchains. Bis diese Ihre Mindest-Deployment-Zielversion erreichen, ist die vierzeilige Extension die portable Antwort, und sie funktioniert per Konstruktion auf jeder Plattform weiter.
JSON und APIs: Das Base64, das Sie nie verlangt haben
Das überrascht die meisten Menschen, die mit Codable arbeiten, und verdient deshalb seinen eigenen Abschnitt: Die Standard-Strategie von JSONEncoder für eine Data-Eigenschaft ist schon Base64. Wenn ein Codable-Struct ein Data-Feld hat, packt der Encoder es automatisch mit Standard-Base64, und JSONDecoder packt es auf dem Rückweg automatisch wieder aus. Keine Option, keine Konfiguration, keine Zeremonie.
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))
// das Icon ist als "8J+QsQ==" über die Leitung gegangen
Die icon-Eigenschaft ist als 8J+QsQ== über die Leitung gegangen, denn das ist der Haus-Stil. Es gibt Alternativen, und die beiden, auf die Sie tatsächlich stoßen werden, sind .custom, der Ihnen die Daten und einen Encoder reicht und Ihnen die Repräsentation überlässt, und das neuere .deferredToData, das an die Daten-Instanz selbst delegiert. In dem Moment, in dem eine API Base64url statt Standard will, ist .custom der Ort, an dem Ihre Extension aus dem vorherigen Abschnitt ansetzt:
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))
// das Icon ist als "8J-QsQ" über die Leitung gegangen
Die eine Warnung, die ein funktionierendes Feature von einem Produktions-Notfall trennt: Ein JSON-String darf keinen rohen Zeilenumbruch enthalten. Wenn Sie einen Payload mit einer .lineLength-Option umbrechen und das Ergebnis ohne Escaping in ein JSON-Dokument interpolieren, haben Sie überhaupt keinen JSON-Wert gebaut; Sie haben einen Syntaxfehler mit Base64-Akzent gebaut, und der Parser wird es beweisen. Umgebrochener Output gehört in E-Mail-Body und Zertifikatsdateien. Alles, was in JSON, URLs oder Query-Strings lebt, bekommt den schlichten, nicht umgebrochenen String.
Data URIs: Das Bild in einem String
Der Lieblingstrick des Webs ist, die Bytes einer Datei direkt in eine URL einzubetten: data:{mime};base64,{payload}. Eines davon in Swift zu bauen ist ein Lesen, ein Kodieren und eine String-Konkatenation:
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
Das Beispiel baut die berühmte transparente 42-Byte-GIF (es gibt kleinere, nicht transparente, aber dies ist die, die alle einbetten) in eine Data URI, die ein Browser ohne eine zweite Anfrage rendert. Auf Apple-Plattformen ist die umgekehrte Richtung ein Einzeiler: Dasselbe Data, das Sie gepackt haben, fließt direkt in UIImage(data:) oder NSImage(data:). Der Trade-off ist die Größe, und sie verzinst sich: Ein 100-Kilobyte-Bild wird zu einem String von über 133.000 Zeichen, noch bevor Sie das data:image/png;base64,-Präfix hinzufügen. Data URIs glänzen bei Icons, Avataren und winzigen Assets, und sie lassen die Bandbreite still und leise anschwellen, wenn Hero-Fotos im Spiel sind, also behalten Sie sie für die kleinen Dinge.
JWTs: Die ersten beiden Teile versiegeln
Die Kodierungsseite eines JSON Web Tokens ist zwei Versiegelungen plus eine Signatur, und die Versiegelung ist Ihre Base64url-Extension mit weggelassenem Padding, genau das, was das Format verlangt. Der Header und der Payload sind JSON-Dokumente, und beide Teile bekommen dieselbe Behandlung:
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
Zwei Erinnerungen. Der dritte, durch Punkte getrennte Teil ist eine kryptografische Signatur, berechnet über den ersten beiden, und er ist der einzige Teil des Tokens, der irgendeine Garantie bietet: Der Header und die Claims sind schlichtes JSON in einem Trenchcoat, also gehören niemals Geheimnisse hinein. Und achten Sie darauf, wie das Padding in seal() verschwindet: JWT-Dekodierer auf der anderen Seite (einschließlich des in dem Schwesteraufsatz) setzen es mit einem Modulo-Auffüllen wieder her, sodass sich die beiden Richtungen der Reise auf gemeinsamem Boden treffen.
HTTP-Header: Basic und der Rest
Der alte Authorization: Basic-Header will einen Benutzernamen und ein Passwort, verbunden durch einen Doppelpunkt, verpackt mit Standard-Base64, denn in einem Header sind + und / harmlos, und die Dialekt-Frage stellt sich nicht:
import Foundation
let credentials = "editor:s3cret"
let header = "Basic " + Data(credentials.utf8).base64EncodedString()
print("Authorization: " + header)
// Authorization: Basic ZWRpdG9yOnMzY3JldA==
Dieselbe laute Fußnote wie überall: Das Packen bietet von selbst null Sicherheit, und der Header ist nur so sicher wie die HTTPS-Verbindung, die ihn trägt. Der moderne Cousin, Authorization: Bearer, trägt stattdessen ein JWT, also ist das Versiegelungsrezept aus dem JWT-Abschnitt das, was dort auf der Leitung geht. Der eine Ort, an dem sich die Dialekt-Frage in HTTP stellt, ist der Query-String: Wenn Ihre API einem Identifikator erlaubt, in einer URL mitzufahren, sollte dieser Identifikator Base64url sein, oder mindestens percent-kodiertes Standard-Base64, niemals das rohe Standard-Alphabet mit seinem +, das als Leerzeichen gelesen wird.
E-Mail-Anhänge: Der 76-Zeichen-Vertrag
Wenn Ihre App einen Anhang erzeugt, der die 7-Bit-Ursprünge von SMTP überleben muss, ist der Vertrag der von MIME: Base64, umgebrochen bei 76 Zeichen mit CRLF-Zeilenenden, und ein Content-Transfer-Encoding: base64-Header, der dem Empfänger sagt, was er erwarten darf. Der Options-Abschnitt hat die Schreibweise bereits gezeigt; hier ist die vollständige Form eines umgebrochenen Body:
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 Zeilen
print(lines.map { $0.count }.max() ?? 0) // 76, die längste
print(body.hasSuffix("\r\n")) // false, die letzte Zeile bleibt nackt
Die Größen-Rechnung für diesen Dialekt ist die berühmte: Die 4/3-Alphabet-Steuer plus ein Zeilenumbruch alle 76 Zeichen landen bei ungefähr 137 Prozent des Originals, und der alte Mail-Ingenieurs-Trick "das Original mal 1,37 nehmen und grob 800 Bytes Header dazurechnen" funktioniert immer noch für das Schätzen von Anhangsgrößen in einem Mail-Client. Es ist Folklore mit korrekter Arithmetik, und es ist der eine Ort in diesem Artikel, an dem der 33-Prozent-Aufschlag eine zweite Dezimalstelle kriegt.
Config, Environment und Datenbanken: Den Unterstrich verstecken
Es gibt eine stille Klasse von Jobs, bei denen die einzige Tugend von Base64 ist, dass sein Output ein kleiner, vorhersehbarer Zeichensatz ist: einen binären Blob oder einen strukturierten Wert an einem Ort unterbringen, der schlichten Text will. Umgebungsvariablen, die eine Shell-Config-Datei überleben müssen, Spalten in einer Datenbank, die mit varchar glücklicher ist als mit blob, eine LDAP-Datei mit ihrem Base64-Marker, ein QR-Code, der Buchstaben zuverlässiger scannt als Bits. Das Muster ist überall dasselbe: die Bytes entscheiden, kodieren, den String speichern, am anderen Ende dekodieren.
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)
}
Zwei Fallgruben leben hier. Die erste ist der doppelte Wrap: zwei Integrations-Schichten, die beide "hilfreich" kodieren, sodass der Wert, den Sie speichern, Base64 von Base64 ist, und der Leser, der einmal dekodiert, bekommt eine Wand aus Buchstaben und denkt, das Feature sei kaputt. Kodieren Sie genau einmal, an genau einer Grenze, und sagen Sie das in einem Kommentar. Die zweite ist Dialekt-Drift durch die Umgebung: Wenn der Wert jemals durch eine URL, ein Formularfeld oder eine Shell reisen wird, die + und / verunstaltet, speichern Sie stattdessen die Base64url-Schreibweise, denn der Zeichensatz ist der ganze Punkt des Formats.
Dateien: Der .b64-Roundtrip
Der Job "Diese Datei in eine .b64-Textdatei verwandeln" ist ein Lesen, ein Aufruf und ein Schreiben:
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)
// später, möglicherweise in einem anderen Prozess
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")
}
Das trimmingCharacters auf der Rückreise ist da, weil das, was die Datei geschrieben hat, ein Zeilenende hinzugefügt haben kann, und der strenge Dekodierer behandelt einen nachfolgenden Zeilenumbruch als Urteil von nil. Dieser Roundtrip kommt Byte für Byte zurück, und das sollten Sie das erste Mal verifizieren, wenn Sie ihn ausliefern. Für Dateien, die groß genug sind, um den Speicherbedarf interessant zu machen, kodieren Sie nicht den ganzen Buffer auf einmal. Base64 hat eine wunderbare Eigenschaft, die Streaming exakt macht: Jedes drei Eingabe-Bytes erzeugen vier unabhängige Ausgabe-Zeichen, solange jeder Chunk, den Sie kodieren, ein Vielfaches von drei Bytes ist, ist der konkatenierte Output identisch mit dem, als würde man die ganze Datei in einem Rutsch kodieren. Brechen Sie die Ausrichtung, und der Output ändert sich, denn eine Chunk-Grenze teilt eine Drei-Byte-Gruppe mitten im Stream:
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)
}
}
Der maximale Speicherbedarf ist ein Lese-Buffer plus die aktuelle Zeile, egal wie groß die Datei, und der umgebrochene Output passt exakt zur Einmal-.lineLength76Characters-Schreibweise. Dieselbe Drei-Byte-Vielfach-Regel, mit vertauschten Rollen, ist die, auf die der Streaming-Dekodierer im Schwesteraufsatz setzt, also teilen sich die beiden Seiten der Reise eine arithmetische Wahrheit.
Große Payloads und die Speicher-Rechnung
Machen wir die Arithmetik, die Sie brauchen, wenn das nächste Mal jemand fragt "Können wir das base64 machen?" Jedes drei Eingabe-Bytes werden zu vier Ausgabe-Zeichen, also multipliziert sich die Größe mit 4/3: Eine 100-Kilobyte-Datei wird zu einem 133.336-Zeichen-String, eine 10-Megabyte-Datei zu 13.333.336 Zeichen, und so weiter. Padding fügt am allerletzten Ende höchstens zwei Zeichen hinzu, ein Rundungsfehler bei allem Größerem als ein paar Bytes, und leere Eingabe ist die einzige Befreiung, wo das Finanzamt eine einzige freie Passage gewährt und das Ergebnis der leere String ist. Drei praktische Konsequenzen. Erstens: Planen Sie, bevor Sie starten: Wenn Ihr Payload schon nahe an einer Grenze ist (etwa die 2.000-Zeichen-Komfortzone einer URL, der Vertrag eines JSON-Felds, die Breite einer Datenbank-Spalte), dividieren Sie die Grenze durch 1,33, bevor Sie kodieren, nicht danach (und durch 1,37, wenn Umbruch im Spiel ist). Zweitens: Während Sie packen, halten Sie die originalen Bytes und den verpackten String gleichzeitig, also ist der Arbeits-Satz etwa 2,33 Mal das Original, und die Streaming-Funktionen oben sind der Notausgang, wenn diese Zahl aufhört, komfortabel zu sein. Drittens: Die Steuer ist in der Praxis einseitig: Sie zahlen sie, wenn Sie packen, und Ihre Bytes kommen heim, wenn jemand auspackt, also ist die echte Frage nie "Ist Base64 teuer?" sondern "Verlangt der nur-für-Text-Weg, auf dem ich bin, ihn?".
Die Fehler, die beißen
- Der failable Zeichensatz-Schritt.
String.data(using:)kann mitnilantworten (probieren Sie.asciimit einem akzentuierten Zeichen), und Force-Unwrapping davon ist das klassische Upgrade einer schlechten Eingabe zu einer abgestürzten App. Schützen Sie die Konvertierung, nicht nur den Base64-Aufruf, der der einfache Teil ist. - Der CRLF-Haus-Stil. Eine
.lineLength-Option ohne Zeilenende-Option erzeugt standardmäßig CRLF. Wenn Ihr Format nur LF will und Sie die Option vergessen haben, trägt Ihr Output Wagenrückläufe, die es nie haben sollte. - Die nur-CR-Falle.
.endLineWithCarriageReturnallein erzeugt nur-CR-Zeilenenden im Stil alter Macs. Wenn Sie CRLF meinten (und bei MIME tun Sie das), geben Sie beide Zeilenende-Optionen. - Umbruch innerhalb von JSON. Ein roher Zeilenumbruch innerhalb eines JSON-Strings ist ungültiges JSON, Punkt. Umgebrochenes Base64, das in ein Dokument interpoliert wird, ist ein Syntaxfehler mit Base64-Akzent. Halten Sie umgebrochenen Output in E-Mail-Body und Zertifikatsdateien.
- Der BOM-Mitfahrer. Die schlichte
.utf16-Konvertierung hängt ein zwei-Byte-BOM vor, das in Ihren verpackten Output reist und Dekodierer verwirrt, die es nicht erwartet haben. Verwenden Sie.utf16LittleEndianoder.utf16BigEndian, wenn Sie UTF-16 ohne das Tag brauchen. - Dialekt-Drift. Standard und Base64url sind verschiedene Alphabete, und der RFC sagt es schriftlich. Ein
+, das in einen Query-String überlebt, wird zu einem Leerzeichen; ein-, das einen nachsichtigen Standard-Dekodierer erreicht, wird gelöscht. Wählen Sie den Dialekt an der Grenze und behalten Sie ihn. - Groß- und Kleinschreibung zählt wie ein Buchstabe. Das Alphabet unterscheidet
Avona. Ein Copy-Paste, das Groß- und Kleinschreibung glättet, oder ein enthusiastischer Uppercase-Aufruf korruptiert die Daten still und leise, denn beide Versionen bestehen jede Alphabet-Prüfung. Base64 ist groß- und kleinschreibungssensibel so wie eine Passnummer. - Die Ausrichtungs-Regel. Streaming-Encoder müssen Chunks an Vielfachen von drei Bytes schneiden. Ein falsch ausgerichteter Chunk ändert den Output, und die Änderung ist still: Der String dekodiert trotzdem, zu den falschen Daten.
- Der doppelte Wrap. Zwei Schichten, die beide kodieren, erzeugen Base64 von Base64. Der Leser, der einmal dekodiert, sieht Buchstaben, wo Bytes sein sollten, und der Unfall schreibt sich selbst.
- Die Verfügbarkeits-Mauer. Die neuen nativen Optionen (
.base64URLAlphabet,.omitPaddingCharacter) existieren auf den neuesten SDK-Betas und in open-source Foundation hinter einem Verfügbarkeits-Marker, aber nicht auf jeder Toolchain, die Ihre CI berühren wird. Wenn Sie sie übernehmen, schützen Sie sie mit Verfügbarkeits-Checks, damit derselbe Quellcode auf älteren Xcode-Versionen und auf Linux gebaut wird. Auf der aktuellen stabilen Toolchain kompiliert die vierzeilige Extension überall, wo die Optionen nicht existieren. - Base64 ist keine Verschlüsselung. Wenn die Anforderung Vertraulichkeit ist, haben Sie das falsche Werkzeug um eine ganze Kategorie gewählt. Base64s Job ist, Bytes reisen zu lassen, und es macht genau diesen Job, nichts mehr.
Wie man es ausliefert
- Kodieren Sie Bytes, nicht Wünsche. Entscheiden Sie die Byte-Form, bevor Sie die Methode aufrufen, UTF-8 als Standard und benennen Sie sie explizit, wenn es nicht so ist, und schützen Sie den failable
data(using:)-Schritt, denn dort gehen Daten tatsächlich verloren. - Standardmäßig nicht umgebrochen, umgebrochen nach Vertrag. Der schlichte Single-Line-Output ist korrekt für JSON, APIs und die meisten Datenbanken; greifen Sie zu den 64/76-Umbruch-Optionen nur, wenn das empfangende Format sie verlangt, und zahlen Sie für beide Zeilenende-Optionen, wenn Sie CRLF meinen.
- Ein Dialekt pro Grenze. Standard-Base64 für textlastige Ziele, Base64url für alles, was eine URL oder einen Dateinamen berühren wird, niemals die beiden im selben Dokument. Schreiben Sie die Konvertierung einmal, benennen Sie sie ehrlich und verwenden Sie sie wieder.
- Planen Sie den Aufschlag ein. Multiplizieren Sie mit 4/3, bevor Sie starten (mit 1,37, wenn Umbruch im Spiel ist), und streamen Sie mit 3-Byte-ausgerichteten Chunks, wenn der Payload groß genug ist, um den Arbeits-Satz unangenehm zu machen.
- Verwenden Sie kein Packband als Schloss. Wenn die Anforderung Geheimhaltung ist, bleiben Sie am Base64-Regal stehen und nehmen Sie stattdessen Verschlüsselung.
Eine kurze Geschichte des Packens
Das Alphabet, mit dem Sie packen, und die Zeilenlängen, zu denen Sie umbrechen, sind Fossilien aus vier Jahrzehnten von Streitigkeiten darüber, wie viel Binäres eine nur-für-Text-Straße überleben kann, und Swifts Position in dieser Geschichte ist kurz, aber interessant:
- 1980er, die Ära der gleichen Maschine. Die ersten Encoder dieser Familie existierten, um Dateien über Dial-Up zwischen Systemen zu bewegen, die annahmen, das andere Ende sei eine Maschine wie ihre. uuencode auf UNIX verwendete Großbuchstaben, Ziffern und Interpunktion, und seine Designer fanden einen Trick, der Rechenleistung sparte: Das Alphabet liegt auf aufeinanderfolgenden ASCII-Positionen, also war Kodieren buchstäblich "32 addieren", ohne Lookup-Tabelle. BinHex, der Cousin, der 1981 auf dem TRS-80 geboren wurde, hüpfte auf den Apple II, wurde 1984 das Format des klassischen Macintosh und setzte auf eine andere Wette: Seine 64 Zeichen lassen
7,O,W,g,ound fast die Hälfte der Kleinbuchstaben aus. - 1987, das Alphabet bekommt eine Adresse. RFC 989, die erste Privacy-Enhanced-Mail-Spezifikation, standardisierte die exakten 64 Zeichen, die Sie heute tippen, brach Output bei 64 Zeichen pro Zeile um und verwendete
=für Padding und*, um kodiertes, aber unverschlüsseltes Daten zu markieren. Jeder PEM-artige Block, den Sie jemals in eine Server-Config gepackt haben, ist ein Nachkomme dieses Dokuments. - 1996, die liberale Ära. MIME (RFC 2045) nahm das Alphabet für E-Mail-Anhänge, verschob den Umbruch auf 76 Zeichen und fügte die Regel hinzu, die Umbruch sicher erzeugbar machte: Dekodierer sollten die Zeilenumbrüche ignorieren. Encoder lernten, umzubrechen; Dekodierer lernten, zu verzeihen. Swifts 76-Zeichen-Option ist ein lebendes Andenken an genau diese Debatte.
- 2003 bis 2006, die Regeln härten aus. RFC 3548 (2003) erklärte, dass Padding nicht übersprungen werden darf (es sei denn, ein Format sagt das Gegenteil) und dass Dekodierer Zeichen außerhalb des Alphabets verwerfen müssen; RFC 4648 (Oktober 2006) regelte die Familie und fügte das URL-sichere Alphabet hinzu, ausdrücklich damit lange Identifikatoren in URLs leben können, ohne jedes Sonderzeichen zu percent-encoden. Die Konvention "kein Padding im URL-Dialekt" wurde im selben Dokument geboren, weil ein Padding-Zeichen in einer URL typischerweise zu
%3Dwird, was den Zweck zunichtemacht. - 2013 bis 2014, die API ist schon da. Apples
NSData-Klasse hatte seit Jahren Base64 gepackt, und die optionenbasierte API mit den vier Umbruch-Optionen kam in iOS 7, im Jahr 2013, bevor Swift überhaupt existierte. Als Swift 1.0 am 9. September 2014 ankam, erbte es einen total Encoder mit vier Umbruch-Optionen und ein 64-Buchstaben-Alphabet aus 1987, und die Persönlichkeit hat sich seitdem nicht geändert. - 3. Dezember 2015, die Toolchain verlässt das Gebäude. Swift wurde an diesem Tag open-sourced, und Foundations Base64 überquerte mit ihm nach Linux und später nach Windows. "Base64-Kodierung in Swift abseits einer Apple-Maschine" ist knappe zehn Jahre alt: ein sehr junger Gast auf einer Party, die 1987 begann.
- 2023 bis 2026, der Rewrite und der URL-Dialekt. Der Foundation-Rewrite (das swift-foundation-Projekt) verlegte
Datain einen reinen Swift-Kern, und 2025 fügte ein Community-Pitch native Optionen für Base64url und für das Weglassen des Paddings hinzu. Stand dieses Artikels liefern die neuesten SDK-Betas und die Open-Source-Toolchain die Kodierungs-Optionen aus, der Rest der Familie reift in open-source Foundation hinter Verfügbarkeits-Markern, und die Community-Extension bleibt bis dahin die portable Brücke.
Kleine Freuden
- Ein Megabyte packt zu exakt 1.333.336 Base64-Zeichen, die 4/3-Steuer plus zwei Padding-Zeichen, auf die Ziffer genau. Die einzige Eingabe, die der Steuer ganz entkommt, ist die leere: nichts rein, nichts raus.
- Der Encoder ist total in einer Hinsicht, in der der Dekodierer es nicht ist. Er liefert nie nil, wirft nie, verweigert nie. Der einzige Fehler in der ganzen Pipeline lebt upstream, im Zeichensatz-Schritt, und deshalb fühlt sich die Methode viel ruhiger an als ihre Cousine.
- Kodieren Sie das Wort
hélloin UTF-8, und es wird zuaMOpbGxv; kodieren Sie es in UTF-16 little-endian, und es wird zuaADpAGwAbABvAA==. Dasselbe Wort, zwei verschiedene Pässe, beide gültig, keiner austauschbar. - Das Testwort der Base64-Welt ist
foobar, und es packt zuZm9vYmFy. Wenn Sie schon einmal ein Base64-Beispiel in der Wildnis gesehen haben, ist es gut möglich, dass foobar beteiligt war. - Die berühmte 1x1-transparente GIF ist 42 Bytes und beginnt mit dem Magic-Word
GIF89a, und deshalb erscheint das PräfixR0lGODlhauf der Erde in mehr Codebasen als fast jeder andere Base64-String. - Ihr
Codable-Struct hat wahrscheinlich seit Jahren Base64 gesendet, ohne dass Sie es bemerkt haben: Die Standard-Data-Strategie vonJSONEncoderpackt mit Standard-Base64, und deshalb überquert einData-Feld die Leitung als gepaddeter String statt als Zahl-Array. - Padding übersteigt niemals zwei Zeichen, jemals. Ein 1-Byte-Payload endet in
==, ein 2-Byte-Payload endet in=, und ein 3-Byte-Payload endet in nichts. Die gesamte Grammatik der letzten Gruppe passt auf einen Fingernagel. - Swift ist 27 Jahre jünger als das Alphabet, mit dem es packt. Die Sprache erschien 2014; die 64 Buchstaben wurden 1987 standardisiert und haben sich seither nicht geändert.
Das ist die komplette Pack-Werkzeugkiste: eine total Methode, ein failable Schritt, der vor ihr kommt, vier Umbruch-Optionen mit CRLF-Haus-Stil, eine vierzeilige Base64url-Extension, eine 3-Byte-Ausrichtungs-Regel für Streaming und ein 4/3-Aufschlag, der der Eintrittspreis für die nur-für-Text-Straße ist. Kodieren ist der Ort, an dem Sie die Rechnung von Base64 bezahlen, und jetzt kennen Sie jeden Posten, bevor Sie unterschreiben. In dem Moment, in dem Sie die Reise umdrehen und anfangen, zu öffnen, was andere gepackt haben, nehmen die nil-Rückkehr, die Weitespace-Urteile und der blinde Fleck des nachsichtigen Reglers die Bühne ein. Der verwandte Dekodierungs-Artikel spielt die komplette Show auf dieser Hälfte des Roundtrips, also werden Sie, wenn die Buchstaben anfangen anzukommen, schon genau wissen, wie man sie öffnet.
Zuletzt aktualisiert: 2026-09-08
Verwandter Artikel: Base64-Dekodierung in Swift: Ein vollständiger Leitfaden