Vous avez à traiter le format Base64 ? Alors ce site est parfait pour vous ! Utilisez notre outil en ligne super pratique pour encoder ou décoder vos données.

Encodage Base64 en Swift : un guide complet

Vous avez quelque chose qui doit voyager, et la route n'accepte que du texte : une API JSON qui refuse les octets bruts, un canal e-mail qui se souvient de ses origines sur 7 bits, une URL qui étouffe devant tout ce qu'elle ne peut pas nommer, un fichier de configuration qui n'accepte que les caractères les plus simples. Bienvenue dans le côté emballage du base64, où Swift transforme vos octets en un mur amical de lettres avec un seul appel de méthode, une majoration d'environ un caractère pour tous les trois octets, et quelques options de découpage qui existent parce que deux décennies différentes avaient un avis sur la longueur des lignes.

La page d'accueil de ce site explique déjà le format en détail (64 caractères imprimables, quatre pour chaque trio d'octets d'entrée, au plus deux caractères = de padding sur le dernier groupe), alors le cours sur le format est terminé avant même de commencer. Deux faits à emporter dans cet article : le base64 est de l'emballage, pas un cadenas, et l'emballage gonfle vos données d'environ 33 pour cent, ce qui compte chaque fois que vous êtes près d'une limite de taille. En Swift, tout le travail passe par un seul type, Data, et une seule méthode totale, base64EncodedString(options:). La seule vraie compétence requise est de savoir ce qui se passe dans les deux étapes autour de cette méthode, parce que la méthode elle-même n'échoue jamais. Ce sont les étapes qui échouent.

Une méthode, zéro excuse

Tout ce qui concerne le base64 en Swift vit sur Data du framework Foundation, et c'est le cas depuis les premières versions du langage (Apple liste la méthode depuis iOS 8.0, macOS 10.10, tvOS 9.0, watchOS 2.0 et visionOS 1.0). Le pipeline est toujours composé des mêmes trois étapes : mettez votre contenu dans un Data, appelez la méthode, expédiez la chaîne.

import Foundation

let note = "Pack it, wrap it, ship it."
let packed = Data(note.utf8).base64EncodedString()
print(packed) // UGFjayBpdCwgd3JhcCBpdCwgc2hpcCBpdC4=

Deux détails dans ces trois lignes méritent un regard de plus près. D'abord, Data(note.utf8) est l'étape discrète : la vue utf8 peut représenter chaque scalaire Unicode par définition, alors elle n'échoue jamais, ce qui explique pourquoi c'est le choix par défaut dans la plupart des exemples. Le cousin failable, note.data(using:), peut et sait répondre nil pour certains encodages, et toute cette décision de « quels octets » a sa propre section plus bas, parce que c'est le premier endroit où vos données peuvent disparaître. Ensuite, la méthode elle-même est totale : elle répond toujours, elle n'a pas de cas d'erreur, et la seule chose qu'elle vous demande, c'est le découpage de ligne que vous voulez. Il y a aussi un frère, base64EncodedData(options:), qui renvoie le résultat emballé en Data d'octets ASCII au lieu d'une chaîne, pour les pipelines où l'étape suivante est une API binaire plutôt qu'un champ texte.

Et parce que la moitié d'entre vous est arrivée via « mon app Swift a besoin d'une dépendance base64 » : il n'y a rien à installer. Le base64 fait partie de Foundation, Foundation fait partie de la toolchain, et la toolchain arrive de la même façon sur chaque plateforme. Sur macOS, c'est Xcode ou les outils ligne de commande ; sur Linux et Windows, c'est l'installateur de swift.org, où la branche stable actuelle à la date de cette rédaction est la 6.3.x et le gestionnaire de versions Swiftly est la porte d'entrée recommandée ; et les images Docker officielles couvrent la tribu des conteneurs. Votre Package.swift reste vide, et il devrait.

La première vraie décision : quels octets ?

