Base64-decodering in Swift: een complete gids
Ergens in je pipeline is je data vermomd: een token verstopt in een HTTP-header, een avatar die zich in een JSON-veld verschuilt, een .b64-bestand dat je de afgelopen week al wilt bekijken, een e-mailbijlage die aankwam als een muur van letters. Die vermommingen afzetten in Swift is een van de aangenaamste klussen in de taal: één framework, één initialiser, en een regelwerk dat zo kort is dat het op een post-it past.
De startpagina van deze site dekt het formaat zelf al (64 weergavebare tekens, zes bits per teken, hooguit twee =-tekens als padding in de laatste groep), dus vertellen we dat verhaal hier niet opnieuw. Houd gewoon twee feiten in je zak. Ten eerste is base64 een manier om bytes te verkleden als tekst, geen slot. Ten tweede gaat elke base64-rit in Swift via één type, Data, en de decoder zit erop als initialiser die kan falen. Dat ene feit stuurt de rest van dit artikel, want een initialiser die kan falen verandert de manier waarop je elke regel die erop volgt schrijft.
Eén type draagt het hele werk
Swift verstrooit base64-helpers niet over een dozijn modules, en het laat je ook niets installeren. De decoder is Data(base64Encoded:options:) in Foundation, en het maakt sinds de vroege dagen van het framework deel uit van het platform (Apple noemt de initialiser vanaf iOS 8.0, macOS 10.10, tvOS 9.0, watchOS 2.0 en visionOS 1.0; de regellengte-opties aan de coderingskant reiken zelfs terug tot iOS 7.0). Op Linux en Windows wordt dezelfde Foundation meegeleverd met de open-source-toolchain, dus gedraagt de code hieronder zich hetzelfde in een iPhone-app, een serverworker en een script in je terminal.
Er is een zusterinitialiser, Data(base64Encoded: Data, options:), voor het geval je base64 aankomt als rauwe ASCII-bytes in plaats van als tekenreeks. Beide nemen een options-argument dat standaard [] is. En beide delen één persoonlijkheidsstreep die belangrijker is dan elke optie: ze kunnen falen.
import Foundation
let packed = "SGVsbG8sIFN3aWZ0IQ=="
if let data = Data(base64Encoded: packed) {
let text = String(data: data, encoding: .utf8)
print(text ?? "not text after all")
} else {
print("that was not base64")
}
// Hello, Swift!
De documentatie van Apple voor de initialiser is prachtig nuchter: het "geeft nil terug wanneer de invoer niet als geldige Base-64 wordt herkend". Geen excepties, geen gegoide fouten, geen logspam. Gewoon een stille nil en de verantwoordelijkheid om te bepalen wat dat voor je gebruiker betekent. Als je één ding onthoudt over base64 in Swift, dan dit: de decoder crasht nooit en klaagt nooit. Ze weigert gewoon.
Het oordeel van de decoder: een tabel van ja en nee
Wat betekent "geldig" dan voor deze decoder? Het blijkt een kort lijstje met harde regels te zijn, en dat lijstje is het verschil tussen "werkt in de demo" en "overleeft productie". Elke rij van de tabel hieronder is echt gedrag van de initialiser op een hedendaagse toolchain, dus kun je het zo rechtstreeks in je foutmeldingen citeren:
| Invoer | Oordeel | Waarom |
|---|---|---|
TWFu |
Man |
een volledige groep van vier tekens heeft helemaal geen padding nodig |
TQ== |
M |
één byte plus twee pads, het geval uit het leerboek |
SGVsbG8h |
Hello! |
acht tekens is een veelvoud van vier, dus zijn er geen pads nodig |
==== |
lege Data |
padding met niets erachter is legaal en decodeert naar nul bytes |
| de lege tekenreeks | lege Data |
niets erin, niets eruit, en de Optional slaagt nog steeds |
TQ |
nil |
lengte twee: er was een groep van vier beloofd, maar die kwam nooit |
T |
nil |
één teken draagt zes bits en een byte heeft er acht nodig |
SGVsbG8hTQ |
nil |
tien tekens: de laatste groep hangt in de lucht zonder zijn pads |
TQ=== |
nil |
drie pads: de derde heeft niets meer om te vullen |
TQ==TQ |
nil |
data na de padding is een stellig nee |
SGVs bG8h |
nil |
één spatie is buiten het alfabet, en de strikte modus laat geen genade over |
SGVsbG8h plus een regeleinde aan het einde |
nil |
de regeleinde aan het einde van een bestand dat je zojuist las telt als ruis |
Drie rijen verdienen een tweede blik. De ====-rij betekent dat een if let-check slaagt en je code verder vaart met nul bytes, dus als een lege payload in je app geen geldige staat is, check dan het aantal bytes direct na het decoderen. De rij met de lege tekenreeks is dezelfde truc met minder make-up. En de rij met het regeleinde aan het einde is met een slag de meest voorkomende reden dat een base64-bestand dat 's ochtends perfect werd gecodeerd 's middags weigert te decoderen: ergens onderweg is een regeleinde bijgekomen, en de strikte decoder neemt dat persoonlijk.
Er is ook een beroemde zachte plek die de tabel niet kan tonen. Vergelijk TQ== en TS==: beide decoderen naar dezelfde byte, M, want de twee laagste bits van dat laatste teken worden verworpen voordat ze ooit gecontroleerd worden. Gebruik in plaats daarvan Tg== en je krijgt N zonder protest. De decoder houdt de tekens in toom en laat de overige bits gaan. Die tolerantie is geen bug, maar het betekent wel dat twee verschillende tekenreeksen dezelfde data kunnen betekenen, en dat begint ertoe te doen het moment dat je systeem base64-waarden vergelijkt, dedupliceert of in de cache legt (meer daarover in de beveiligingssectie).
Wanneer de invoer rommeliger is dan je denkt
In de echte wereld komt base64 zelden aan als één spotloze regel. E-mailbijlagen worden omgebroken op 76 tekens, na elke regel met een carriage return en line feed, een gewoonte geërfd van de MIME-specificatie uit 1996, en certificaatbestanden worden omgebroken op 64. De decoder heeft precies één optie om die ruis af te handelen, en die is een grote:
import Foundation
let mimeBody = "SGVs\r\nbG8sIG1h\naWwgbm9pc2Uu"
if let data = Data(base64Encoded: mimeBody, options: .ignoreUnknownCharacters) {
print(String(data: data, encoding: .utf8) ?? "")
}
// Hello, mail noise.
.ignoreUnknownCharacters staat in de documentatie beschreven als een decoder die "onbekende niet-Base-64-bytes negeert, waaronder regeleindetekens", en voor dat werk is het het juiste gereedschap: de ruis wordt verwijderd, het alfabet overleeft, en de payload komt intact tevoorschijn. Maar de optie heeft een blinde vlek, en dat is de vlek die Swift-ontwikkelaars het hardst beet: hij verwijdert elk teken buiten het alfabet, inclusief de - en _ van base64url. Hij vertaalt ze niet naar + en /; hij gooit ze gewoon weg. Afhankelijk van wat die verwijdering achterlaat, krijg je een nil (wanneer de overlevenden geen hele groepen meer vormen) of, erger, een zelfverzekerd antwoord met het verkeerde aantal bytes. Een base64url-tekenreeks van 16 tekens die 12 bytes codeert kan uit de tolerante decoder terugkomen als 9 andere bytes, zonder fout en zonder excuus.
De regel om te onthouden: .ignoreUnknownCharacters is voor transportruis (regeleindes, verdwaalde spaties uit een kopie-plakactie), nooit voor alfabetsverschillen. Als de payload base64url zou kunnen zijn, zet dan eerst zelf de tekens om, precies zoals de volgende sectie laat zien, en geef de decoder een schone standaard-tekenreeks.
Het URL-alfabet
Sectie 5 van RFC 4648 definieert de neef van het standaard-alfabet dat je al de hele tijd tegenkomt: base64url, waar + wordt -, / wordt _, en de =-padding doorgaans wordt weggelaten. De reden is dezelfde die je URLs eerlijk houdt: in een query string wordt een + door form-parsing gelezen als spatie, een / is een path-scheidingsteken, en een = scheidt sleutels van waarden. De RFC is er helder over de relatie tussen de twee: de URL-variant "mag niet als hetzelfde beschouwd worden als de base64-codering". JWTs, Web Push-berichten, YouTube-videoids en de meeste moderne API-identifiers spreken base64url, dus verwacht het op je eerste dag tegen te komen.
Aan de decoderkant heeft het recept twee stappen: zet het alfabet om, en vul daarna de padding aan, want de strikte decoder wil zijn veelvoud van vier nog steeds.
import Foundation
extension String {
func dataFromBase64URL() -> Data? {
var fixed = self
.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")
let missing = fixed.count % 4
if missing > 0 {
fixed += String(repeating: "=", count: 4 - missing)
}
return Data(base64Encoded: fixed)
}
}
let tokenPart = "0S__zMWaTC-iVgJ-"
if let bytes = tokenPart.dataFromBase64URL() {
print(bytes.count) // 12
}
De modulo-regel is de hele truc: base64url-payloads komen typisch aan zonder padding, en één of twee =-tekens (nooit drie) herstellen de groep van vier die de decoder verwacht. Je zult een versie van deze extensie van vijf regels in een verrassend aantal Swift-codebases tegenkomen, en dat is geen toeval. Er is één reden waarom hij in de toekomst korter wordt: de nieuwste Apple SDKs (26.4 en hoger, op het moment van schrijven) kregen een native .base64URLAlphabet-optie voor de encoder, terwijl de bijbehorende decoder-opties nog rijpen in open-source Foundation, achter een availability-marker voor een latere toolchain. Tot die optie je minimale deployment target bereikt, is de extensie het draagbare antwoord, en blijft hij per constructie op elke versie werken.
Eerst bytes, pas later woorden
Hier is de beslissing die de decoder niet voor je kan nemen: hij reikt je een Data aan, een zak vol bytes zonder het geringste idee in welke tekenset de oorspronkelijke payload was geschreven. Als de payload tekst was, is het kiezen van die tekenset jouw taak, en Swift geeft je twee deuren uit de byte-wereld met heel verschillende karakters.
String(data:encoding:)is de strikte deur. Ze geeft een optional terug en antwoordtnilwanneer de bytes niet geldig zijn in de codering die je noemde. Ideaal voor validatie, gevaarlijk als je het antwoord force-unwrapt.String(decoding:as:)is de nooit-weigerende deur. Ze geeft altijd een tekenreeks terug, en vervangt alles waar hij geen boodschap van kan maken door het U+FFFD-vervangsteken. Ideaal voor logging en previews, gevaarlijk als je het resultaat opslaat en het data gaat noemen.
import Foundation
let bytes = Data([0xC3, 0xA5]) // de UTF-8-schrijfwijze van de letter a met ring
print(String(data: bytes, encoding: .utf8) ?? "?") // a met ring, correct gelezen
print(String(data: bytes, encoding: .isoLatin1) ?? "?") // twee verwarde letters, dezelfde bytes
print(String(decoding: bytes, as: UTF8.self)) // a met ring, en het crasht nooit
Het recept dat bijna alles dekt: probeer eerst strikte UTF-8, want dat is wat moderne APIs bijna altijd bedoelen; val alleen terug op ISO Latin-1 wanneer het contract zwijgt en je liever leesbaar-maar-fout hebt dan stilte; reserveer de nooit-weigerende deur voor debug-output. En één onzichtbare indringer om op te letten: als de payload begint met een UTF-8 BOM (de drie bytes EF BB BF), behoudt de strikte conversie hem, en begint je tekenreeks nu met een onzichtbaar U+FEFF-teken dat stilletjes gelijkheidschecks en JSON-roundtrips kapotmaakt. Strip hem weg met een prefix-check als de specificatie er geen belooft.
Bestanden openen
De klus "er is een .b64-bestand, geef me wat het verbergt" is lezen, bijknippen, decoderen en schrijven. Het bijknippen is geen decoratie; het is het verschil tussen een bestand dat opent en een bestand dat nil teruggeeft, want tools, mailclients en editors houden er allemaal van om een regeleinde achter te laten:
import Foundation
let inbox = URL(fileURLWithPath: "Downloads/avatar.b64")
let outbox = URL(fileURLWithPath: "Downloads/avatar.png")
let raw = try String(contentsOf: inbox, encoding: .utf8)
if let data = Data(base64Encoded:
raw.trimmingCharacters(in: .whitespacesAndNewlines)) {
try data.write(to: outbox)
} else {
print("the file was not base64 after all")
}
Als het bestand MIME-omwikkeld is (regeleindes om de 76 tekens), heb je twee schone ontsnappingswegen: decodeer met .ignoreUnknownCharacters en laat de optie de regeleindes opeten, of strip ze zelf weg met replacingOccurrences vóór een strikte decode. Beide zijn één regel. Voor bestanden die gewoon groot zijn, decodeer dan in uitgelijnde groepen in plaats van het hele bestand te lezen: elke groep van vier tekens decodeert op zich, dus je hoeft alleen de huidige groep plus een klein restant mee te nemen over de leesgrenzen heen.
import Foundation
func decodeBase64Chunks(_ stream: InputStream, into result: inout Data) throws {
let chunkSize = 65_536
var buffer = [UInt8](repeating: 0, count: chunkSize)
var leftover = ""
result = Data()
stream.open()
defer { stream.close() }
while stream.hasBytesAvailable {
let read = stream.read(&buffer, maxLength: chunkSize)
if read < 0 { throw CocoaError(.fileReadUnknown) }
if read == 0 { break }
var text = String(decoding: buffer[0..<read], as: UTF8.self)
text = text.replacingOccurrences(of: "\r", with: "")
.replacingOccurrences(of: "\n", with: "")
text = leftover + text
if text.count % 4 != 0 {
let whole = text.count - (text.count % 4)
leftover = String(text.suffix(text.count - whole))
text = String(text.prefix(whole))
} else {
leftover = ""
}
guard !text.isEmpty else { continue }
guard let part = Data(base64Encoded: text) else {
throw CocoaError(.fileReadCorruptFile)
}
result.append(part)
}
if !leftover.isEmpty {
guard let part = Data(base64Encoded: leftover) else {
throw CocoaError(.fileReadCorruptFile)
}
result.append(part)
}
}
Het geheugen blijft vlak, hoe groot het bestand ook is: één leesbuffer, één restant en het resultaat dat je aan het bouwen bent. Dezelfde lus behandelt een download die als base64 over de kabel aankomt, een logbestand dat eigenlijk een gecodeerde stream is, of elke payload die te groot is om in je hand te houden.
JWTs: de drie puntjes lezen
Een compacte JSON Web Token is drie base64url-delen aan elkaar gekoppeld met puntjes, en de eerste twee van die delen zijn gewoon JSON in een loopjas. Ze komen aan zonder padding, precies de combinatie die de strikte decoder op het eerste gezicht afwijst, dus jouw dataFromBase64URL()-helper uit de URL-sectie doet het zware werk:
import Foundation
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
func openPart(_ part: String) -> String? {
var fixed = part
.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")
let missing = fixed.count % 4
if missing > 0 {
fixed += String(repeating: "=", count: 4 - missing)
}
guard let data = Data(base64Encoded: fixed) else { return nil }
return String(data: data, encoding: .utf8)
}
let pieces = token.split(separator: ".")
print(openPart(String(pieces[0])) ?? "?")
// {"alg":"HS256","typ":"JWT"}
print(openPart(String(pieces[1])) ?? "?")
// {"sub":"1234567890","name":"John Doe"}
Twee herinneringen reizen mee. Een JWT is ondertekend, niet versleuteld: de header en de payload zijn publieke informatie, en daarom hoort een wachtwoord er nooit in thuis (de versleutelde neef, JWE, is helemaal een andere specificatie). En het derde, door puntjes gescheiden deel is een cryptografische handtekening, geen document, dus decodeer delen één en twee en laat de rest in rust.
Data-URIs: het bestand achter het komma
Web-APIs houden ervan binair in tekst te verstoppen met het data:-schema: een PNG in een profelveld, een lettertype in een CSS-blob, een QR-code in een instellingenbestand. Het formaat is data:{mime};base64,{payload}, en de payload eraf pellen is één split verwijderd:
import Foundation
let uri = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
let payload = uri.components(separatedBy: ",").last ?? ""
if let bytes = Data(base64Encoded: payload) {
print(String(decoding: bytes.prefix(6), as: UTF8.self)) // GIF89a
print(bytes.count) // 42
} else {
print("not a base64 data uri")
}
Het voorbeeld gebruikt de beroemde transparante GIF van 42 byte, de kleinste afbeelding van het formaat, en daarom verschijnen de openingstekens ervan in meer codebases dan bijna elke andere base64-tekenreeks op internet. Op Apple-platformen eindigt de pipeline in een one-liner: dezelfde Data die je zojuist decodeerde gaat rechtstreeks in UIImage(data:) of NSImage(data:), en daarom is "een avatar uit een API tonen" een klein feature en geen project.
HTTP: de Basic-header en zijn vrienden
De oude Authorization: Basic-header is een gebruikersnaam en een wachtwoord, aan elkaar gekoppeld met een dubbele punt, ingepakt met standaard base64 voor de reis (niet de URL-dialect: die woont in een header waar + en / volkomen onschuldig zijn). Het uitpakken is een split en een decode:
import Foundation
let header = "Basic ZWRpdG9yOnMzY3JldA=="
let packed = header.replacingOccurrences(of: "Basic ", with: "")
if let creds = Data(base64Encoded: packed) {
print(String(data: creds, encoding: .utf8) ?? "") // editor:s3cret
} else {
print("malformed header")
}
Houd de beveiligingsvoetnoot luid, want ze geldt voor elke base64 die je ooit zult tegenkomen: dit is inpakken, geen bescherming. Basic auth is alleen aanvaardbaar over HTTPS, waar TLS het echte waken doet en base64 er slechts voor zorgt dat de bytes de header-grammatica niet kapotmaken. Dezelfde redenering verklaart Authorization: Bearer-tokens: het token zelf is een JWT, dus geldt het decoderrecept uit de JWT-sectie erop ongewijzigd.
E-mail: de gewoonte van 76 tekens
Een e-mailbijlage die als base64 is gecodeerd, wordt omgebroken op 76 tekens met CRLF-regeleindes, precies de ruis waar de tolerante optie voor bestaat. De ruwe MIME-headers vertellen je welk alfabet en welke omwikkeling de afzender gebruikte (Content-Transfer-Encoding: base64), en de reparatie is één vlag:
import Foundation
let attachment = "VGhpcyBhdHRhY2htZW50IHN1cnZpdmVk\r\nIHRoZSA3Ni1jaGFyYWN0ZXIgaGFiaXQu"
if let data = Data(base64Encoded: attachment, options: .ignoreUnknownCharacters) {
print(String(data: data, encoding: .utf8) ?? "")
}
// Deze bijlage heeft de 76-karakters-gewoonte overleefd.
Als je een mailfunctie bouwt in plaats van er een te lezen, onthoud dan dat de 76-teken-omwikkeling je ook iets kost: met een regeleinde om de 76 tekens landt de gecodeerde tekst in de buurt van 137 procent van de oorspronkelijke grootte, en daarom schatten oude mail-ingenieurs bijlagengroottes op het oog met de snelle regel "vermenigvuldig het origineel met 1,37 en tel er ruwweg 800 bytes headers bij op". Het getal is folklore geworden, maar de rekensom klopt nog steeds.
De tweemaal omwikkelde payload
Het meest voorkomende "mijn data is corrupt"-ticket in base64-land is data dat tweemaal is ingepakt: een integratielaag codeerde het, en een tweede laag die de documentatie nooit las codeerde het resultaat. De verdedigingsbeweging is om één keer te decoderen, te kijken wat je krijgt, en als het resultaat zelf een nette base64-achtige tekenreeks is (goede lengte, goed alfabet, niets verrassends), dan nog een keer bewust te decoderen en daar te stoppen. Schrijf geen lus die doorgaat met decoderen tot het faalt. Zo'n lus opet zonder morren een perfect goed bestand waarvan de inhoud toevallig base64-achtig oogt, en nadat hij gelopen is, kan niemand meer zeggen waar de originele data begon.
import Foundation
func unwrapOnce(_ packed: String) -> Data? {
let cleaned = packed.trimmingCharacters(in: .whitespacesAndNewlines)
return Data(base64Encoded: cleaned)
}
let suspicious = "WVdKag==" // ziet er al ingepakt uit
if let first = unwrapOnce(suspicious) {
let inner = String(data: first, encoding: .utf8) ?? ""
if let second = unwrapOnce(inner) {
print("it was wrapped twice:", String(data: second, encoding: .utf8) ?? "?")
}
}
// it was wrapped twice: abc
Twee ontpakkingen, twee bewuste beslissingen, en een payload die eindelijk weer gewoon abc is.
Nil iets laten betekenen
Omdat de decoder nil antwoordt in plaats van een exceptie te gooien, is de foutafhandelingsstijl van je base64-code een keuze die je zelf maakt, en de keuze waar je later blij mee zult zijn is een kleine verpakkingslaag die de stilzwijgende weigering omschrijft naar een luide, specifieke fout:
import Foundation
enum Base64Failure: Error, CustomStringConvertible {
case notBase64(Int)
var description: String {
switch self {
case .notBase64(let length):
return "input of \(length) characters is not valid base64"
}
}
}
func decodeStrict(_ text: String) throws -> Data {
let cleaned = text.trimmingCharacters(in: .whitespacesAndNewlines)
guard let data = Data(base64Encoded: cleaned) else {
throw Base64Failure.notBase64(cleaned.count)
}
return data
}
do {
let bytes = try decodeStrict("c3ludGF4IGVycm")
print(String(data: bytes, encoding: .utf8) ?? "?")
} catch {
print(error) // input of 14 characters is not valid base64
}
De verpakkingslaag wordt tevens de enige plek waar normalisatie woont: het bijknippen, een eventuele alfabetomzetting, een eventuele padding-aanvulling. Aanroepers krijgen één functie, één betekenis voor falen, en geen ! in beeld. Force-unwrappen van Data(base64Encoded:)! is hoe een slechte payload een gecrashte app wordt, en de verpakkingslaag is de goedkope verzekering ertegen. Hetzelfde patroon werkt ook aan de command line, waar een script met CommandLine.arguments en een FileHandle-write "dit bestand vanuit de shell decoderen" een utility van vijf regels maakt in plaats van een kopieer-plak-omweggetje via een website.
Beveiliging, gemeten in bytes
- Het is geen versleuteling. Base64 is een omkeerbaar, direct leesbaar herinpakken. Als je dreigingsmodel een mens omvat met een browser en vijf seconden, heb je nul bescherming, en elke JWT-header bewijst dat punt dagelijks.
- Normaliseer voordat je vergelijkt. Omdat
TQ==enTS==naar dezelfde bytes decoderen, kunnen twee systemen verschillende spellingen van dezelfde data aanhouden. Een paper uit 2022, "Base64-vervormbaarheid in de praktijk", documenteerde wat die gebroken uniciteitsgarantie in het wild doet: log-mismatches, denial-of-service-aanvallen en gedupliceerde databaseregels. Als je Swift-app base64-waarden cacht, dedupliceert of vergelijkt, voer dan aan de poort één canonieke decode (of één canonieke hercodering) uit. - Leg een plafond op de invoer vóór de decode. Het decoderen van N tekens reserveert ruwweg driekwart van N bytes, terwijl je de invoer-tekenreeks nog in handen hebt. Een vijandige client kan 100 megabytes van de letter
Asturen en toezien hoe je geheugengebruik klimt voordat de decoder ooit nee zegt. Check eerst de lengte, goedkoop, en wijs wat te groot is af. - Wees op je hoede met de tolerante optie als filter.
.ignoreUnknownCharactersverwijdert tekens. Een "saniterende" passage erdoorheen kan een geldige base64url-payload veranderen in andere data zonder foutmelding. Het is een ruisfilter voor regeleindes, geen validator. - Houd het zoveel mogelijk uit URLs. Grote base64-payloads in query strings of pads schieten voorbij comfortabele URL-lengtes en worden door proxies verminkt. Leg ze in plaats daarvan in request bodies, bestanden of tokens.
Prestaties, kort gezegd
De decoder is een rondgang door een opzoektabel: elk teken wordt opgezocht in een kleine tabel, en een paar bits worden verschoven en OR-gecombineerd tot uitbytes. Op een hedendaagse toolchain is dat ruim snel genoeg voor alles dat in het geheugen past, en het getal om te onthouden is de output-verhouding: gedecodeerde bytes zijn ongeveer driekwart van de invoerlengte, dus een tekenreeks van 4 megabyte kost je zo'n 3 megabyte resultaat, bovenop de tekenreeks die je al vasthoudt. Als je op een pad zit waar Foundation zelf niet is toegestaan (een diep ge-embedded target, een WebAssembly-bundle), dan is het community-pakket swift-extras-base64 het opvallende alternatief: pure Swift zonder Foundation-afhankelijkheid, een RFC 4648-conforme encoder en decoder met base64url- en paddingopties, en benchmarks die het enkele malen sneller stellen dan Foundation. Een eerdere implementatie van hetzelfde pakket wordt zelfs meegeleverd in de WebSocket-ondersteuning van swift-nio, wat zo nabij productieniveau komt als een bijproject maar kan. Voor een gewone app of script is het overbodige bagage; voor de ingesnoerde hoek van Swift is het het standaardantwoord.
Een decennium aan uitpakken
Swift heeft hier niets van uitgevonden, en het is de moeite waard te weten waar elk onderdeel van de gereedschapskist vandaan komt:
- 1980s, de era van dezelfde machines. De vroegste coderingen van dit geslacht (uuencode op UNIX, BinHex op de TRS-80 en de klassieke Mac) verplaatsten bestanden tussen machines die aannamen dat de andere kant er net zo uitzag als hun eigen. uuencode gebruikte een alfabet van hoofdletters, cijfers en leestekens, en die letters staan op opeenvolgende ASCII-posities, dus coderen was een kwestie van 32 optellen, helemaal zonder opzoektabel. Decoders van deze era konden heel veel aannemen, en op het moment dat data ecosystemen overstak, ging het stuk.
- 1987, het alfabet krijgt een adres. RFC 989 (Privacy-Enhanced Mail, februari 1987) standaardiseerde het alfabet van 64 tekens, brak regels af op exact 64 tekens en gebruikte
=voor padding en*om gecodeerde-maar-onversleutelde data te markeren. Elk PEM-stijl-blok is een nakomeling van dat document. - 1996, het liberale tijdperk. MIME (RFC 2045) nam het alfabet voor e-mail over, verplaatste de omwikkeling naar 76 tekens en voorschreef aan conforme decoders elk teken buiten het alfabet te negeren, zoals de CRLF-regeleindes. Dit is het tijdperk dat een generatie trainde om tolerante decoders te verwachten, en het tijdperk waarvan de verwachtingen de strikte standaardinstelling van Swift opzettelijk doorbreekt.
- 2003 tot 2006, de regels verharderen. RFC 3548 (2003) deed een eerste poging het geslacht te verenigen; RFC 4648 (oktober 2006) stelde het definitief, codificeerde de paddingregels en voegde het URL-veilige alfabet toe. Zijn paragraaf over decoders is degene die Swift volgt: weiger tekens buiten het alfabet, tenzij het formaat dat je verwerkt expliciet zegt ze te negeren, zoals MIME doet.
- 2013 tot 2014, de API staat paraat. Apples
NSData-klasse pakte en ontpakte base64 al jaren, en de optie-gebaseerde API met haar decode-optie landde in 2013 in iOS 7, een jaar voordat Swift bestond. Toen Swift 1.0 op 9 september 2014 verscheen, kwam de decoder met de taal mee naar binnen en heeft sindsdien dezelfde persoonlijkheid behalten: een strikte kern, één tolerante knop, een initialiser die kan falen. - 3 december 2015, Linux krijgt een decoder. Swift werd die dag open source, en met hem oversteeg de base64 van Foundation de grens naar Linux en later Windows. "Base64-decoderen in Swift op een niet-Apple-machine" is nog geen decennium oud: een laatgekomen gast op een feest dat in 1987 begon.
- 2023 tot 2026, de herschrijving. 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, terwijl de decode-opties nog rijpen in de open-source-toolchain, dus blijven de handgemaakte extensies in de tussentijd het universele antwoord.
Kleine wonderen
====is legale invoer. Vier pads en geen data decoderen naar een legeData, de enige base64-tekenreeks waarvan de volledige inhoud "er is niets hier" is, en Swift is het daarmee eens.- De tekenpolitie van de decoder controleert het werk van de bitpolitie niet:
TS==enTQ==reiken je allebeiMaan, terwijlTg==jeNaanreikt. Dezelfde grammatica, andere bits, geen vragen gesteld. - Swifts
Datakan base64 decoderen dat aankwam als bytes in plaats van als tekenreeks, via deData(base64Encoded: Data)-variant, dus een payload die de reis over de kabel als ASCII maakte kan de tekenreeks-roundtrip helemaal overslaan. - Het woord dat al base64 is sinds de testvectoren werden geboren, 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 exact 42 byte en opent met het magische woord
GIF89a, en daarom verschijnen de eerste acht gecodeerde tekens ervan in meer codebases dan bijna elk ander base64-voorvoegsel op aarde. - De moderne open-source-decoder doet zijn ongeldige-tekencheck met één enkele vergelijking: hij OR-t de vier opzoekwaarden per positie bij elkaar en test het resultaat tegen een sentinel, dus één branch beslist het lot van een hele groep van vier tekens. De oudere implementatie deed hetzelfde werk met een tabel van 128 bytes, waarin elke waarde op of boven 0x80 "geen letter" betekende.
- UTF-8 BOMs zijn onzichtbaar:
EF BB BFaan het begin van een payload wordt een U+FEFF-teken dat de strikte conversie overleeft en dan een paar regels code later de gelijkheidschecks kapotmaakt. - Swift is 27 jaar jonger dan het alfabet dat het decodeert. De taal verscheen in 2014; de 64 letters die het verwerkt werden in 1987 gestandaardiseerd en zijn sindsdien niet meer veranderd.
Dat is de hele decoderingsgereedschapskist: één initialiser die kan falen, met een kort regelwerk, één tolerante knop met een gedocumenteerde blinde vlek, een helper van vijf regels voor base64url, een tekensetbeslissing die bij jou behoort, een lus in blokken voor de grote bestanden, en een verpakkingslaag die de nil iets laat betekenen. Decoderen is waar base64 bijt, en je kent nu de namen van elke tand. Op het moment dat het werk omslaat en je begint bytes in te pakken voor de reis in plaats van ze uit te pakken, neemt de toeslag van ruwweg 33 procent het over en verschijnen de omwikkelopties. Het gerelateerde coderingsartikel dekt die helft van de rondreis volledig, dus ga daarheen wanneer je klaar bent om de andere kant op te verzenden.
Laatst bijgewerkt: 2026-10-06
Gerelateerd artikel: Base64-codering in Swift: een complete gids