Codifica Base64 in Swift: una guida completa
Hai qualcosa che deve viaggiare, e la strada ammette solo testo: un'API JSON che rifiuta i byte grezzi, un canale email che ricorda le sue origini a 7 bit, un URL che si strozza su qualsiasi cosa non sappia nominare, un file di configurazione che accetta solo i caratteri più banali. Benvenuto sul lato imballaggio del base64, dove Swift trasforma i tuoi byte in un muro amichevole di lettere con una sola chiamata di metodo, un sovrapprezzo di circa un carattere in più ogni tre byte, e alcune opzioni di spezzatura che esistono perché due decenni diversi avevano opinioni sulle lunghezze delle righe.
La home page di questo sito spiega già il formato nel dettaglio (64 caratteri stampabili, quattro per ogni tre byte in ingresso, al massimo due caratteri = di padding sull'ultimo gruppo), quindi la lezione sul formato è finita prima di iniziare. Due fatti da portare in questo articolo: base64 è imballaggio, non chiusura a chiave, e l'imballaggio espande i tuoi dati di circa il 33 percento, cosa che conta ogni volta che sei vicino a un limite di dimensione. In Swift, tutto il lavoro passa attraverso un unico tipo, Data, e un unico metodo totale, base64EncodedString(options:). L'unica vera abilità richiesta è sapere cosa succede nei due passi intorno a quel metodo, perché il metodo in sé non fallisce mai. Sono i passi a farlo.
Un solo metodo, zero scuse
Tutto ciò che riguarda il base64 in Swift vive su Data del framework Foundation, e ci vive dai primi rilasci della lingua (Apple riporta il metodo a partire da iOS 8.0, macOS 10.10, tvOS 9.0, watchOS 2.0 e visionOS 1.0). La pipeline è sempre la stessa in tre passi: porta il tuo contenuto in un Data, chiama il metodo, spedisci la stringa.
import Foundation
let note = "Pack it, wrap it, ship it."
let packed = Data(note.utf8).base64EncodedString()
print(packed) // UGFjayBpdCwgd3JhcCBpdCwgc2hpcCBpdC4=
Due dettagli di quelle tre righe meritano un'occhiata più da vicino. Il primo: Data(note.utf8) è il passo silenzioso: la vista utf8 può rappresentare ogni scalare Unicode per definizione, quindi non fallisce mai, ed è per questo che è il default nella maggior parte degli esempi. Il cugino fallibile, note.data(using:), può e risponde nil per alcune codifiche, e tutta quella decisione dei "quali byte" ha la sua sezione qui sotto, perché è il primo posto dove i tuoi dati possono andare persi. Il secondo: il metodo in sé è totale: risponde sempre, non ha casi di errore, e l'unica domanda che ti fa è quale spezzatura a capo vuoi. C'è anche un fratello, base64EncodedData(options:), che restituisce il risultato impacchettato come Data di byte ASCII invece che come stringa, per le pipeline dove la fermata successiva è un'API binaria e non un campo di testo.
E dato che metà di voi è arrivata via "la mia app Swift ha bisogno di una dipendenza base64": non c'è nulla da installare. Base64 fa parte di Foundation, Foundation fa parte della toolchain, e la toolchain arriva allo stesso modo su ogni piattaforma. Su macOS è Xcode o gli strumenti da riga di comando; su Linux e Windows è l'installer da swift.org, dove la linea stabile attuale, a data di stesura, è la 6.3.x e il gestore di versioni Swiftly è la porta d'ingresso consigliata; e le immagini Docker ufficiali coprono la folla dei container. Il tuo Package.swift resta vuoto, e così deve essere.
La prima vera decisione: quali byte?
Prima che anche un solo carattere base64 venga prodotto, hai già preso la decisione che conta di più, perché il base64 impacchetta byte, e una stringa è solo una stringa finché non scegli la sua forma in byte. L'UTF-8 è il default sensato e la risposta giusta per quasi tutto, ma nel momento in cui i tuoi dati vengono da un sistema legacy, da un protocollo binario o da un angolo di Unicode, la scelta smette di essere invisibile:
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==
| Conversione | Byte di "héllo" | Cosa trasporta il base64 |
|---|---|---|
.utf8 |
6 | aMOpbGxv, la grafia che le API moderne si aspettano |
.ascii |
fallisce con nil |
il carattere accentato sta sopra 0x7F e ASCII lo rifiuta |
.utf16 |
12 | il doppio delle dimensioni dell'UTF-8, più un byte-order mark di due byte che accompagna tutto in testa |
.utf16LittleEndian |
10 | la stessa parola senza il tag BOM: 10 byte, ancora la più pesante tra le opzioni senza BOM elencate qui |
In quell'output si nascondono tre lezioni. La forma data(using:) è fallibile e .ascii è il candidato perfetto per fallire, quindi farne il force-unwrap è il modo in cui una frase perfettamente buona diventa un'app in crash. La conversione .utf16 semplice antepone un byte-order mark di due byte (FF FE su una macchina little-endian), e quel BOM viaggia dentro il tuo output impacchettato e confonde qualsiasi decodificatore che non se l'aspettasse. E i conti delle dimensioni non perdono nulla: una scelta scriteriata del set di caratteri ti costa il sovrapprezzo base64 su due volte i dati, quindi la domanda non è mai "questo si codifica?" ma "cosa si aspetta di trovare l'altro capo quando apre?" La regola d'oro: entrambe le estremità del viaggio devono accordarsi sulla forma in byte prima che il base64 parta, perché il decodificatore non ha modo di indovinare cosa hai scelto e non lo chiederà.
Spezzatura delle righe: due abitudini, un parametro
Le opzioni del metodo riguardano tutte il cambio di riga, e esistono tutte perché due formati del Novecento non sono riusciti a mettersi d'accordo su quanto debba essere lunga una riga di lettere. MIME, lo standard email del 1996, spezza il base64 a 76 caratteri con a capo CRLF. PEM, la stirpe Privacy-Enhanced Mail del 1987, spezza a 64 caratteri, ed è quella la forma che trovi dentro certificati e chiavi, i blocchi -----BEGIN CERTIFICATE----- che i tuoi server tengono in una directory di configurazione.
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 caratteri su una riga sola
print(pemStyle.components(separatedBy: "\n").count) // 7 righe di massimo 64
print(mimeStyle.components(separatedBy: "\r\n").count) // 6 righe di massimo 76
| Opzione | Lavoro | Occhio a |
|---|---|---|
.lineLength64Characters |
taglia una riga dopo 64 caratteri, l'abitudine PEM | l'a capo è CRLF a meno che tu non dica diversamente |
.lineLength76Characters |
taglia una riga dopo 76 caratteri, l'abitudine MIME | stesso default CRLF |
.endLineWithCarriageReturn |
include un carriage return nell'a capo | da solo è solo-CR, stile Mac vecchio, e raramente è quello che vuoi |
.endLineWithLineFeed |
include un line feed nell'a capo | passa entrambe le opzioni quando intendi CRLF |
Ora il default che sorprende: chiedi qualsiasi opzione .lineLength senza scegliere un a capo, e l'a capo che ricevi è CRLF, la coppia completa di carriage return più line feed. Il metodo ha uno stile di casa, e il suo stile di casa è il 1996. Vuoi solo LF? Pagalo esplicitamente con .endLineWithLineFeed e nient'altro. Un'altra regola di casa per il verbale: l'ultima riga non riceve mai un a capo finale. Un risultato spezzato termina con l'ultimo carattere dei dati o con i suoi pad =, qualunque siano le opzioni scelte, quindi puoi concatenare e incollare senza una riga vuota orfana alla fine. E senza opzioni, l'output è una riga sola ininterrotta, che è la forma giusta per i body JSON, gli URL e i payload API: il lavoro che un'app Swift moderna fa davvero la maggior parte del tempo.
Base64url: una stringa che sa viaggiare
L'alfabeto standard è un ottimo cittadino del JSON e un cittadino terribile di un URL. In una query string, un + viene letto come spazio dal parsing dei form, un / è un separatore di percorso, e un = separa le chiavi dai valori, ed è per questo che fare il percent-encoding dell'alfabeto standard lo rende più lungo e più brutto invece che più corto. La sezione 5 di RFC 4648 esiste per correggere esattamente questo: l'"alfabeto sicuro per URL e nomi di file", dove + diventa -, / diventa _, e il padding = viene di solito tolto perché un pad in un URL di solito diventa %3D, vanificando lo scopo. Lo RFC aggiunge un avviso che meriterebbe la cornice: questa codifica "non deve essere considerata la stessa della codifica base64". ID video di YouTube, JWT e la maggior parte degli identificatori API moderni lo parlano, quindi aspettati di usarlo.
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
Guarda da vicino quell'output: per questo payload in particolare non è saltato fuori nessun + o /, quindi le due grafie differiscono solo per il padding tolto. Cambia un byte e divergeranno nell'alfabeto, che è proprio il punto. Due regole d'ingaggio. Scegli il dialetto una volta, al confine dove i tuoi dati incontrano il mondo esterno, e non mescolare mai alfabeti dentro lo stesso documento: un decodificatore standard che riceve base64url (o viceversa) rifiuterà l'input oppure, in modalità tolleranti, eliminerà i caratteri forestieri e ti consegnerà byte sbagliati. E dai al tuo helper un nome onesto, così lo sviluppatore successivo sa che la stringa è base64url e non un refuso. La stessa estensione può diventare più corta su toolchain più recenti: gli SDK beta più recenti includono ora un'opzione nativa .base64URLAlphabet che fa lo scambio di alfabeto dentro il framework, con un'opzione .omitPaddingCharacter corrispondente, e la Foundation open-source porta le stesse opzioni dietro un marcatore di disponibilità per toolchain successive. Finché non arriveranno al tuo target di deployment minimo, l'estensione di quattro righe è la risposta portatile, e continuerà a funzionare su ogni piattaforma per costruzione.
JSON e API: il Base64 che non hai mai chiesto
Questa sorprende la maggior parte di chi lavora con Codable, quindi si guadagna la sua sezione: la strategia default di JSONEncoder per una proprietà Data è già base64. Se uno struct Codable ha un campo Data, l'encoder lo impacchetta automaticamente con base64 standard, e JSONDecoder lo decodifica automaticamente nel viaggio di ritorno. Nessuna opzione, nessuna configurazione, nessuna cerimonia.
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))
// l'icona ha attraversato la rete come "8J+QsQ=="
La proprietà icon ha attraversato la rete come 8J+QsQ== perché quello è lo stile di casa. Ci sono alternative, e le due che incontrerai davvero sono .custom, che ti passa i dati e un encoder e ti fa decidere la rappresentazione, e la più recente .deferredToData, che delega all'istanza dei dati stessa. Nel momento in cui un'API vuole base64url invece di standard, .custom è il posto dove la tua estensione della sezione precedente si aggancia:
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))
// l'icona ha attraversato la rete come "8J-QsQ"
Un'avvertenza che separa una feature che funziona da un incidente in produzione: una stringa JSON non può contenere un a capo grezzo. Se spezzi un payload con un'opzione .lineLength e interpoli il risultato in un documento JSON senza escaping, non hai fatto un valore JSON in nessun modo; hai fatto un errore di sintassi con un accento base64, e il parser lo dimostrerà. L'output spezzato sta nei body email e nei file di certificati. Tutto ciò che vive dentro JSON, URL o query string riceve la stringa semplice non spezzata.
Data URI: l'immagine dentro una stringa
Il trucco preferito del web è incorporare i byte di un file direttamente in un URL: data:{mime};base64,{payload}. Costruirne uno in Swift è una lettura, una codifica e una concatenazione di stringhe:
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
L'esempio ricrea il famoso GIF trasparente di 42 byte (ne esistono di più piccoli e non trasparenti, ma questo è quello che tutti incorporano) in un data URI che un browser renderizza senza una seconda richiesta. Sulle piattaforme Apple la direzione opposta è una riga sola: lo stesso Data che hai impacchettato va dritto in UIImage(data:) o NSImage(data:). Il compromesso è la dimensione, e si compone: un'immagine da 100 kilobyte diventa una stringa di oltre 133.000 caratteri prima ancora di aggiungere il prefisso data:image/png;base64,. I data URI brillano per icone, avatar e asset minuscoli, e gonfiano silenziosamente la banda per le foto di copertina, quindi tienili per le cose piccole.
JWT: sigillare le prime due parti
Il lato codifica di un JSON Web Token è due sigillature più una firma, e la sigillatura è la tua estensione base64url con il padding tolto, che è esattamente ciò che il formato pretende. L'header e il payload sono documenti JSON, e entrambe le parti ricevono lo stesso trattamento:
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
Due promemoria. La terza parte separata da puntini è una firma crittografica calcolata sulle prime due, ed è l'unica parte del token che fornisce qualche garanzia: l'header e i claims sono JSON comune con un trench addosso, quindi i segreti non finiscono mai dentro. E nota come il padding sparisca in seal(): i decodificatori JWT dall'altra parte (incluso quello dell'articolo collegato) lo rimettono con un completamento a modulo, quindi le due direzioni del viaggio si incontrano su terreno comune.
Intestazioni HTTP: Basic e il resto
L'header Authorization: Basic classico vuole un nome utente e una password, uniti da due punti e impacchettati con base64 standard, perché in un header + e / sono inoffensivi e la domanda sul dialetto non sorge:
import Foundation
let credentials = "editor:s3cret"
let header = "Basic " + Data(credentials.utf8).base64EncodedString()
print("Authorization: " + header)
// Authorization: Basic ZWRpdG9yOnMzY3JldA==
Stessa nota a piè di pagina chiassosa, come dappertutto: l'imballaggio da solo fornisce zero sicurezza, e l'header è sicuro solo quanto la connessione HTTPS che lo trasporta. Il cugino moderno, Authorization: Bearer, trasporta invece un JWT, quindi la ricetta di sigillatura della sezione JWT è ciò che va sul cavo lì. L'unico posto dove la domanda sul dialetto sorge in HTTP è la query string: se la tua API lascia a un identificatore di viaggiare in un URL, quell'identificatore dovrebbe essere base64url, o almeno base64 standard percent-encodato, mai l'alfabeto standard grezzo con il suo + lasciato a essere letto come spazio.
Allegati email: il contratto dei 76 caratteri
Quando la tua app produce un allegato che deve sopravvivere alle origini a 7 bit di SMTP, il contratto è quello di MIME: base64 spezzato a 76 caratteri con a capo CRLF, e un header Content-Transfer-Encoding: base64 che dice al ricevente cosa aspettarsi. La sezione sulle opzioni ha già mostrato la grafia; qui c'è la forma completa di un body spezzato:
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 righe
print(lines.map { $0.count }.max() ?? 0) // 76, la più lunga
print(body.hasSuffix("\r\n")) // false, l'ultima riga resta nuda
Il conto delle dimensioni per questo dialetto è quello famoso: la tassa alfabetica del 4/3 più un a capo ogni 76 caratteri atterra vicino al 137 percento dell'originale, e la vecchia scorciatoia dell'ingegneria della posta "moltiplica l'originale per 1.37 e aggiungi circa 800 byte di header" funziona ancora per stimare a occhio la dimensione degli allegati in un client di posta. È folklore con l'aritmetica corretta, ed è l'unico posto di questo articolo dove il sovrapprezzo del 33 percento cresce un secondo decimale.
Configurazioni, ambiente e database: nascondere il trattino basso
Esiste una classe silenziosa di lavori dove l'unica virtù del base64 è che il suo output è un set di caratteri piccolo e prevedibile: nascondere un blob binario o un valore strutturato in un posto che vuole testo semplice. Variabili d'ambiente che devono sopravvivere a un file di configurazione della shell, colonne in un database che sta meglio con varchar che con blob, un file LDAP con il suo marcatore base64, un codice QR che legge le lettere più affidabilmente dei bit. Lo schema è lo stesso dappertutto: decidi i byte, codifica, salva la stringa, decodifica dall'altra parte.
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)
}
Qui vivono due trabocchetti. Il primo è il doppio avvolgimento: due livelli di integrazione che entrambi "gentilmente" codificano, quindi il valore che salvi è base64 di base64, e chi legge e decodifica una volta ottiene un muro di lettere e pensa che la feature sia rotta. Codifica esattamente una volta, a un solo confine, e dillo in un commento. Il secondo è la deriva del dialetto per ambiente: se il valore dovrà mai viaggiare attraverso un URL, un campo di un form, o una shell che rovina + e /, salva la grafia base64url invece, perché il set di caratteri è l'intero senso del formato.
File: l'andata e ritorno del .b64
Il lavoro del tipo "trasforma questo file in un file di testo .b64" è una lettura, una chiamata e una scrittura:
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)
// più tardi, magari in un altro processo
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")
}
Il trimmingCharacters nel viaggio di ritorno c'è perché chi ha scritto il file potrebbe aver aggiunto un terminatore di riga, e il decodificatore rigoroso tratta un a capo finale come un verdetto di nil. Quell'andata e ritorno torna byte per byte, e dovresti verificarlo la prima volta che lo metti in produzione. Per file abbastanza grandi da rendere interessante l'uso della memoria, non codificare l'intero buffer tutto in una volta. Il base64 ha una proprietà deliziosa che rende lo stream esatto: ogni tre byte in ingresso producono quattro caratteri di output indipendenti, quindi finché ogni chunk che codifichi è un multiplo di tre byte, l'output concatenato è identico a quello che si ottiene codificando l'intero file in un colpo solo. Rompi l'allineamento e l'output cambia, perché un confine di chunk taglia un gruppo di tre byte a metà 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)
}
}
La memoria al picco è un buffer di lettura più la riga corrente, non importa quanto sia grande il file, e l'output spezzato corrisponde esattamente alla grafia ottenuta in un colpo solo con .lineLength76Characters. La stessa regola del multiplo di tre, con i ruoli invertiti, è quella su cui poggia il decodificatore a stream dell'articolo collegato, quindi i due lati del viaggio condividono una verità aritmetica.
Contenuti grandi e il conto della memoria
Facciamo i conti che ti serviranno la prossima volta che qualcuno chiede "possiamo fare il base64 di questo?" Ogni tre byte in ingresso diventano quattro caratteri di output, quindi la dimensione si moltiplica per 4/3: un file da 100 kilobyte diventa una stringa di 133.336 caratteri, un file da 10 megabyte diventa 13.333.336 caratteri, e via dicendo. Il padding aggiunge al massimo due caratteri in fondo, un errore di arrotondamento per qualsiasi cosa di più di qualche byte, e l'input vuoto è l'unica esenzione, dove l'ufficio delle tasse concede un singolo passaggio gratuito e il risultato è la stringa vuota. Tre conseguenze pratiche. La prima: fai i conti prima di iniziare: se il tuo payload è già vicino a un limite (la zona comoda di circa 2.000 caratteri di un URL, il contratto di un campo JSON, la larghezza di una colonna di database), dividi il limite per 1.33 prima di codificare, non dopo (e per 1.37 quando è in gioco la spezzatura). La seconda: mentre impacchetti, tieni in mano i byte originali e la stringa impacchettata allo stesso tempo, quindi l'insieme di lavoro è circa 2,33 volte l'originale, e le funzioni a stream qui sopra sono la scappatoia quando quel numero smette di essere comodo. La terza: la tassa è in pratica a senso unico: la paghi quando impacchetti e i tuoi byte tornano a casa quando qualcuno lo decodifica, quindi la domanda vera non è mai "il base64 è costoso?" ma "la strada solo-per-testo su cui sono la richiede?".
Gli errori che mordono
- Il passo fallibile del set di caratteri.
String.data(using:)può risponderenil(prova.asciicon un carattere accentato), e farne il force-unwrap è l'upgrade classico di un input cattivo in un'app in crash. Proteggi la conversione, non solo la chiamata base64, che è la parte facile. - Lo stile di casa CRLF. Un'opzione
.lineLengthsenza un'opzione di a capo produce CRLF di default. Se il tuo formato vuole solo LF e hai dimenticato l'opzione, il tuo output porta dei carriage return che non avrebbe mai dovuto avere. - La trappola del solo-CR.
.endLineWithCarriageReturnda solo produce a capo solo-CR in stile Mac vecchio. Se intendevi CRLF (e per MIME lo intendi), passa entrambe le opzioni di a capo. - Spezzatura dentro il JSON. Un a capo grezzo dentro una stringa JSON è JSON non valido, punto. Base64 spezzato interpolato in un documento è un errore di sintassi con un accento base64. Tieni l'output spezzato nei body email e nei file di certificati.
- Il BOM soprassalitore. La conversione
.utf16semplice antepone un BOM di due byte che viaggia nel tuo output impacchettato e confonde i decodificatori che non se lo aspettavano. Usa.utf16LittleEndiano.utf16BigEndianquando ti serve UTF-16 senza il tag. - La deriva del dialetto. Standard e base64url sono alfabeti diversi, e lo RFC lo dice per iscritto. Un
+che sopravvive dentro una query string diventa uno spazio; un-che arriva a un decodificatore standard tollerante viene eliminato. Scegli il dialetto al confine e tienitelo. - Le maiuscole sono una lettera. L'alfabeto distingue
Adaa. Un copia-incolla con le maiuscole mescolate o una chiamata di uppercasing entusiasta corrompe silenziosamente i dati, perché entrambe le versioni passano comunque ogni controllo di alfabeto. Il base64 è sensibile alle maiuscole come un numero di passaporto. - La regola dell'allineamento. Gli encoder a stream devono tagliare i chunk sui multipli di tre byte. Un chunk non allineato cambia l'output, e il cambio è silenzioso: la stringa si decodifica comunque, sui dati sbagliati.
- Il doppio avvolgimento. Due livelli che entrambi codificano producono base64 di base64. Chi legge e decodifica una volta vede lettere dove dovrebbero essere byte, e l'incidente si scrive da solo.
- Il muro della disponibilità. Le nuove opzioni native (
.base64URLAlphabet,.omitPaddingCharacter) esistono sugli SDK beta più recenti e nella Foundation open-source dietro un marcatore di disponibilità, ma non su ogni toolchain che il tuo CI toccherà. Se le adotti, proteggi con controlli di disponibilità così che la stessa sorgente compili su Xcode più vecchi e su Linux. Sulla toolchain stabile attuale, l'estensione di quattro righe compila dappertutto dove le opzioni no. - Il base64 non è cifratura. Se il requisito è la riservatezza, hai scelto lo strumento sbagliato di un'intera categoria. Il lavoro del base64 è far viaggiare i byte, e fa esattamente quel lavoro, non di più.
Come spedirlo
- Codifica byte, non desideri. Decidi la forma in byte prima di chiamare il metodo, UTF-8 di default e nominata esplicitamente quando non lo è, e proteggi il passo fallibile
data(using:), perché è lì che i dati davvero vanno persi. - Non spezzato di default, spezzato per contratto. L'output semplice di una riga è corretto per JSON, API e la maggior parte dei database; rivolgiti alle opzioni di spezzatura 64/76 solo quando il formato ricevente le richiede, e paga entrambe le opzioni di a capo quando intendi CRLF.
- Un dialetto per confine. Base64 standard per le destinazioni centrate sul testo, base64url per tutto ciò che toccherà un URL o un nome di file, mai i due nello stesso documento. Scrivi la conversione una volta, dale un nome onesto, e riusala.
- Tieni conto del sovrapprezzo. Moltiplica per 4/3 prima di iniziare (per 1.37 quando la spezzatura è in gioco), e streama con chunk allineati a 3 byte quando il payload è abbastanza grande da rendere scomodo l'insieme di lavoro.
- Non usare il nastro da imballaggio come lucchetto. Se il requisito è il segreto, fermati allo scaffale del base64 e prendi la cifratura invece.
Una breve storia dell'imballaggio
L'alfabeto con cui impacchetti e le lunghezze di riga a cui spezzi sono fossili di quattro decenni di discussioni su quanto binario può sopravvivere a una strada solo per testo, e la posizione di Swift in quella storia è breve ma interessante:
- Anni '80, l'era della stessa macchina. I primi encoder di questa famiglia esistevano per spostare file via dial-up tra sistemi che assumevano che l'altro capo fosse una macchina come la loro. uuencode su UNIX usava lettere maiuscole, cifre e punteggiatura, e i suoi progettatori trovarono un trucco che risparmiava potenza di calcolo: l'alfabeto sta in posizioni ASCII consecutive, quindi codificare era letteralmente "aggiungi 32" senza tabella di ricerca. BinHex, il cugino nato sul TRS-80 nel 1981, saltò sull'Apple II, diventò il formato del Macintosh classico nel 1984, e fece una scommessa diversa: i suoi 64 caratteri omettono
7,O,W,g,oe quasi metà delle minuscole. - 1987, l'alfabeto riceve un indirizzo. RFC 989, la prima specifica Privacy-Enhanced Mail, standardizzò i 64 caratteri esatti che digiti oggi, spezzò l'output a 64 caratteri per riga, e usò
=per il padding e*per marcare i dati codificati ma non cifrati. Ogni blocco in stile PEM che hai mai incollato in una configurazione di server è un discendente di questo documento. - 1996, l'era liberale. MIME (RFC 2045) prese l'alfabeto per gli allegati email e spostò la spezzatura a 76 caratteri, aggiungendo la regola che rese la spezzatura sicura da produrre: i decodificatori devono ignorare gli a capo. Gli encoder impararono a spezzare; i decodificatori impararono a perdonare. L'opzione a 76 caratteri di Swift è un souvenir vivente di esattamente questa discussione.
- 2003-2006, le regole si induriscono. RFC 3548 (2003) dichiarò che il padding non deve essere saltato (a meno che un formato non dica diversamente) e che i decodificatori devono rifiutare i caratteri fuori dall'alfabeto; RFC 4648 (ottobre 2006) chiuse la famiglia e aggiunse l'alfabeto URL-safe, esplicitamente perché identificatori lunghi potessero vivere in URL senza percent-escaping di ogni carattere speciale. La convenzione "niente padding nel dialetto URL" nacque nello stesso documento, perché un carattere pad in un URL di solito diventa
%3D, vanificando lo scopo. - 2013-2014, l'API c'è già. La classe
NSDatadi Apple impacchettava base64 da anni, e l'API basata su opzioni con le quattro opzioni di spezzatura arrivò in iOS 7, nel 2013, prima che Swift esistesse anche solo come idea. Quando Swift 1.0 arrivò il 9 settembre 2014, ereditò un encoder totale con quattro opzioni di spezzatura e l'alfabeto di 64 lettere del 1987, e da allora la personalità non è più cambiata. - 3 dicembre 2015, la toolchain lascia l'edificio. Swift è diventato open-source quel giorno, e con lui il base64 della Foundation è passato su Linux e poi su Windows. "Codifica base64 in Swift lontano da una macchina Apple" ha appena una decina d'anni: un ospite molto giovane a una festa iniziata nel 1987.
- 2023-2026, la riscrittura e il dialetto URL. La riscrittura della Foundation (il progetto swift-foundation) ha spostato
Datain un nucleo Swift puro, e nel 2025 una proposta della community ha aggiunto opzioni native base64url e di omissione del padding. A data di stesura, gli SDK beta più recenti e la toolchain open-source distribuiscono le opzioni di codifica, il resto della famiglia è in maturazione nella Foundation open-source dietro marcatori di disponibilità, e l'estensione della community resta il ponte portatile nel frattempo.
Piccoli piaceri
- Un megabyte si impacchetta in esattamente 1.333.336 caratteri base64, la tassa del 4/3 più due caratteri di padding, al centesimo. L'unico input che sfugge completamente alla tassa è quello vuoto: niente in ingresso, niente in uscita.
- L'encoder è totale in un modo in cui il decodificatore non lo è. Non restituisce mai nil, non lancia mai, non rifiuta mai. L'unico fallimento in tutto il pipeline vive a monte, nel passo del set di caratteri, ed è per questo che il metodo sembra molto più tranquillo del suo cugino.
- Codifica la parola
hélloin UTF-8 e diventaaMOpbGxv; codificala in UTF-16 little-endian e diventaaADpAGwAbABvAA==. Stessa parola, due passaporti diversi, entrambi validi, nessuno intercambiabile. - La parola di prova del mondo base64 è
foobar, e si impacchetta inZm9vYmFy. Se hai mai visto un esempio di base64 nel mondo reale, c'è una buona probabilità che foobar ci fosse. - Il famoso GIF trasparente 1x1 è 42 byte e si apre con la parola magica
GIF89a, ed è per questo che il prefissoR0lGODlhcompare in più codebase sulla Terra di quasi ogni altra stringa base64. - Il tuo struct
Codableprobabilmente invia base64 da anni senza che tu te ne accorga: la strategiaDatadefault diJSONEncoderimpacchetta con base64 standard, ed è per questo che un campoDataattraversa la rete come una stringa con padding invece che come un array di numeri. - Il padding non supera mai due caratteri, mai. Un payload di 1 byte finisce con
==, un payload di 2 byte finisce con=, e un payload di 3 byte non finisce con nulla. L'intera grammatica dell'ultimo gruppo sta in un'unghia. - Swift ha 27 anni in meno dell'alfabeto con cui impacchetta. La lingua è uscita nel 2014; le 64 lettere sono state standardizzate nel 1987 e da allora non sono più cambiate.
Questa è l'intera cassetta degli attrezzi di imballaggio: un metodo totale, un passo fallibile che viene prima di lui, quattro opzioni di spezzatura con uno stile di casa CRLF, un'estensione base64url di quattro righe, una regola di allineamento a 3 byte per lo streaming, e un sovrapprezzo del 4/3 che è il prezzo d'ingresso alla strada solo per testo. La codifica è dove paghi il conto del base64, e ora conosci ogni voce prima di firmare. Nel momento in cui giri il viaggio e inizi ad aprire ciò che altri hanno impacchettato, le nil tornano, i verdetti sugli spazi bianchi e il punto cieco della manopola tollerante salgono sul palco. L'articolo collegato sulla decodifica conduce lo spettacolo completo su quella metà dell'andata e ritorno, quindi quando le lettere inizieranno ad arrivare, saprai già esattamente come aprirle.
Ultimo aggiornamento: 2026-09-08
Articolo correlato: Decodifica Base64 in Swift: una guida completa