Avant même qu'un seul caractère base64 soit produit, vous avez déjà pris la décision qui compte le plus, parce que le base64 emballe des octets, et une chaîne n'est qu'une chaîne tant que vous n'avez pas choisi sa forme en octets. L'UTF-8 est le défaut raisonnable et la bonne réponse pour presque tout, mais dès que vos données viennent d'un système legacy, d'un protocole binaire ou d'un coin de l'Unicode, le choix cesse d'être invisible :

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==
Conversion Octets pour « héllo » Ce que le base64 porte
.utf8 6 aMOpbGxv, la notation que les API modernes attendent
.ascii échoue avec nil le caractère accentué se situe au-dessus de 0x7F, et l'ASCII le refuse
.utf16 12 le double de la taille de l'UTF-8, plus un marqueur d'ordre des octets de deux octets qui voyage en tête
.utf16LittleEndian 10 le même mot sans l'étiquette BOM : 10 octets, toujours le plus lourd des options sans BOM listées ici

Trois leçons se cachent dans cette sortie. La forme data(using:) peut échouer, et .ascii est le candidat idéal à l'échec, alors le force-unwrap est le moyen classique de transformer une phrase parfaitement saine en app plantée. La conversion .utf16 simple préfixe un marqueur d'ordre des octets de deux octets (FF FE sur une machine little-endian), et ce BOM voyage dans votre sortie emballée, embrouillant tout décodeur qui ne l'attendait pas. Et le calcul de taille est impitoyable : un choix de charset malavisé vous coûte la taxe base64 sur le double des données, alors la question n'est jamais « ça va s'encoder ? » mais « que l'autre bout s'attend-il à trouver quand il déballera ? » La règle d'or : les deux bouts du voyage doivent s'accorder sur la forme en octets avant que le base64 commence, parce que le décodeur n'a aucun moyen de deviner ce que vous avez choisi, et il ne posera pas la question.

Le découpage : deux habitudes, un paramètre

Toutes les options de la méthode concernent le retour à la ligne, et toutes existent parce que deux formats du XXe siècle ne pouvaient pas s'entendre sur la longueur qu'une ligne de lettres doit avoir. Le MIME, le standard e-mail de 1996, coupe le base64 à 76 caractères avec des fins de ligne CRLF. Le PEM, la lignée Privacy-Enhanced Mail de 1987, coupe à 64 caractères, et c'est la forme que vous trouvez dans les certificats et les clés, les blocs -----BEGIN CERTIFICATE----- que vos serveurs gardent dans un répertoire de configuration.

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 caractères sur une seule ligne
print(pemStyle.components(separatedBy: "\n").count)    // 7 lignes de 64 caractères au maximum
print(mimeStyle.components(separatedBy: "\r\n").count) // 6 lignes de 76 caractères au maximum
Option Rôle Attention
.lineLength64Characters couper la ligne après 64 caractères, l'habitude PEM la fin de ligne est CRLF sauf si vous dites le contraire
.lineLength76Characters couper la ligne après 76 caractères, l'habitude MIME même défaut CRLF
.endLineWithCarriageReturn inclure un retour chariot dans la fin de ligne seul, c'est du CR uniquement, style vieux Mac, et c'est rarement ce que vous voulez
.endLineWithLineFeed inclure un retour à la ligne dans la fin de ligne passez les deux options si vous voulez dire CRLF

Maintenant le défaut qui surprend : demandez n'importe quelle option .lineLength sans choisir de fin de ligne, et la fin de ligne que vous recevez est CRLF, la paire complète retour chariot plus retour à la ligne. La méthode a son style maison, et ce style maison est 1996. Vous voulez du LF uniquement ? Payez-le explicitement avec .endLineWithLineFeed et rien d'autre. Une règle maison de plus pour la route : la dernière ligne ne reçoit jamais de fin de ligne traînante. Un résultat découpé se termine par son dernier caractère de donnée ou par ses pads =, quelles que soient les options choisies, alors vous pouvez concaténer et coller sans ligne vide orpheline à la fin. Et sans aucune option, la sortie est une seule ligne non rompue, c'est la bonne forme pour les corps JSON, les URL et les payloads d'API : le travail qu'une app Swift moderne fait en réalité le plus souvent.

