Décodage Base64 en Swift : un guide complet
Quelque part dans votre pipeline, vos données portent un déguisement : un token glissé dans un en-tête HTTP, un avatar caché dans un champ JSON, un fichier .b64 que vous avez promis d'examiner la semaine dernière, une pièce jointe d'e-mail arrivée sous forme de mur de lettres. Enlever ces déguisements en Swift est l'un des travaux les plus agréables du langage : un framework, un initialiseur, et un règlement court au point de tenir sur un post-it.
La page d'accueil de ce site couvre déjà le format lui-même (64 caractères imprimables, six bits par caractère, au plus deux caractères = de padding dans le dernier groupe), alors nous ne raconterons pas cette histoire ici. Gardez simplement deux faits en poche. D'abord, le base64 est une façon de habiller des octets en texte, pas un cadenas. Ensuite, chaque trajet base64 en Swift passe par un seul type, Data, et le décodeur y vit sous la forme d'un initialiseur failable. Ce seul fait façonne le reste de cet article, car un initialiseur failable change la façon dont vous écrivez chacune des lignes qui suivent.
Un seul type détient tout le travail
Swift ne disperse pas ses aides base64 dans une douzaine de modules, et il ne vous oblige pas à installer quoi que ce soit. Le décodeur est Data(base64Encoded:options:) dans Foundation, et il fait partie de la plateforme depuis les premiers temps du framework (Apple liste l'initialiseur depuis iOS 8.0, macOS 10.10, tvOS 9.0, watchOS 2.0 et visionOS 1.0 ; les options de longueur de ligne côté encodage remontent même jusqu'à iOS 7.0). Sur Linux et Windows, le même Foundation est livré avec la toolchain open source, si bien que le code ci-dessous se comporte de la même façon dans une app iPhone, un worker serveur et un script dans votre terminal.
Il existe un initialiseur de la même famille, Data(base64Encoded: Data, options:), pour le cas où votre base64 arrive sous forme d'octets ASCII bruts au lieu d'une chaîne. Les deux prennent un argument options qui vaut [] par défaut. Et les deux partagent un trait de caractère qui compte plus que n'importe quelle option : ils sont failables.
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!
La documentation d'Apple pour l'initialiseur est étonnamment directe : il « renvoie nil lorsque l'entrée n'est pas reconnue comme du Base-64 valide ». Pas d'exceptions, pas d'erreurs levées, pas de spam de logs. Juste un nil tout discret et la responsabilité de décider ce que cela signifie pour votre utilisateur. Si vous ne retenez qu'une chose sur le base64 en Swift, que ce soit celle-ci : le décodeur ne plante jamais et ne se plaint jamais. Il décline tout simplement.
Le verdict du décodeur : une table de oui et de non
Alors, que veut dire « valide » pour ce décodeur ? Il s'avère que c'est une courte liste de règles strictes, et cette liste fait la différence entre « ça marche dans la démo » et « ça survit en production ». Chaque ligne du tableau ci-dessous est un comportement réel de l'initialiseur sur une toolchain actuelle, vous pouvez donc le citer tel quel dans vos messages d'erreur :
| Entrée | Verdict | Pourquoi |
|---|---|---|
TWFu |
Man |
un groupe complet de quatre caractères n'a besoin d'aucun padding |
TQ== |
M |
un octet plus deux pads, le cas du manuel |
SGVsbG8h |
Hello! |
huit caractères, c'est un multiple de quatre, donc aucun pad n'est nécessaire |
==== |
un Data vide |
du padding sans rien derrière est légal et se décode en zéro octet |
| la chaîne vide | un Data vide |
rien dans, rien dehors, et l'optional réussit quand même |
TQ |
nil |
longueur deux : un groupe de quatre était promis mais n'est jamais livré |
T |
nil |
un caractère porte six bits et un octet en veut huit |
SGVsbG8hTQ |
nil |
dix caractères : le groupe final pendouille sans ses pads |
TQ=== |
nil |
trois pads : le troisième n'a plus rien à rembourrer |
TQ==TQ |
nil |
des données après le padding, c'est un non catégorique |
SGVs bG8h |
nil |
un seul espace est hors alphabet, et le mode strict ne montre aucune pitié |
SGVsbG8h plus un retour à la ligne final |
nil |
le retour à la ligne en fin de fichier que vous venez de lire compte comme du bruit |
Trois lignes méritent un second regard. La ligne ==== veut dire qu'un contrôle if let passe et que votre code file son chemin avec zéro octet, alors si un payload vide n'est pas un état valide dans votre app, vérifiez le compteur juste après le décodage. La ligne chaîne vide est le même tour avec moins de maquillage. Et la ligne du retour à la ligne final est la raison la plus courante pour laquelle un fichier base64 qui s'est encodé parfaitement le matin refuse de se décoder l'après-midi : quelque chose a ajouté une fin de ligne en chemin, et le décodeur strict le prend personnellement.
Il y a aussi un point faible célèbre que le tableau ne peut pas montrer. Comparez TQ== et TS== : les deux se décodent en le même octet, M, parce que les deux bits les moins significatifs de ce caractère final sont jetés avant même d'être inspectés. Remplacez-le par Tg== et vous obtenez N sans même un débat. Le décodeur surveille les caractères et laisse passer les bits de traîne. Cette tolérance n'est pas un bug, mais cela veut dire que deux chaînes différentes peuvent désigner les mêmes données, et ça commence à compter dès que votre système compare, déduplique ou met en cache des valeurs base64 (on en reparle dans la section sécurité).
Quand l'entrée est plus bruyante que vous ne le pensez
Le base64 du monde réel arrive rarement en une ligne immaculée. Les pièces jointes d'e-mail sont coupées à 76 caractères par ligne avec un retour chariot et un retour à la ligne après chaque ligne, une habitude héritée de la spécification MIME de 1996, et les fichiers de certificat sont coupés à 64. Le décodeur a exactement une option pour gérer ce bruit, et c'est une grosse option :
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 est documenté comme un décodeur qui « ignore les octets non Base-64 inconnus, y compris les caractères de fin de ligne », et pour ce travail c'est l'outil approprié : le bruit est supprimé, l'alphabet survit, et le payload en sort intact. Mais l'option a un angle mort, et c'est celui qui mord le plus fort sur les développeurs Swift : elle supprime tous les caractères hors alphabet, y compris le - et le _ du base64url. Elle ne les traduit pas en + et / ; elle les jette tout simplement. Selon ce que cette suppression laisse derrière elle, vous obtenez un nil (quand les survivants ne forment plus de groupes entiers) ou, pire, une réponse sûre d'elle avec le mauvais nombre d'octets. Une chaîne base64url de 16 caractères qui encode 12 octets peut revenir du décodeur tolérant sous la forme de 9 octets différents, sans erreur et sans excuse.
La règle à retenir : .ignoreUnknownCharacters est fait pour le bruit de transport (retours à la ligne, espaces perdus d'un copier-coller), jamais pour les différences d'alphabet. Si le payload peut être du base64url, convertissez d'abord les caractères vous-même, exactement comme le montre la section suivante, et remettez au décodeur une chaîne standard propre.
L'alphabet URL
La section 5 de la RFC 4648 définit le cousin de l'alphabet standard que vous croisez depuis le début : le base64url, où + devient -, / devient _, et le padding = est habituellement retiré. La raison est la même que celle qui garde vos URL honnêtes : dans une chaîne de requête, un + est lu comme un espace par l'analyse des formulaires, un / est un séparateur de chemin, et un = sépare les clés des valeurs. La RFC est franche sur la relation entre les deux : la variante URL « ne doit pas être considérée comme identique à l'encodage base64 ». Les JWT, les messages Web Push, les identifiants de vidéos YouTube et la plupart des identifiants d'API modernes parlent base64url, alors attendez-vous à le croiser dès le premier jour.
Côté décodage, la recette a deux mouvements : traduire l'alphabet, puis compléter le padding, parce que le décodeur strict veut toujours son multiple de quatre.
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
}
La ligne modulo est tout le tour de passe-passe : les payloads base64url arrivent généralement sans padding, et un ou deux caractères = (jamais trois) restaurent le groupe de quatre que le décodeur attend. Vous trouverez une version de cette extension de cinq lignes dans un nombre surprenant de bases de code Swift, et pour de bonnes raisons. Il y a une raison pour laquelle elle sera plus courte à l'avenir : les SDK Apple les plus récents (26.4 et au-delà, à la date de cette rédaction) ont fait pousser une option native .base64URLAlphabet pour l'encodeur, avec les options de décodage correspondantes encore en maturation dans le Foundation open source, derrière un marqueur de disponibilité pour une toolchain ultérieure. Jusqu'à ce qu'elle atteigne votre cible de déploiement minimale, l'extension est la réponse portable, et elle continuera de fonctionner sur chaque version par construction.
D'abord les octets, ensuite les mots
Voici la décision que le décodeur ne peut pas prendre à votre place : il vous remet un Data, un sac d'octets sans la moindre idée du jeu de caractères dans lequel le payload d'origine était écrit. Si le payload était du texte, choisir ce jeu de caractères est votre travail, et Swift vous donne deux portes de sortie du monde des octets, aux tempéraments très différents.
String(data:encoding:)est la porte stricte. Elle renvoie un optional et répondnilquand les octets ne sont pas valides dans l'encodage que vous avez nommé. Idéale pour la validation, dangereuse si vous forcez le déballage de la réponse.String(decoding:as:)est la porte jamais de refus. Elle renvoie toujours une chaîne, en remplaçant par le caractère de remplacement U+FFFD tout ce qu'elle n'arrive pas à comprendre. Idéale pour les logs et les aperçus, dangereuse si vous stockez le résultat et l'appelez des données.
import Foundation
let bytes = Data([0xC3, 0xA5]) // l'orthographe UTF-8 de la lettre a avec un rond
print(String(data: bytes, encoding: .utf8) ?? "?") // a avec un rond, lu correctement
print(String(data: bytes, encoding: .isoLatin1) ?? "?") // deux lettres embrouillées, mêmes octets
print(String(decoding: bytes, as: UTF8.self)) // a avec un rond, et ça ne plante jamais
La recette qui couvre presque tout : essayez d'abord l'UTF-8 strict, car c'est ce que les API modernes veulent presque toujours ; retombez sur l'ISO Latin-1 seulement quand le contrat se tait et que vous préférez du lisible mais faux au silence ; réservez la porte jamais de refus aux sorties de debug. Et un intrus invisible à vérifier : si le payload commence par un BOM UTF-8 (les trois octets EF BB BF), la conversion stricte le garde, et votre chaîne commence désormais par un caractère U+FEFF invisible qui casse tranquillement les contrôles d'égalité et les allers-retours JSON. Retirez-le avec un contrôle de préfixe quand la spécification n'en promet pas.
Ouvrir des fichiers
Le travail « il y a un fichier .b64, donne-moi ce qu'il cache » est une lecture, un nettoyage, un décodage et une écriture. Le nettoyage n'est pas de la décoration ; c'est la différence entre un fichier qui s'ouvre et un fichier qui renvoie nil, parce que les outils, les clients de messagerie et les éditeurs adorent tous laisser un retour à la ligne à la fin :
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")
}
Si le fichier est enveloppé en MIME (retours à la ligne tous les 76 caractères), vous avez deux échappatoires propres : décodez avec .ignoreUnknownCharacters et laissez l'option dévorer les fins de ligne, ou retirez-les vous-même avec replacingOccurrences avant un décodage strict. Les deux tiennent en une ligne chacun. Pour les fichiers qui sont simplement grands, décodez par groupes alignés au lieu de tout lire : chaque groupe de quatre caractères se décode tout seul, alors vous n'avez à transporter qu'un groupe en cours plus un petit reste à chaque frontière de lecture.
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)
}
}
La mémoire reste plate, peu importe la taille du fichier : un tampon de lecture, un fragment de reste, et le résultat que vous construisez. La même boucle gère un téléchargement qui arrive en base64 sur le câble, un fichier de log qui est en réalité un flux encodé, ou n'importe quel payload trop gros pour tenir dans la main.
JWT : lire les trois points
Un JSON Web Token compact est trois parties base64url jointes par des points, et les deux premières de ces parties sont du JSON tout simplement en trench-coat. Elles arrivent sans padding, ce qui est exactement la combinaison que le décodeur strict rejette d'un coup d'œil, si bien que votre aide dataFromBase64URL() de la section URL fait tout le travail de force :
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"}
Deux rappels à garder en tête. Un JWT est signé, pas chiffré : l'en-tête et le payload sont des informations publiques, c'est exactement pourquoi un mot de passe n'y a jamais sa place (le cousin chiffré, JWE, est une spécification entièrement différente). Et la troisième partie séparée par des points est une signature cryptographique, pas un document, alors décodez les parties un et deux, et laissez le reste tranquille.
Data URIs : le fichier derrière la virgule
Les API web adorent cacher du binaire dans du texte avec le schéma data: : un PNG dans un champ de profil, une police dans un blob CSS, un code QR dans un fichier de configuration. Le format est data:{mime};base64,{payload}, et décoller le payload n'est qu'un split de loin :
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")
}
L'exemple utilise le célèbre GIF transparent de 42 octets, la plus petite image du format, si bien que ses caractères d'ouverture apparaissent dans plus de bases de code que presque n'importe quelle autre chaîne base64 d'internet. Sur les plateformes Apple, le pipeline se termine en une ligne : le même Data que vous venez de décoder est envoyé tel quel dans UIImage(data:) ou NSImage(data:), et c'est pourquoi « afficher un avatar venant d'une API » est une petite fonctionnalité et pas un projet.
HTTP : l'en-tête Basic et ses amis
L'ancien en-tête Authorization: Basic est un nom d'utilisateur et un mot de passe, joints par deux-points, emballés en base64 standard pour le voyage (pas le dialecte URL : celui-ci vit dans un en-tête où + et / sont parfaitement inoffensifs). Le déballer, c'est un split et un décodage :
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")
}
Gardez la note de bas de page sécurité bien audible, parce qu'elle s'applique à chaque base64 que vous croiserez : c'est de l'emballage, pas de la protection. L'authentification Basic n'est acceptable que sur HTTPS, où c'est le TLS qui fait la garde et où le base64 se contente d'empêcher les octets de casser la grammaire de l'en-tête. Le même raisonnement explique les tokens Authorization: Bearer : le token lui-même est un JWT, donc la recette de décodage de la section JWT s'applique à lui sans changement.
E-mail : l'habitude des 76 caractères
Une pièce jointe d'e-mail encodée en base64 est coupée à 76 caractères avec des fins de ligne CRLF, exactement le bruit pour lequel existe l'option tolérante. Les en-têtes MIME bruts vous disent quel alphabet et quel découpage l'expéditeur a utilisés (Content-Transfer-Encoding: base64), et le correctif est un seul drapeau :
import Foundation
let attachment = "VGhpcyBhdHRhY2htZW50IHN1cnZpdmVk\r\nIHRoZSA3Ni1jaGFyYWN0ZXIgaGFiaXQu"
if let data = Data(base64Encoded: attachment, options: .ignoreUnknownCharacters) {
print(String(data: data, encoding: .utf8) ?? "")
}
// This attachment survived the 76-character habit.
Si vous construisez l'envoi d'e-mails plutôt que leur lecture, souvenez-vous que le découpage à 76 caractères a aussi un coût : avec un retour à la ligne tous les 76 caractères, le texte encodé atterrit aux alentours de 137 pour cent de la taille d'origine, ce qui explique pourquoi les vieux ingénieurs du mail estimaient la taille des pièces jointes d'un coup d'œil avec le raccourci « multipliez l'original par 1,37, ajoutez environ 800 octets d'en-têtes ». Le chiffre est du folklore maintenant, mais l'arithmétique est toujours la bonne.
Le payload doublement enveloppé
Le ticket « mes données sont corrompues » le plus courant dans le monde du base64 est des données qui ont été emballées deux fois : une couche d'intégration les a encodées, et une seconde couche qui n'a jamais lu la documentation a encodé le résultat. Le geste défensif est de décoder une fois, regarder ce que vous obtenez, et si le résultat est lui-même une chaîne propre qui a l'air du base64 (bonne longueur, bon alphabet, rien de surprenant), la décoder une fois de plus, délibérément, et s'arrêter. N'écrivez pas une boucle qui décode jusqu'à l'échec. Une telle boucle avale gaiement un fichier parfaitement bon dont le contenu ressemble juste à du base64, et une fois qu'elle a tourné, plus personne ne peut dire où commençaient les données d'origine.
import Foundation
func unwrapOnce(_ packed: String) -> Data? {
let cleaned = packed.trimmingCharacters(in: .whitespacesAndNewlines)
return Data(base64Encoded: cleaned)
}
let suspicious = "WVdKag==" // a déjà l'air d'être emballé
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
Deux déballages, deux décisions conscientes, et un payload qui n'est finalement plus que abc.
Faire dire quelque chose au nil
Comme le décodeur répond nil au lieu de lever une exception, le style de gestion d'erreurs de votre code base64 est un choix que vous faites, et le choix dont vous vous réjouirez plus tard est un petit wrapper qui transforme le refus silencieux en une erreur bruyante et précise :
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
}
Le wrapper devient aussi le seul endroit où vit la normalisation : le nettoyage, toute conversion d'alphabet, tout complément de padding. Les appelants obtiennent une fonction, un sens pour l'échec, et pas un ! à l'horizon. Le force-unwrap de Data(base64Encoded:)!, c'est ainsi qu'un mauvais payload devient une app plantée, et le wrapper est l'assurance pas chère contre ça. Le même motif fonctionne en ligne de commande, où un script avec CommandLine.arguments et une écriture FileHandle fait de « décoder ce fichier depuis le shell » un utilitaire de cinq lignes au lieu d'un détour copier-coller par un site web.
La sécurité, mesurée en octets
- Ce n'est pas du chiffrement. Le base64 est un réemballage réversible et lisible instantanément. Si votre modèle de menace inclut un humain avec un navigateur et cinq secondes, vous avez zéro protection, et chaque en-tête JWT le prouve chaque jour.
- Normalisez avant de comparer. Parce que
TQ==etTS==se décodent en les mêmes octets, deux systèmes peuvent détenir des orthographes différentes des mêmes données. Un article de 2022, « La malléabilité du base64 en pratique », a documenté ce que cette garantie d'unicité cassée fait dans la nature : des logs qui ne correspondent plus, des attaques par déni de service, et des entrées de base de données dupliquées. Si votre app Swift met en cache, déduplique ou compare des valeurs base64, exécutez un décodage canonique (ou un ré-encodage canonique) à l'entrée. - Limitez l'entrée avant le décodage. Décoder N caractères alloue environ les trois quarts de N octets, pendant que vous tenez toujours la chaîne d'entrée. Un client hostile peut envoyer 100 mégaoctets de la lettre
Aet regarder votre mémoire grimper avant que le décodeur n'ait dit non. Vérifiez d'abord la longueur, ce qui coûte peu, et rejetez ce qui est trop gros. - Méfiez-vous de l'option tolérante en filtre.
.ignoreUnknownCharacterssupprime des caractères. Un passage « d'assainissement » par elle peut transformer un payload base64url valide en des données différentes, sans erreur. C'est un filtre de bruit pour les retours à la ligne, pas un validateur. - Tenez-le hors des URL quand vous pouvez. De gros payloads base64 dans des chaînes de requête ou des chemins dépassent les longueurs d'URL confortables et se font massacrer par les proxys. Mettez-les plutôt dans des corps de requête, des fichiers ou des tokens.
La performance, brièvement
Le décodeur est une marche dans une table de correspondance : chaque caractère est indexé dans une petite table, et quelques bits sont décalés puis oués dans les octets de sortie. Sur une toolchain actuelle, c'est largement assez rapide pour tout ce qui tient en mémoire, et le chiffre à retenir est le ratio de sortie : les octets décodés font environ les trois quarts de la longueur d'entrée, alors une chaîne de 4 mégaoctets vous coûte environ 3 mégaoctets de résultat en plus de la chaîne que vous tenez déjà. Si vous êtes sur un chemin où le Foundation lui-même n'est pas autorisé (une cible très embarquée, un bundle WebAssembly), le package communautaire swift-extras-base64 est l'alternative notable : du Swift pur sans dépendance à Foundation, un encodeur et un décodeur conformes à la RFC 4648 avec options base64url et padding, et des benchmarks qui le placent plusieurs fois plus rapide que Foundation. Une implémentation antérieure du même package est même livrée dans le support WebSocket de swift-nio, ce qui est aussi proche du grade de production qu'un projet de côté puisse l'être. Pour une app ou un script ordinaire, c'est un bagage inutile ; pour le coin contraint de Swift, c'est la réponse standard.
Une décennie de déballage
Swift n'a rien inventé de tout cela, et il est utile de savoir d'où vient chaque pièce de la boîte à outils :
- Années 1980, l'ère de la même machine. Les premiers encodages de cette famille (uuencode sur UNIX, BinHex sur le TRS-80 et le Mac classique) faisaient circuler des fichiers entre des machines qui supposaient que l'autre bout leur ressemblait. uuencode utilisait un alphabet de majuscules, de chiffres et de ponctuation, et ses lettres sont à des positions ASCII consécutives, si bien que l'encodage consistait à ajouter 32, sans aucune table de correspondance. Les décodeurs de cette époque pouvaient supposer beaucoup de choses, et dès que les données traversaient les écosystèmes, tout s'effondrait.
- 1987, l'alphabet obtient une adresse. La RFC 989 (Privacy-Enhanced Mail, février 1987) a standardisé l'alphabet de 64 caractères, coupé les lignes à exactement 64 caractères, et utilisé
=pour le padding et*pour marquer les données encodées mais non chiffrées. Chaque bloc de type PEM est un descendant de ce document. - 1996, l'ère libérale. Le MIME (RFC 2045) a pris l'alphabet pour l'e-mail, déplacé le découpage à 76 caractères, et dit aux décodeurs conformes d'ignorer tout caractère en dehors de l'alphabet, comme les retours à la ligne CRLF. C'est l'ère qui a entraîné une génération à attendre des décodeurs indulgents, et le défaut strict de Swift brise délibérément les attentes de cette ère.
- 2003 à 2006, les règles se durcissent. La RFC 3548 (2003) a porté un premier coup pour unifier la famille ; la RFC 4648 (octobre 2006) a tranché, codifié les règles de padding, et ajouté l'alphabet URL-safe. Son paragraphe sur les décodeurs est celui que Swift suit : rejeter les caractères hors alphabet, sauf si le format que vous servez dit explicitement de les ignorer, comme le MIME.
- 2013 à 2014, l'API attend en coulisses. La classe
NSDatad'Apple emballait et déballait du base64 depuis des années, et l'API basée sur des options avec son option de décodage est arrivée dans iOS 7, en 2013, un an avant l'existence de Swift. Quand Swift 1.0 est sorti le 9 septembre 2014, le décodeur est entré avec le langage et a gardé la même personnalité depuis : cœur strict, une molette tolérante, initialiseur failable. - 3 décembre 2015, Linux obtient un décodeur. Swift est passé en open source ce jour-là, et avec lui le base64 de Foundation a traversé vers Linux et, plus tard, Windows. « Décoder du base64 en Swift sur une machine non Apple » a à peine une décennie : un invité en retard à une fête qui a commencé en 1987.
- 2023 à 2026, la réécriture. La réécriture de Foundation (le projet swift-foundation) a déplacé
Datadans un cœur en Swift pur, et en 2025 une proposition communautaire a ajouté des options natives base64url et d'omission du padding. À la date de cette rédaction, les bêta des SDK les plus récents et la toolchain open source livrent les options d'encodage, tandis que les options de décodage sont encore en maturation dans la toolchain open source, si bien que les extensions faites maison restent la réponse universelle en attendant.
Petites merveilles
====est une entrée légale. Quatre pads et aucune donnée se décodent en unDatavide, la seule chaîne base64 dont tout le contenu est « il n'y a rien ici », et Swift est d'accord avec elle.- La police des caractères du décodeur ne vérifie pas le travail de la police des bits :
TS==etTQ==vous remettent tous les deuxM, tandis queTg==vous remetN. Même grammaire, bits différents, aucune question posée. - Le
Datade Swift peut décoder du base64 arrivé sous forme d'octets plutôt que de chaîne, via la varianteData(base64Encoded: Data), si bien qu'un payload qui a traversé le câble en ASCII peut sauter entièrement l'aller-retour par la chaîne. - Le mot qui est base64 depuis la naissance des vecteurs de test est
foobar, et son encodage estZm9vYmFy. Si vous avez déjà vu un exemple de base64 dans la nature, il y a de bonnes chances que foobar soit de la partie. - Le célèbre GIF transparent 1x1 fait exactement 42 octets et s'ouvre avec le mot magique
GIF89a, c'est pourquoi ses huit premiers caractères encodés apparaissent dans plus de bases de code que presque n'importe quel autre préfixe base64 sur Terre. - Le décodeur open source moderne fait son contrôle de caractère invalide avec une seule comparaison : il combine en OU logique quatre valeurs de table par position et teste le résultat contre un sentinel, si bien qu'une seule branche décide du sort d'un groupe entier de quatre caractères. L'implémentation plus ancienne faisait le même travail avec une table de 128 octets où toute valeur à 0x80 ou au-delà signifiait « pas une lettre ».
- Les BOM UTF-8 sont invisibles :
EF BB BFau début d'un payload devient un caractère U+FEFF qui survit à la conversion stricte, puis casse des contrôles d'égalité quelques lignes de code plus loin. - Swift a 27 ans de moins que l'alphabet qu'il décode. Le langage est sorti en 2014 ; les 64 lettres qu'il manipule ont été standardisées en 1987 et n'ont pas changé depuis.
Voilà la boîte à outils de décodage complète : un initialiseur failable avec un règlement court, une molette tolérante avec un angle mort documenté, une aide base64url de cinq lignes, une décision de charset qui vous appartient, une boucle en blocs pour les gros fichiers, et un wrapper qui fait dire quelque chose au nil. Le décodage, c'est là que le base64 mord, et vous connaissez maintenant le nom de chaque dent. Quand le travail se retourne et que vous commencez à emballer des octets pour le voyage au lieu de les déballer, la majoration d'environ 33 pour cent prend le relais et les options de découpage apparaissent. L'article d'encodage lié couvre cette moitié de l'aller-retour en entier, alors rendez-vous là-bas quand vous serez prêt à expédier dans l'autre direction.
Dernière mise à jour : 2026-09-08
Article associé : Encodage Base64 en Swift : un guide complet