Base64-codering in Swift: een complete gids
Je hebt iets dat moet reizen, en de weg accepteert alleen tekst: een JSON-API die rauwe bytes weigert, een e-mailkanaal dat zijn 7-bit-herkomst nog herinnert, een URL die verstikt aan alles wat hij niet kan benoemen, een configuratiebestand dat alleen de gewoonste tekens accepteert. Welkom aan de inpakkant van base64, waar Swift je bytes met één methodeaanroep omtovert naar een vriendelijke muur van letters, een toeslag van ruwweg één extra teken per drie bytes, en een paar omwikkelopties die bestaan omdat twee verschillende decennia meningen hadden over regellengtes.
De startpagina van deze site legt het formaat al in detail uit (64 weergavebare tekens, vier daarvan per drie invoerbytes, hooguit twee =-tekens als padding op de laatste groep), dus de formatles is voorbij vóórdat ze begint. Twee feiten om mee te nemen in dit artikel: base64 is inpakken, niet vergrendelen, en het inpakken vergroot je data met ongeveer 33 procent, wat ertoe doet elke keer dat je dicht bij een groottelimiet zit. In Swift loopt het hele werk via één type, Data, en één totale methode, base64EncodedString(options:). De enige echte vaardigheid die vereist is, is weten wat er gebeurt in de twee stappen rondom die methode, want de methode zelf faalt nooit. Het zijn de stappen die dat doen.
Eén methode, nul smoesjes
Alles wat met base64 te maken heeft in Swift leeft op Data uit het Foundation-framework, en het leeft daar sinds de eerste releases van de taal (Apple noemt de methode vanaf iOS 8.0, macOS 10.10, tvOS 9.0, watchOS 2.0 en visionOS 1.0). De pipeline is altijd dezelfde drie stappen: breng je inhoud in een Data, roep de methode aan, verzend de tekenreeks.
import Foundation
let note = "Pack it, wrap it, ship it."
let packed = Data(note.utf8).base64EncodedString()
print(packed) // UGFjayBpdCwgd3JhcCBpdCwgc2hpcCBpdC4=
Twee details in die drie regels verdienen een nadere blik. Ten eerste is Data(note.utf8) de stille stap: de utf8-weergave kan per definitie elke Unicode-scalar vertegenwoordigen, dus faalt hij nooit, en daarom is hij de standaard in de meeste voorbeelden. De zuster die kan falen, note.data(using:), kan en zal bij sommige coderingen nil antwoorden, en die hele "welke bytes"-beslissing krijgt zijn eigen sectie hieronder, want het is de eerste plek waar je data weg kan zijn. Ten tweede is de methode zelf total: hij antwoordt altijd, hij heeft geen foutgeval, en de enige vraag die hij je stelt is welke regelomwikkeling je wilt. Er is ook een zuster, base64EncodedData(options:), die het ingepakte resultaat als Data van ASCII-bytes teruggeeft in plaats van als tekenreeks, voor pipelines waar de volgende halte een binaire API is in plaats van een tekstveld.
En omdat de helft van jullie is aangekomen via "mijn Swift-app heeft een base64-afhankelijkheid nodig": er is niets om te installeren. Base64 is onderdeel van Foundation, Foundation is onderdeel van de toolchain, en de toolchain arriveert op elk platform op dezelfde manier. Op macOS is dat Xcode of de command line tools; op Linux en Windows is dat de installer van swift.org, waar de huidige stabiele lijn op het moment van schrijven 6.3.x is en de Swiftly-versiebeheerder de aanbevolen voordeur is; en officiële Docker-images dekken de containermensen. Je Package.swift blijft leeg, en zo hoort dat.
De eerste echte beslissing: welke bytes?
Voordat er ook maar één base64-teken is geproduceerd, heb je al de beslissing genomen die het meest ertoe doet, want base64 pakt bytes in, en een tekenreeks is pas een tekenreeks totdat je de bytevorm ervan kiest. UTF-8 is de verstandige standaard en het juiste antwoord voor bijna alles, maar op het moment dat je data afkomstig is uit een legacysysteem, een binair protocol of een hoekje van Unicode, wordt de keuze niet langer onzichtbaar:
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==
| Omzetting | Bytes voor "héllo" | Wat de base64 draagt |
|---|---|---|
.utf8 |
6 | aMOpbGxv, de spelling die moderne APIs verwachten |
.ascii |
faalt met nil |
het accentteken zit boven 0x7F en ASCII wijst het af |
.utf16 |
12 | het dubbele van de UTF-8-grootte, plus een twee-byte byte-order mark die aan de voorkant meereist |
.utf16LittleEndian |
10 | hetzelfde woord zonder het BOM-label: 10 bytes, nog steeds het zwaarst van de BOM-vrije opties die hier staan |
Drie lessen verstoppen zich in die output. De data(using:)-vorm kan falen en .ascii is de eerste kandidaat om te falen, dus force-unwrappen is de manier waarop een perfect goede frase een gecrashte app wordt. De gewone .utf16-omzetting voegt vooraan een twee-byte byte-order mark toe (FF FE op een little-endian-machine), en die BOM reist mee naar je ingepakte output en verwarrt elke decoder die hem niet had verwacht. En de groottemath is genadeloos: een slordige tekensetkeus kost je de base64-toeslag op twee keer zoveel data, dus de vraag is nooit "gaat dit coderen?" maar "wat zal de andere kant verwachten te vinden wanneer het uitpakt?" De gouden regel: beide eindes van de reis moeten vóór de start van de base64 het eens zijn over de bytevorm, want de decoder heeft geen manier om te raden wat je koos, en het zal ook niet vragen.
Omwikkeling: twee gewoontes, één parameter
De opties van de methode gaan allemaal over regelbreking, en ze bestaan allemaal omdat twee formaten uit de twintigste eeuw het niet eens konden worden over hoe lang een regel letters moet zijn. MIME, de e-mailstandaard van 1996, wikkelt base64 af op 76 tekens met CRLF-regeleindes. PEM, de Privacy-Enhanced Mail-stam uit 1987, wikkelt af op 64 tekens, en dat is de vorm die je vindt in certificaten en sleutels, in de -----BEGIN CERTIFICATE------blokken die je servers in een configuratiemap bewaren.
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 tekens op één regel
print(pemStyle.components(separatedBy: "\n").count) // 7 regels van hoogstens 64
print(mimeStyle.components(separatedBy: "\r\n").count) // 6 regels van hoogstens 76
| Optie | Taak | Let op |
|---|---|---|
.lineLength64Characters |
knipt een regel af na 64 tekens, de PEM-gewoonte | het regeleinde is CRLF tenzij je het anders zegt |
.lineLength76Characters |
knipt een regel af na 76 tekens, de MIME-gewoonte | dezelfde CRLF-standaard |
.endLineWithCarriageReturn |
neemt een carriage return op in het regeleinde | op zichzelf is dit CR-only, old-Mac-stijl, en zelden wat je wilt |
.endLineWithLineFeed |
neemt een line feed op in het regeleinde | geef beide opties door wanneer je CRLF bedoelt |
En nu de standaardwaarde die mensen verrast: vraag om een .lineLength-optie zonder een regeleinde te kiezen, en het regeleinde dat je krijgt is CRLF, het complete paar van carriage return plus line feed. De methode heeft een huisstijl, en die huisstijl is 1996. Alleen LF gewenst? Betaal er expliciet voor met .endLineWithLineFeed en niets anders. Nog één huisregel voor het record: de laatste regel krijgt nooit een regeleinde achteraan. Een omgewikkeld resultaat eindigt met zijn laatste datateken of zijn =-pads, welke opties je ook koos, dus kun je samenvoegen en plakken zonder een verlaten lege regel aan het einde. En zonder enige optie is de output één ononderbroken regel, en dat is de juiste vorm voor JSON-bodies, URLs en API-payloads: het werk dat een moderne Swift-app in de praktijk het meest doet.
Base64url: een tekenreeks die kan reizen
Het standaard-alfabet is een goed staatsburger van JSON en een verschrikkelijke staatsburger van een URL. In een query string wordt een + door form-parsing gelezen als spatie, een / is een pad-scheidingsteken, en een = scheidt sleutels van waarden, en daarom maakt percent-coderen van het standaard-alfabet het langer en lelijker in plaats van korter. Sectie 5 van RFC 4648 bestaat om precies dat te herstellen: het "URL- en bestandsnaam-veilige" alfabet, waar + wordt -, / wordt _, en de =-padding doorgaans wordt weggelaten, omdat een pad in een URL doorgaans %3D wordt, wat de bedoeling tenietdoet. De RFC voegt een waarschuwing toe die je zou kunnen omlijsten: deze codering "mag niet als hetzelfde beschouwd worden als de base64-codering". YouTube-videoids, JWTs en de meeste moderne API-identifiers spreken het, dus verwacht het te gaan gebruiken.
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
Kijk goed naar die output: deze specifieke payload produceerde toevallig geen + of /, dus verschillen de twee spellingen alleen door de weggelaten padding. Wijzig één byte en ze gaan in het alfabet uit elkaar, en dat is precies het punt. Twee regels van aanpak. Kies het dialect één keer, aan de grens waar je data de buitenwereld ontmoet, en meng nooit alfabetten binnen hetzelfde document: een standaarddecoder die base64url ontvangt (of vice versa) wijst de invoer af of, in tolerante modi, verwijdert de buitenlandse tekens en reikt je de verkeerde bytes aan. En noem je helper eerlijk, zodat de volgende ontwikkelaar weet dat de tekenreeks base64url is en geen typefout. Dezelfde extensie kan korter op nieuwere toolchains: de nieuwste SDK-betas bevatten nu een native .base64URLAlphabet-optie die de alfabetruil binnen het framework doet, met een bijbehorende .omitPaddingCharacter-optie, en open-source Foundation draagt dezelfde opties mee achter een availability-marker voor latere toolchains. Tot ze je minimale deployment target bereiken, is de extensie van vier regels het draagbare antwoord, en blijft hij per constructie op elk platform werken.
JSON en APIs: de Base64 waar je niet om vroeg
Deze verrast de meeste mensen die met Codable werken, en verdient daarom zijn eigen sectie: de standaardstrategie van JSONEncoder voor een Data-property is al base64. Als een Codable-struct een Data-veld heeft, pakt de encoder het automatisch in met standaard base64, en JSONDecoder pakt het automatisch weer uit onderweg terug. Geen optie, geen configuratie, geen ceremonie.
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))
// het icoon reisde over de kabel als "8J+QsQ=="
De icon-property reisde over de kabel als 8J+QsQ==, omdat dat de huisstijl is. Er zijn alternatieven, en de twee die je daadwerkelijk zult tegenkomen zijn .custom, die je de data en een encoder aanreikt en je laat beslissen over de representatie, en het nieuwere .deferredToData, dat het aan de data-instantie zelf overlaat. Op het moment dat een API base64url wil in plaats van standaard, is .custom de plek waar je extensie uit de vorige sectie past:
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))
// het icoon reisde over de kabel als "8J-QsQ"
Één waarschuwing die een werkend feature scheidt van een productie-incident: een JSON-tekenreeks mag geen rauw regeleinde bevatten. Als je een payload omwikkelt met een .lineLength-optie en het resultaat zonder escaperen in een JSON-document interpoleert, heb je helemaal geen JSON-waarde gemaakt; je hebt een syntaxfout met een base64-accent gemaakt, en de parser zal dat bewijzen. Omgewikkelde output hoort in e-mailbodies en certificaatbestanden. Alles wat in JSON, URLs of query strings woont krijgt de gewone, niet-omwikkelde tekenreeks.
Data-URIs: de afbeelding in een tekenreeks
De lievelingsstreuk van het web is de bytes van een bestand direct in een URL verstoppen: data:{mime};base64,{payload}. Eentje bouwen in Swift is lezen, coderen en tekenreeksconcatenatie:
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
Het voorbeeld bouwt de beroemde transparante GIF van 42 byte (kleinere niet-transparante bestaan wel, maar dit is degene die iedereen insluit) om tot een data-URI die een browser weergeeft zonder een tweede request. Op Apple-platformen is de omgekeerde richting een one-liner: dezelfde Data die je inpakte gaat rechtstreeks in UIImage(data:) of NSImage(data:). De afweging is grootte, en die stapelt op: een afbeelding van 100 kilobyte wordt een tekenreeks van meer dan 133.000 tekens vóórdat je de data:image/png;base64,-prefix toevoegt. Data-URIs schitteren voor iconen, avatars en kleine assets, en ze laten bandbreedte stilletjes opzwellen voor hero-afbeeldingen, dus houd ze voor de kleine dingen.
JWTs: de eerste twee delen verzegelen
De coderingskant van een JSON Web Token is twee verzegelingen plus een handtekening, en de verzegeling is jouw base64url-extensie met weggelaten padding, precies wat het formaat eist. De header en de payload zijn JSON-documenten, en beide delen krijgen dezelfde behandeling:
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
Twee herinneringen. Het derde, door puntjes gescheiden deel is een cryptografische handtekening, berekend over de eerste twee, en het is het enige deel van het token dat enige garantie biedt: de header en de claims zijn gewoon JSON in een loopjas, dus geheimen horen er nooit in. En let hoe de padding verdwijnt in seal(): JWT-decoders aan de andere kant (inclusief die in het zusterartikel) zetten hem terug met een modulo-aanvulling, zodat de twee richtingen van de reis elkaar ontmoeten op gemeenschappelijke grond.
HTTP-headers: Basic en de rest
De oude Authorization: Basic-header wil een gebruikersnaam en een wachtwoord, aan elkaar gekoppeld met een dubbele punt, ingepakt met standaard base64, omdat in een header + en / onschuldig zijn en de dialectvraag niet opkomt:
import Foundation
let credentials = "editor:s3cret"
let header = "Basic " + Data(credentials.utf8).base64EncodedString()
print("Authorization: " + header)
// Authorization: Basic ZWRpdG9yOnMzY3JldA==
Dezelfde luide voetnoot als overal: het inpakken biedt van zichzelf nul beveiliging, en de header is alleen zo veilig als de HTTPS-verbinding die hem vervoert. De moderne neef, Authorization: Bearer, vervoert in plaats daarvan een JWT, dus is het verzegelrecept uit de JWT-sectie wat daar over de kabel gaat. De enige plek waar de dialectvraag in HTTP wél opkomt is de query string: als je API een identifier in een URL laat reizen, dan moet die identifier base64url zijn, of op z'n minst percent-gecodeerde standaard base64, nooit het rauwe standaard-alfabet met een + die als spatie kan worden gelezen.
E-mailbijlagen: het contract van 76 tekens
Wanneer je app een bijlage produceert die de 7-bit-herkomst van SMTP moet overleven, is het contract dat van MIME: base64 omgebroken op 76 tekens met CRLF-regeleindes, en een Content-Transfer-Encoding: base64-header die de ontvanger vertelt wat hij moet verwachten. De optiessectie toonde al de spelling; hier is de complete vorm van een omgewikkelde 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 regels
print(lines.map { $0.count }.max() ?? 0) // 76, de langste
print(body.hasSuffix("\r\n")) // false, de laatste regel blijft kaal
De grootte-inkasso voor dit dialect is de beroemde: de 4/3-alfabettoeslag plus een regeleinde om de 76 tekens landt in de buurt van 137 procent van het origineel, en de oude mail-engineering-snelregel "vermenigvuldig het origineel met 1,37 en tel er ruwweg 800 bytes headers bij op" werkt nog steeds om bijlagengroottes op het oog te schatten in een mailclient. Het is folklore met de juiste rekensom, en het is de ene plek in dit artikel waar de 33-procent-toeslag een tweede decimaal groeit.
Configuratie, omgeving en databanken: het onderstreepteken verbergen
Er is een stille klasse van klussen waar de enige deugd van base64 is dat zijn output een kleine, voorspelbare tekenset is: een binair blob of een gestructureerde waarde weglekken op een plek die gewone tekst wil. Omgevingsvariabelen die een shell-configuratiebestand moeten overleven, kolommen in een databank die eerder van varchar houdt dan van blob, een LDAP-bestand met zijn base64-marker, een QR-code die letters betrouwbaarder scant dan bits. Het patroon is overal hetzelfde: beslis de bytes, codeer, bewaar de tekenreeks, decodeer aan de andere kant.
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)
}
Twee valkuilen wonen hier. De eerste is de dubbele omwikkeling: twee integratielagen die allebei "helpend" coderen, zodat de waarde die je bewaart base64 van base64 is, en de lezer die één keer decodeert een muur van letters krijgt en denkt dat het feature kapot is. Codeer exact één keer, aan exact één grens, en zeg dat in een commentaar. De tweede is dialectdrift door omgeving: als de waarde ooit een URL, een formulierveld of een shell zal doorreizen die + en / verminkt, bewaar dan de base64url-spelling in plaats daarvan, want de tekenset is het hele punt van het formaat.
Bestanden: de .b64-rondreis
De klus "maak van dit bestand een .b64-tekstbestand" is lezen, aanroepen en schrijven:
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)
// later, mogelijk in een ander proces
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")
}
De trimmingCharacters op de terugreis is er omdat wat het bestand schreef mogelijk een regeleinde heeft toegevoegd, en de strikte decoder behandelt een regeleinde aan het einde als een vonnis van nil. Die rondreis komt byte voor byte terug, en dat moet je de eerste keer dat je het verstuurt verifiëren. Voor bestanden groot genoeg om geheugengebruik interessant te maken, codeer dan de hele buffer niet in één keer. Base64 heeft een prachtige eigenschap die streaming exact maakt: elke drie invoerbytes geven vier onafhankelijke tekens in de output, dus zolang elk blok dat je codeert een veelvoud van drie bytes is, is de samengevoegde output identiek aan het coderen van het hele bestand in één keer. Breek de uitlijning en de output verandert, want een blokgrens snijdt een drie-byte-groep midden in de stream doormidden:
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)
}
}
Het piekgeheugen is één leesbuffer plus de huidige regel, hoe groot het bestand ook is, en de omgewikkelde output komt exact overeen met de one-shot-spelling van .lineLength76Characters. Dezelfde veelvoud-van-drie-regel, met de rollen omgekeerd, is degene waar de streaming-decoder in het zusterartikel op leunt, zodat de twee kanten van de reis één rekenkundige waarheid delen.
Grote payloads en de geheugenrekening
Laten we de rekensommen doen die je nodig zult hebben de volgende keer dat iemand vraagt "kunnen we dit base64-coderen?" Elke drie invoerbytes worden vier uitvoertekens, dus de grootte vermenigvuldigt met 4/3: een bestand van 100 kilobyte wordt een tekenreeks van 133.336 tekens, een bestand van 10 megabyte wordt 13.333.336 tekens, enzovoort. Padding voegt hooguit twee tekens toe aan het allerlaatste eind, een afrondingsfout voor alles groter dan een paar bytes, en lege invoer is de enige vrijstelling, waar de belastingdienst één gratis ticket geeft en het resultaat de lege tekenreeks is. Drie praktische gevolgen. Eerst, budgetteer vóórdat je begint: als je payload al dicht bij een limiet zit (de comfortzone van zo'n 2.000 tekens van een URL, het contract van een JSON-veld, de breedte van een databankkolom), deel dan de limiet door 1,33 vóórdat je codeert, niet erna (en door 1,37 wanneer omwikkeling betrokken is). Ten tweede, terwijl je inpakt, houd je de originele bytes en de ingepakte tekenreeks tegelijk vast, dus is de werkset ongeveer 2,33 keer het origineel, en zijn de streaming-functies hierboven de ontsnappingsroute wanneer dat getal niet langer comfortabel is. Ten derde, de belasting is in de praktijk eenweg: je betaalt hem wanneer je inpakt en je bytes komen thuis wanneer iemand uitpakt, dus is de echte vraag nooit "is base64 duur?" maar "vereist de tekst-weg waar ik op zit dat?".
De fouten die bijten
- De tekensetstap die kan falen.
String.data(using:)kannilantwoorden (probeer.asciimet een accentletterteken), en force-unwrappen is de klassieke upgrade van slechte invoer naar een gecrashte app. Bewaak de omzetting, niet alleen de base64-aanroep, en dat is het makkelijke deel. - De CRLF-huisstijl. Een
.lineLength-optie zonder een regeleinde-optie produceert standaard CRLF. Als je formaat alleen LF wil en je de optie vergeten bent, draagt je output carriage returns die het nooit zou moeten hebben. - De CR-only-val.
.endLineWithCarriageReturnalleen produceert CR-only regeleindes in old-Mac-stijl. Als je CRLF bedoelt (en dat doet je voor MIME), geef dan beide regeleinde-opties door. - Omwikkeling in JSON. Een rauw regeleinde binnen een JSON-tekenreeks is ongeldig JSON, punt. Omgewikkelde base64 die in een document geïnterpoleerd wordt, is een syntaxfout met een base64-accent. Houd omgewikkelde output in e-mailbodies en certificaatbestanden.
- De BOM-liftpassagier. De gewone
.utf16-omzetting voegt vooraan een twee-byte BOM toe die meereist naar je ingepakte output en decoders verwarrt die hem niet hadden verwacht. Gebruik.utf16LittleEndianof.utf16BigEndianwanneer je UTF-16 nodig hebt zonder het label. - Dialectdrift. Standaard en base64url zijn verschillende alfabetten, en de RFC zegt het in zwarte letters. Een
+die overleeft tot in een query string wordt een spatie; een-die een tolerante standaarddecoder bereikt, wordt verwijderd. Kies het dialect aan de grens en houd het. - Hoofdletter is een letter. Het alfabet onderscheidt
Avana. Een kopie-plak met case-folding of een enthousiaste uppercase-aanroep corrupteert de data stilletjes, want beide versies halen nog steeds elke alfabetcheck. Base64 is hoofdlettergevoelig op de manier dat een paspoortnummer dat is. - De uitlijningsregel. Streaming-encoders moeten blokken snijden op veelvouden van drie bytes. Een niet-uitgelijnd blok verandert de output, en de verandering is stil: de tekenreeks decodeert nog steeds, naar de verkeerde data.
- De dubbele omwikkeling. Twee lagen die allebei coderen produceren base64 van base64. De lezer die één keer decodeert ziet letters waar bytes zouden moeten zijn, en het incident schrijft zichzelf.
- De availability-muur. De nieuwe native opties (
.base64URLAlphabet,.omitPaddingCharacter) bestaan op de nieuwste SDK-betas en in open-source Foundation achter een availability-marker, maar niet op elke toolchain die je CI zal raken. Als je ze overneemt, bewaak ze dan met availability-checks zodat dezelfde bron ook op oudere Xcode en op Linux bouwt. Op de huidige stabiele toolchain compileert de extensie van vier regels overal waar de opties niet bestaan. - Base64 is geen versleuteling. Als de eis vertrouwelijkheid is, heb je het verkeerde gereedschap gekozen, een hele categorie ver. Het werk van Base64 is bytes laten reizen, en het doet precies dat werk, niets meer.
Zo verzend je het
- Codeer bytes, geen wensen. Beslis de bytevorm vóórdat je de methode aanroept, standaard UTF-8 en expliciet genoemd wanneer het dat niet is, en bewaak de
data(using:)-stap die kan falen, want dat is waar data daadwerkelijk weggaat. - Standaard niet omgewikkeld, omgewikkeld per contract. De gewone single-line-uitvoer is correct voor JSON, APIs en de meeste databanken; grijp naar de 64/76-omwikkelopties alleen wanneer het ontvangende formaat ze eist, en betaal voor beide regeleinde-opties wanneer je CRLF bedoelt.
- Eén dialect per grens. Standaard base64 voor tekstgeoriënteerde bestemmingen, base64url voor alles dat een URL of een bestandsnaam zal raken, nooit de twee in hetzelfde document. Schrijf de omzetting één keer, noem die eerlijk en hergebruik die.
- Budgetteer de toeslag. Vermenigvuldig met 4/3 vóórdat je begint (met 1,37 wanneer omwikkeling in het spel is), en stream met 3-byte-uitgelijnde blokken wanneer de payload groot genoeg is om de werkset ongemakkelijk te maken.
- Gebruik verpakkingsplastic niet als slot. Als de eis geheimhouding is, stop dan bij het base64-schap en pak in plaats daarvan versleuteling.
Een korte geschiedenis van inpakken
Het alfabet waarmee je inpakt en de regellengtes waarnaar je omwikkelt zijn fossielen uit vier decennia aan ruzie over hoeveel binair een tekst-weg kan overleven, en de positie van Swift in die geschiedenis is kort maar interessant:
- 1980s, de era van dezelfde machines. De eerste encoders van dit geslacht bestonden om bestanden over dial-up te verplaatsen tussen systemen die aannamen dat de andere kant een machine was zoals de hunne. uuencode op UNIX gebruikte hoofdletters, cijfers en leestekens, en de ontwerpers vonden een truc die rekenkracht bespaarde: het alfabet zit op opeenvolgende ASCII-posities, dus coderen was letterlijk "32 optellen" zonder opzoektabel. BinHex, de neef die in 1981 op de TRS-80 werd geboren, hopte over naar de Apple II, werd in 1984 het formaat van de klassieke Macintosh, en zette een andere inzet in: zijn 64 tekens laten
7,O,W,g,oen bijna de helft van de kleine letters weg. - 1987, het alfabet krijgt een adres. RFC 989, de eerste Privacy-Enhanced Mail-specificatie, standaardiseerde de exacte 64 tekens die je vandaag typt, brak output af op 64 tekens per regel, en gebruikte
=voor padding en*om gecodeerde-maar-onversleutelde data te markeren. Elk PEM-stijl-blok dat je ooit in een serverconfiguratie hebt geplakt, is een nakomeling van dit document. - 1996, het liberale tijdperk. MIME (RFC 2045) nam het alfabet voor e-mailbijlagen over en verplaatste de omwikkeling naar 76 tekens, met de regel die omwikkeling veilig maakte om te produceren: decoders moeten de regeleindes negeren. Encoders leerden omwikken; decoders leerden vergeven. De 76-tekens-optie van Swift is een levend souvenir van precies dit debat.
- 2003 tot 2006, de regels verharderen. RFC 3548 (2003) verklaarde dat padding niet mag worden overgeslagen (tenzij een formaat anders zegt) en dat decoders tekens buiten het alfabet moeten weigeren; RFC 4648 (oktober 2006) stelde het geslacht op z'n plaats en voegde het URL-veilige alfabet toe, expliciet zodat lange identifiers in URLs konden leven zonder elk speciaal teken percent-geescape te hoeven. De conventie "geen padding in het URL-dialect" werd geboren in hetzelfde document, omdat een padteken in een URL doorgaans
%3Dwordt, wat de bedoeling tenietdoet. - 2013 tot 2014, de API is al hier. Apples
NSData-klasse had base64 al jaren ingepakt, en de optie-gebaseerde API met de vier omwikkelopties arriveerde in 2013 in iOS 7, voordat Swift überhaupt bestond. Toen Swift 1.0 op 9 september 2014 arriveerde, erfde het een totale encoder met vier omwikkelopties en het alfabet van 64 letters uit 1987, en de persoonlijkheid is sindsdien niet meer veranderd. - 3 december 2015, de toolchain verlaat het gebouw. Swift werd die dag open source, en de base64 van Foundation oversteeg met hem Linux en later Windows. "Base64-coderen in Swift weg van een Apple-machine" is nog geen decennium oud: een zeer jonge gast op een feest dat in 1987 begon.
- 2023 tot 2026, de herschrijving en het URL-dialect. De herschrijving van Foundation (het swift-foundation-project) verplaatste
Datanaar een pure-Swift-kern, en in 2025 voegde een community-pitch native opties toe voor base64url en voor het weglaten van padding. Op het moment van schrijven leveren de nieuwste SDK-betas en de open-source-toolchain de encode-opties, de rest van het geslacht rijpt in open-source Foundation achter availability-markers, en blijft de community-extensie in de tussentijd de draagbare brug.
Kleine verrukkingen
- Eén megabyte pakt uit naar exact 1.333.336 base64-tekens, de 4/3-belasting plus twee paddingtekens, tot op het cijfer na. De enige invoer die de belasting helemaal ontkomt, is de lege: niets erin, niets eruit.
- De encoder is total op een manier die de decoder niet is. Hij geeft nooit nil terug, gooit nooit iets omhoog en weigert nooit. Het enige falen in de hele pipeline woont stroomopwaarts, in de tekensetstap, en daarom voelt de methode zoveel kalmer aan dan zijn zuster.
- Codeer het woord
hélloin UTF-8 en het wordtaMOpbGxv; codeer het in UTF-16 little-endian en het wordtaADpAGwAbABvAA==. Zelfde woord, twee verschillende paspoorten, allebei geldig, geen van beide verwisselbaar. - Het testwoord van de base64-wereld is
foobar, en het pakt uit naarZm9vYmFy. Als je ooit een base64-voorbeeld in het wild hebt gezien, is de kans flink dat foobar betrokken was. - De beroemde transparante 1x1 GIF is 42 byte en opent met het magische woord
GIF89a, en daarom verschijnt de prefixR0lGODlhin meer codebases op aarde dan bijna elke andere base64-tekenreeks. - Je
Codable-struct stuurt al jaren base64 zonder dat je het merkt: de standaardstrategie voorDatavanJSONEncoderpakt in met standaard base64, en daarom kruist eenData-veld de kabel als gepadde tekenreeks in plaats van als array van getallen. - Padding gaat nooit verder dan twee tekens, ooit niet. Een payload van 1 byte eindigt op
==, een payload van 2 bytes eindigt op=, en een payload van 3 bytes eindigt op niets. De hele grammatica van de laatste groep past op een vingernagel. - Swift is 27 jaar jonger dan het alfabet waarmee het inpakt. De taal verscheen in 2014; de 64 letters werden in 1987 gestandaardiseerd en zijn sindsdien niet meer veranderd.
Dat is de complete inpakgereedschapskist: één totale methode, één falende stap die ervoor komt, vier omwikkelopties met een CRLF-huisstijl, een extensie van vier regels voor base64url, een 3-byte-uitlijningsregel voor streaming, en een 4/3-toeslag die de toegangsprijs is tot de tekst-weg. Encoderen is waar je de rekening van base64 betaalt, en je kent nu elke post vóórdat je ondertekent. Op het moment dat je de rit omdraait en begint te openen wat anderen hebben ingepakt, nemen de nils, de witruimtevonnissen en de blinde vlek van de tolerante knop het podium in. Het gerelateerde decoderingsartikel draait de complete show op die helft van de rondreis, dus wanneer de letters beginnen aan te komen, weet je al precies hoe je ze moet openen.
Laatst bijgewerkt: 2026-10-06
Gerelateerd artikel: Base64-decodering in Swift: een complete gids