Base64url : une chaîne qui peut voyager

L'alphabet standard est un bon citoyen du JSON et un mauvais citoyen de l'URL. Dans une chaîne de requête, + est lu comme un espace par l'analyse des formulaires, / est un séparateur de chemin, et = sépare les clés des valeurs, voilà pourquoi l'encodage en pourcentage de l'alphabet standard le rend plus long et plus laid au lieu de le raccourcir. La section 5 de la RFC 4648 existe pour corriger exactement ça : l'« alphabet sûr pour URL et noms de fichiers », où + devient -, / devient _, et le padding = est habituellement retiré, parce qu'un pad dans une URL devient typiquement %3D, ce qui tue l'intérêt. La RFC ajoute un avertissement qui mérite un cadre : cet encodage « ne doit pas être considéré comme identique à l'encodage base64 ». Les identifiants de vidéos YouTube, les JWT et la plupart des identifiants d'API modernes le parlent, alors attendez-vous à l'utiliser.

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

Regardez bien cette sortie : ce payload en particulier n'a pas produit de + ni de /, alors les deux notations ne diffèrent que par le padding retiré. Changez un octet et elles divergeront aussi dans l'alphabet, ce qui est tout le but. Deux règles d'engagement. Choisissez le dialecte une seule fois, à la frontière où vos données rencontrent le monde extérieur, et ne mélangez jamais les alphabets dans le même document : un décodeur standard qui reçoit du base64url (ou l'inverse) rejettera l'entrée ou, en modes tolérants, supprimera les caractères étrangers et vous remettra de faux octets. Et nommez votre aide honnêtement, pour que le prochain développeur sache que la chaîne est du base64url et pas une coquille. La même extension peut raccourcir sur des toolchains plus récentes : les bêta des SDK les plus récents incluent désormais une option native .base64URLAlphabet qui fait le changement d'alphabet à l'intérieur du framework, avec une option .omitPaddingCharacter correspondante, et le Foundation open source porte les mêmes options derrière un marqueur de disponibilité pour des toolchains ultérieures. Jusqu'à ce qu'elles atteignent votre cible de déploiement minimale, l'extension de quatre lignes est la réponse portable, et elle continuera de fonctionner sur chaque plateforme par construction.

JSON et API : le base64 que vous n'avez jamais demandé

Celle-ci surprend le plus de gens qui travaillent avec Codable, alors elle mérite sa propre section : la stratégie par défaut de JSONEncoder pour une propriété Data est déjà du base64. Si un struct Codable a un champ Data, l'encodeur l'emballera automatiquement en base64 standard, et JSONDecoder le déballera automatiquement au retour. Pas d'option, pas de configuration, pas de cérémonie.

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'icône a traversé le réseau sous la forme "8J+QsQ=="

La propriété icon a traversé le réseau sous la forme 8J+QsQ==, parce que c'est le style maison. Il existe des alternatives, et les deux que vous rencontrerez vraiment sont .custom, qui vous remet les données et un encodeur et vous laisse décider de la représentation, et le plus récent .deferredToData, qui délègue à l'instance de données elle-même. Le moment où une API veut du base64url plutôt que du standard, c'est .custom qui devient l'endroit où se branche votre extension de la section précédente :

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'icône a traversé le réseau sous la forme "8J-QsQ"

Il y a un avertissement qui sépare une fonctionnalité qui marche d'un incident de production : une chaîne JSON ne peut pas contenir de retour à la ligne brut. Si vous découpez un payload avec une option .lineLength et que vous interpolatez le résultat dans un document JSON sans l'échapper, vous n'avez pas fait de valeur JSON ; vous avez fait une erreur de syntaxe à l'accent base64, et le parser le prouvera. La sortie découpée est pour les corps d'e-mail et les fichiers de certificat. Tout ce qui vit dans du JSON, des URL ou des chaînes de requête reçoit la chaîne simple, sans découpage.

Data URIs : l'image dans une chaîne

Le tour préféré du web est d'incorporer les octets d'un fichier directement dans une URL : data:{mime};base64,{payload}. En construire un en Swift, c'est une lecture, un encodage et une concaténation de chaînes :

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'exemple reconstruit le célèbre GIF transparent de 42 octets (il en existe de plus petits non transparents, mais c'est celui que tout le monde incorpore) en une data URI qu'un navigateur affichera sans seconde requête. Sur les plateformes Apple, le sens inverse tient en une ligne : le même Data que vous avez emballé est envoyé tel quel dans UIImage(data:) ou NSImage(data:). Le prix à payer, c'est la taille, et elle s'accumule : une image de 100 kilo-octets devient une chaîne de plus de 133 000 caractères avant même d'ajouter le préfixe data:image/png;base64,. Les data URIs brillent pour les icônes, les avatars et les petits assets, et elles gonflent discrètement la bande passante des photos héroïques, alors gardez-les pour les petites choses.

JWT : sceller les deux premières parties

Le côté encodage d'un JSON Web Token, c'est deux scellés plus une signature, et le scellé est votre extension base64url avec le padding retiré, exactement ce que le format exige. L'en-tête et le payload sont des documents JSON, et les deux parties reçoivent le même traitement :

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

Deux rappels. La troisième partie séparée par des points est une signature cryptographique calculée sur les deux premières, et c'est la seule partie du token qui offre une quelconque garantie : l'en-tête et les claims sont du JSON tout simplement en trench-coat, alors les secrets n'y vont jamais. Et notez comme le padding disparaît dans seal() : les décodeurs JWT de l'autre bout (dont celui de l'article sœur) le remettent en place avec un complément modulo, si bien que les deux directions du voyage se rencontrent sur un terrain commun.

En-têtes HTTP : Basic et le reste

L'ancien en-tête Authorization: Basic veut un nom d'utilisateur et un mot de passe, joints par deux-points, emballés en base64 standard, parce que dans un en-tête + et / sont inoffensifs et la question du dialecte ne se pose pas :

import Foundation

let credentials = "editor:s3cret"
let header = "Basic " + Data(credentials.utf8).base64EncodedString()
print("Authorization: " + header)
// Authorization: Basic ZWRpdG9yOnMzY3JldA==

Même grande note de bas de page qu'un peu partout : l'emballage ne fournit zéro sécurité en lui-même, et l'en-tête est aussi sûr que la connexion HTTPS qui le transporte. Le cousin moderne, Authorization: Bearer, porte un JWT à la place, alors c'est la recette de scellement de la section JWT qui passe sur le câble. Le seul endroit où la question du dialecte se pose vraiment en HTTP, c'est la chaîne de requête : si votre API laisse un identifiant voyager dans une URL, cet identifiant devrait être du base64url, ou au minimum du base64 standard encodé en pourcentage, jamais l'alphabet standard brut avec son + laissé à être lu comme un espace.

Pièces jointes d'e-mail : le contrat des 76 caractères

Quand votre app produit une pièce jointe qui doit survivre aux origines sur 7 bits du SMTP, le contrat est celui du MIME : base64 découpé à 76 caractères avec des fins de ligne CRLF, et un en-tête Content-Transfer-Encoding: base64 qui dit au receveur à quoi s'attendre. La section sur les options a déjà montré l'orthographe ; voici la forme complète d'un corps découpé :

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 lignes
print(lines.map { $0.count }.max() ?? 0) // 76, la plus longue
print(body.hasSuffix("\r\n"))           // false, la dernière ligne reste nue

La facture de taille de ce dialecte est la fameuse : la taxe d'alphabet 4/3 plus un retour à la ligne tous les 76 caractères atterrit aux alentours de 137 pour cent de l'original, et le raccourci de l'ingénierie du mail d'antan, « multipliez l'original par 1,37 et ajoutez environ 800 octets d'en-têtes », marche toujours pour estimer la taille des pièces jointes dans un client de messagerie. C'est du folklore avec de l'arithmétique correcte, et c'est le seul endroit de cet article où la majoration de 33 pour cent prend un deuxième chiffre après la virgule.

Config, environnement et bases de données : cacher le tiret bas

Il y a une classe discrète de travaux où la seule vertu du base64 est que sa sortie est un petit jeu de caractères prévisible : ranger un blob binaire ou une valeur structurée dans un endroit qui veut du texte simple. Des variables d'environnement qui doivent survivre à un fichier de configuration shell, des colonnes dans une base de données qui préfère le varchar au blob, un fichier LDAP avec son marqueur base64, un code QR qui scanne les lettres plus fiablement que les bits. Le motif est le même partout : décidez les octets, encodez, stockez la chaîne, décodez à l'autre bout.

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)
}

Deux pièges vivent ici. Le premier est le double emballage : deux couches d'intégration qui encodent toutes les deux avec « gentillesse », si bien que la valeur que vous stockez est du base64 de base64, et le lecteur qui décode une fois obtient un mur de lettres et croit que la fonctionnalité est cassée. Encodez exactement une fois, à exactement une frontière, et dites-le dans un commentaire. Le second est la dérive de dialecte par environnement : si la valeur doit un jour voyager à travers une URL, un champ de formulaire ou un shell qui massacre + et /, stockez l'orthographe base64url à la place, parce que le jeu de caractères est tout le sens du format.

Fichiers : l'aller-retour .b64

Le travail « transformer ce fichier en fichier texte .b64 » est une lecture, un appel et une écriture :

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)

// plus tard, peut-être dans un autre processus
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")
}

Le trimmingCharacters du voyage de retour est là parce que celui qui a écrit le fichier a peut-être ajouté une fin de ligne, et le décodeur strict traite un retour à la ligne final comme un verdict de nil. Cet aller-retour revient octet pour octet, ce que vous devriez vérifier la première fois que vous l'expédiez. Pour les fichiers assez grands pour que l'usage de mémoire devienne intéressant, n'encodez pas tout le tampon d'un coup. Le base64 a une propriété merveilleuse qui rend le streaming exact : chaque trio d'octets d'entrée produit quatre caractères de sortie indépendants, alors tant que chaque bloc que vous encodez est un multiple de trois octets, la sortie concaténée est identique à l'encodage du fichier entier d'un seul coup. Cassez l'alignement et la sortie change, parce que la frontière de bloc coupe un groupe de trois octets en plein milieu du flux :

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 mémoire au pic est un tampon de lecture plus la ligne en cours, peu importe la taille du fichier, et la sortie découpée correspond exactement à l'orthographe .lineLength76Characters d'un seul coup. La même règle du multiple de trois, avec les rôles inversés, est celle sur laquelle s'appuie le décodeur en streaming de l'article sœur, alors les deux directions du voyage partagent une même vérité arithmétique.

Gros payloads et la facture mémoire

Faisons l'arithmétique qui vous servira la prochaine fois que quelqu'un demandera « est-ce qu'on peut mettre ça en base64 ? ». Chaque trio d'octets d'entrée devient quatre caractères de sortie, alors la taille est multipliée par 4/3 : un fichier de 100 kilo-octets devient une chaîne de 133 336 caractères, un fichier de 10 méga-octets devient 13 333 336 caractères, et ainsi de suite. Le padding ajoute au plus deux caractères tout à la fin, une erreur d'arrondi pour tout ce qui dépasse quelques octets, et l'entrée vide est la seule exemption, où le bureau de l'impôt accorde un passage gratuit unique et le résultat est la chaîne vide. Trois conséquences pratiques. D'abord, budgétez avant de commencer : si votre payload est déjà près d'une limite (la zone de confort d'une URL aux alentours de 2 000 caractères, le contrat d'un champ JSON, la largeur d'une colonne de base de données), divisez la limite par 1,33 avant d'encoder, pas après (et par 1,37 quand le découpage est de la partie). Deuxièmement, pendant que vous emballez, vous tenez les octets d'origine et la chaîne emballée en même temps, alors le jeu de travail fait environ 2,33 fois l'original, et les fonctions en streaming ci-dessus sont la soupape quand ce chiffre cesse d'être confortable. Troisièmement, la taxe est en pratique à sens unique : vous la payez quand vous emballez, et vos octets rentrent chez eux quand quelqu'un déballe, alors la vraie question n'est jamais « le base64 est-il cher ? » mais « la route textuelle sur laquelle je suis l'exige-t-elle ? »

Les erreurs qui mordent

  • L'étape failable du jeu de caractères. String.data(using:) peut répondre nil (essayez .ascii avec un caractère accentué), et le force-unwrap est l'escalade classique d'une mauvaise entrée en app plantée. Gardez la conversion sous garde, pas seulement l'appel base64, qui est la partie facile.
  • Le style maison CRLF. Une option .lineLength sans option de fin de ligne produit du CRLF par défaut. Si votre format veut du LF uniquement et que vous avez oublié l'option, votre sortie transporte des retours chariot qu'elle n'aurait jamais dû avoir.
  • Le piège du CR seul. .endLineWithCarriageReturn seul produit des fins de ligne CR uniquement, style vieux Mac. Si vous vouliez dire CRLF (et pour le MIME, c'est le cas), passez les deux options de fin de ligne.
  • Le découpage dans du JSON. Un retour à la ligne brut dans une chaîne JSON est du JSON invalide, point final. Du base64 découpé interpolé dans un document est une erreur de syntaxe à l'accent base64. Gardez la sortie découpée dans les corps d'e-mail et les fichiers de certificat.
  • Le BOM passager clandestin. La conversion .utf16 simple préfixe un BOM de deux octets qui voyage dans votre sortie emballée et embrouille les décodeurs qui ne l'attendaient pas. Utilisez .utf16LittleEndian ou .utf16BigEndian quand vous avez besoin d'UTF-16 sans l'étiquette.
  • La dérive de dialecte. Le standard et le base64url sont des alphabets différents, et la RFC le dit par écrit. Un + qui survit jusqu'à une chaîne de requête devient un espace. Un - qui atteint un décodeur standard tolérant est supprimé. Choisissez le dialecte à la frontière et tenez-le.
  • La casse est une lettre. L'alphabet distingue A de a. Un copier-coller plié en casse ou un appel d'uppercasing enthousiaste corrompt silencieusement les données, parce que les deux versions passent quand même tous les contrôles d'alphabet. Le base64 est sensible à la casse comme un numéro de passeport.
  • La règle d'alignement. Les encodeurs en streaming doivent couper les blocs sur des multiples de trois octets. Un bloc mal aligné change la sortie, et le changement est silencieux : la chaîne se décode toujours, vers de fausses données.
  • Le double emballage. Deux couches qui encodent toutes les deux produisent du base64 de base64. Le lecteur qui décode une fois voit des lettres là où il devrait y avoir des octets, et l'incident s'écrit tout seul.
  • Le mur de disponibilité. Les nouvelles options natives (.base64URLAlphabet, .omitPaddingCharacter) existent dans les bêta des SDK les plus récents et dans le Foundation open source derrière un marqueur de disponibilité, mais pas dans chaque toolchain que votre CI touchera. Si vous les adoptez, protégez avec des contrôles de disponibilité pour que la même source compile sur un Xcode plus ancien et sur Linux. Sur la toolchain stable actuelle, l'extension de quatre lignes compile partout où les options ne le peuvent pas.
  • Le base64 n'est pas du chiffrement. Si l'exigence est la confidentialité, vous avez choisi le mauvais outil d'une catégorie entière. Le travail du base64 est de faire voyager des octets, et il fait exactement ce travail, pas un de plus.

Comment l'expédier

  • Encodez des octets, pas des vœux. Décidez la forme en octets avant d'appeler la méthode, l'UTF-8 par défaut et nommé explicitement quand ce n'est pas le cas, et gardez sous garde l'étape failable data(using:), parce que c'est là que les données disparaissent vraiment.
  • Sans découpage par défaut, découpé par contrat. La sortie simple sur une seule ligne est correcte pour le JSON, les API et la plupart des bases de données ; ne prenez les options de découpage 64/76 que quand le format récepteur les exige, et payez les deux options de fin de ligne si vous voulez dire CRLF.
  • Un dialecte par frontière. Le base64 standard pour les destinations centrées sur le texte, le base64url pour tout ce qui touchera une URL ou un nom de fichier, jamais les deux dans le même document. Écrivez la conversion une fois, nommez-la honnêtement, et réutilisez-la.
  • Budgétez la majoration. Multipliez par 4/3 avant de commencer (par 1,37 quand le découpage est de la partie), et passez en streaming avec des blocs alignés sur trois octets quand le payload est assez gros pour rendre le jeu de travail inconfortable.
  • N'utilisez pas du ruban d'emballage comme cadenas. Si l'exigence est la discrétion, arrêtez-vous au rayon base64 et prenez du chiffrement à la place.

Une courte histoire de l'emballage

L'alphabet avec lequel vous emballez, et la longueur de ligne à laquelle vous découpez, sont des fossiles de quatre décennies de débats sur la quantité de binaire qui peut survivre à une route textuelle, et la position de Swift dans cette histoire est courte mais intéressante :

  • Années 1980, l'ère de la même machine. Les premiers encodeurs de cette famille existaient pour faire circuler des fichiers en modem entre des systèmes qui supposaient que l'autre bout était une machine comme la leur. uuencode sur UNIX utilisait des majuscules, des chiffres et de la ponctuation, et ses concepteurs ont trouvé un tour qui économisait du calcul : l'alphabet est à des positions ASCII consécutives, alors l'encodage était littéralement « ajouter 32 », sans table de correspondance. BinHex, le cousin né sur le TRS-80 en 1981, a sauté sur l'Apple II, est devenu le format du Macintosh classique en 1984, et a fait un pari différent : ses 64 caractères omettent 7, O, W, g, o, et presque la moitié des minuscules.
  • 1987, l'alphabet obtient une adresse. La RFC 989, la première spécification Privacy-Enhanced Mail, a standardisé les 64 caractères exacts que vous tapez aujourd'hui, découpé la sortie à 64 caractères par ligne, et utilisé = pour le padding et * pour marquer les données encodées mais non chiffrées. Chaque bloc de type PEM que vous avez collé dans une configuration serveur est un descendant de ce document.
  • 1996, l'ère libérale. Le MIME (RFC 2045) a pris l'alphabet pour les pièces jointes d'e-mail, déplacé le découpage à 76 caractères, et ajouté la règle qui a rendu le découpage sûr à produire : les décodeurs devraient ignorer les retours à la ligne. Les encodeurs ont appris à découper, les décodeurs à pardonner. L'option 76 caractères de Swift est un souvenir vivant de ce débat précis.
  • 2003 à 2006, les règles se durcissent. La RFC 3548 (2003) a déclaré que le padding ne doit pas être sauté (sauf si un format dit le contraire) et que les décodeurs doivent rejeter les caractères hors alphabet ; la RFC 4648 (octobre 2006) a réglé la famille et ajouté l'alphabet URL-safe, explicitement pour que de longs identifiants puissent vivre dans des URL sans échapper en pourcentage chaque caractère spécial. La convention « pas de padding dans le dialecte URL » est née dans le même document, parce qu'un caractère de pad dans une URL devient typiquement %3D, ce qui tue l'intérêt.
  • 2013 à 2014, l'API est déjà là. La classe NSData d'Apple emballait du base64 depuis des années, et l'API basée sur des options avec ses quatre options de découpage est arrivée dans iOS 7, en 2013, avant même l'existence de Swift. Quand Swift 1.0 est arrivé le 9 septembre 2014, il a hérité d'un encodeur total, de quatre options de découpage et de l'alphabet de 64 lettres de 1987, et la personnalité n'a pas changé depuis.
  • 3 décembre 2015, la toolchain quitte le bâtiment. Swift est passé en open source ce jour-là, et le base64 de Foundation l'a suivi vers Linux et, plus tard, Windows. « Encoder du base64 en Swift loin d'une machine Apple » a à peine une décennie : un invité très jeune à une fête qui a commencé en 1987.
  • 2023 à 2026, la réécriture et le dialecte URL. La réécriture de Foundation (le projet swift-foundation) a déplacé Data dans 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, le reste de la famille est en maturation dans le Foundation open source derrière des marqueurs de disponibilité, et l'extension communautaire reste le pont portable en attendant.

Petits plaisirs

  • Un méga-octet emballé fait exactement 1 333 336 caractères base64, la taxe 4/3 plus deux caractères de padding, au chiffre près. La seule entrée qui échappe entièrement à la taxe est la vide : rien dans, rien dehors.
  • L'encodeur est total là où le décodeur ne l'est pas. Il ne renvoie jamais nil, ne lève jamais, ne refuse jamais. Le seul échec de tout le pipeline vit en amont, dans l'étape du charset, c'est pourquoi la méthode semble si calme par rapport à sa cousine.
  • Encodez le mot héllo en UTF-8 et il devient aMOpbGxv. Encodez-le en UTF-16 little-endian et il devient aADpAGwAbABvAA==. Même mot, deux passeports différents, tous deux valides, aucun interchangeable.
  • Le mot de test du monde base64 est foobar, et son encodage est Zm9vYmFy. 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 42 octets et s'ouvre avec le mot magique GIF89a, c'est pourquoi le préfixe R0lGODlh apparaît dans plus de bases de code sur Terre que presque n'importe quelle autre chaîne base64.
  • Votre struct Codable envoie probablement du base64 depuis des années sans que vous le remarquiez : la stratégie par défaut Data de JSONEncoder emballe en base64 standard, c'est pourquoi un champ Data traverse le câble en chaîne avec padding au lieu d'un tableau de nombres.
  • Le padding ne dépasse jamais deux caractères, jamais. Un payload de 1 octet finit par ==, un payload de 2 octets finit par =, et un payload de 3 octets ne finit par rien. Toute la grammaire du groupe final tient sur un ongle.
  • Swift a 27 ans de moins que l'alphabet avec lequel il emballe. Le langage est sorti en 2014. Les 64 lettres ont été standardisées en 1987 et n'ont pas changé depuis.

Voilà la boîte à outils d'emballage complète : une méthode totale, une étape failable qui vient avant, quatre options de découpage avec un style maison CRLF, une extension base64url de quatre lignes, une règle d'alignement sur trois octets pour le streaming, et une majoration 4/3 qui est le prix d'entrée de la route textuelle. L'encodage, c'est là qu'on paie la facture du base64, et vous connaissez maintenant chaque ligne avant de signer. Le moment où vous retournez le voyage et commencez à ouvrir ce que d'autres ont emballé, les nil reviennent, les verdicts sur les espaces blancs, et l'angle mort de la molette tolérante prennent le devant de la scène. L'article de décodage lié tient la scène entière sur cette moitié de l'aller-retour, alors quand les lettres commenceront à arriver, vous saurez déjà exactement comment les ouvrir.

Dernière mise à jour : 2026-09-08

Article associé : Décodage Base64 en Swift : un guide complet