Encodage Base64 en R : un guide complet
Vous avez des octets qui doivent voyager, et la route n'autorise que du texte. Un JPEG qui doit vivre à l'intérieur d'un champ JSON. Un certificat qui doit s'asseoir dans une variable d'environnement. Un graphique qui doit voyager dans un rapport HTML autonome. Le Base64 est le ruban adhésif de tout ça : n'importe quelle suite d'octets devient une chaîne de 64 caractères inoffensifs qui survit à chaque canal texte que vous lui lancez. La page d'accueil de ce site couvre le format en entier, donc voici la version courte : trois octets entrent, quatre caractères sortent, tirés de A à Z, a à z, 0 à 9, plus + et /, avec un petit = de padding à la fin quand la cargaison ne se divise pas exactement.
Le détail spécifique à R : le R de base n'embarque aucun encodeur Base64. Il n'y a pas de base64_encode() qui attend dans un paquet de base, et pas de builtin en une ligne à portée de main. Vous choisissez un paquet, et l'écosystème vous donne un vrai choix, avec des vitesses différentes, des habitudes d'enveloppement différentes, et des avis différents sur le padding. À la fin de cet article, vous saurez quel encodeur choisir dans chaque situation, et lequel d'entre eux fera en silence autre chose que d'encoder.
Le paysage des encodeurs
Cinq paquets font l'encodage, et ils se répartissent entre les chevaux de trait du quotidien, ceux qui gravitent autour du chiffrage, et les petits spécialistes. Voici la distribution, à jour en 2026 :
| Paquet | Version (2026) | Points d'entrée de l'encodage | Habitudes d'enveloppement | À choisir quand |
|---|---|---|---|---|
base64enc |
0.1-6 | base64encode() |
linewidth et newline, entièrement à vous de régler |
chaînes du quotidien, enveloppement MIME |
openssl |
2.4.2 | base64_encode() |
lignes de 64 caractères, coupures LF, retour à la ligne final | fichiers PEM, piles de chiffrement existantes |
b64 |
0.1.7 | encode(), encode_file() |
ne plie jamais ; b64_chunk() et b64_wrap() sur demande |
vitesse, vecteurs, moteurs URL-safe |
base64 |
2.0.2 | encode() |
lignes de 64 caractères plus un retour à la ligne final par défaut | corvées fichier à fichier, images de rapports |
base64url |
1.4 | base64_urlencode() |
ne plie jamais, sans padding, chaîne en entrée | chaînes URL-safe |
Trois encodeurs de plus se cachent dans des paquets que vous chargez peut-être déjà. jsonlite exporte base64_enc() et base64url_enc(), donc si vous parsez déjà du JSON, vous avez peut-être déjà un encodeur sous la main. jose exporte base64url_encode() pour le travail avec les JWT. Et le vétéran RCurl porte toujours une enveloppe base64() autour de libcurl qui marche bien et appartient à une époque précédente. Le paquet base64, enfin, se décrit désormais sur son étiquette comme une enveloppe de compatibilité et renvoie les nouvelles applications vers base64enc, openssl ou jsonlite.
La mise en place
Si R n'est pas encore sur la machine, votre système d'exploitation l'offre : r-base sur Debian et Ubuntu, R sur Fedora, Homebrew ou l'installateur officiel sur macOS, un installateur sur Windows. Puis les encodeurs, directement depuis CRAN :
install.packages("base64enc")
install.packages("openssl")
install.packages("b64")
install.packages("base64url")
Les deux endroits où les installations prennent un mauvais virage sont tous les deux du côté de la compilation. openssl se compile contre votre OpenSSL système, donc une machine Linux dépouillée veut d'abord les en-têtes de développement (sudo apt install libssl-dev), ou vous pouvez sauter la compilation entièrement sur Debian et Ubuntu avec sudo apt install r-cran-openssl. Et b64 est un moteur Rust enveloppé avec extendr, donc une build depuis le source veut la chaîne d'outils Rust (sudo apt install cargo installe aussi rustc). Windows et macOS reçoivent des binaires précompilés depuis CRAN et rien de tout cela ne s'applique.
Le premier encodage
Quatre-vingt-dix pour cent de la vie de l'encodage tiennent en trois lignes, avec la même chaîne célèbre que le côté décodage utilise comme test de fumée :
library(base64enc)
packed <- base64encode(charToRaw("Man"))
packed
#> [1] "TWFu"
identical(packed, "TWFu")
#> [1] TRUE
Trois choses à remarquer dans cette cérémonie. Premièrement, l'entrée est un vecteur brut : charToRaw() est le pont entre votre chaîne R et les octets qui seront emballés, et les encodeurs du quotidien de ce tableau - base64enc, openssl, b64 - prennent tous du brut. Deuxièmement, la sortie est dans la direction opposée des décodeurs : une simple chaîne de caractères, parce que l'encodage se termine du côté texte de la frontière. Troisièmement, regardez le calcul de longueur en action : trois octets en entrée, quatre caractères en sortie, pas de padding nécessaire parce que la cargaison se divise exactement. Quand la cargaison ne se divise pas, un ou deux caractères = atterrissent à la fin.
Et parce qu'un encodeur auquel on ne fait pas confiance est pire que rien, voici l'aller-retour qui prouve que les deux directions se mettent d'accord :
text <- "Hello, world!"
packed <- base64encode(charToRaw(text))
packed
#> [1] "SGVsbG8sIHdvcmxkIQ=="
identical(text, rawToChar(base64decode(packed)))
#> [1] TRUE
Chaînes, octets et le piège du nom de fichier
Voici maintenant le piège, parce que chaque développeur R y tombe une fois. base64encode() traite un argument character comme un nom de fichier, pas comme du texte à encoder. Donnez-lui une chaîne et il part à la recherche de ce fichier :
base64encode("Man")
#> Warning in file(what, "rb") :
#> cannot open file 'Man': No such file or directory
#> Error: cannot open the connection
base64encode(charToRaw("Man"))
#> [1] "TWFu"
L'avertissement est le signe révélateur : il a essayé file("Man", "rb"), ce qui veut dire « ouvrir un fichier nommé Man en lecture brute ». Donc avec base64encode(), la discipline tient en un réflexe : charToRaw() d'abord, toujours. Si c'est bien un fichier que vous voulez dire, c'est ce que la fonction fait, et la sortie est les octets du fichier emballés, ce qui est parfois exactement ce que vous voulez.
b64 prend une position différente sur la même question : son encode() accepte un vecteur de caractères directement, traite chaque élément comme du texte UTF-8, et est vectorisé en plus :
b64::encode("Man")
#> [1] "TWFu"
b64::encode(c("Man", "M"))
#> [1] "TWFu" "TQ=="
Deux tranchants de plus de cette frontière méritent d'être connus. L'entrée vide s'encode de trois façons différentes, selon à qui vous demandez :
base64encode(raw(0))
#> character(0)
b64::encode("")
#> [1] ""
base64url::base64_urlencode("")
#> [1] ""
base64enc répond avec un vecteur de caractères de longueur nulle, pas avec une chaîne vide, donc le code en aval qui attend une chaîne et reçoit character(0) échoue dans des endroits surprenants. Et la décision de charset vit aussi du côté encodage : charToRaw() emballe la chaîne dans l'encodage qu'elle porte actuellement, donc du texte UTF-8 voyage en octets UTF-8, ce que l'autre bout du fil attend. Emoji compris, puisque le R moderne stocke les points de code au-dessus de U+FFFF en vrai UTF-8 :
emoji <- "\U0001F600"
nchar(emoji, type = "bytes")
#> [1] 4
round_trip <- base64decode(base64encode(charToRaw(emoji)))
identical(emoji, rawToChar(round_trip))
#> [1] TRUE
Cassures de ligne : MIME, PEM et votre propre largeur
Les longues chaînes Base64 sont coupées en lignes, parce que les plus anciens canaux texte du monde avaient des limites de colonnes, et le MIME n'a jamais eu le temps de les oublier. Les deux enveloppements historiques que vous croiserez sont le MIME, qui coupe à 76 caractères avec CRLF entre les lignes, et le PEM, qui coupe à 64. Chaque encodeur a sa propre idée de lequel utiliser, s'il y en a un, donc c'est dans cette section que vous choisissez le contrat avant d'encoder.
D'abord le calcul de taille, parce que l'enveloppement, c'est savoir quelle longueur prend la sortie. Quatre caractères sortent pour chaque trois octets, ce qui rend la longueur de la forme encodée un simple plafond :
nchar(base64encode(charToRaw(strrep("a", 100))))
#> [1] 136
4 * ceiling(100 / 3)
#> [1] 136
Ça en poche, place aux encodeurs. base64enc est le plus flexible : par défaut il émet une seule ligne non coupée, et l'argument linewidth vous remet à la place un vecteur de lignes :
long <- charToRaw(strrep("R", 100))
base64encode(long, linewidth = 76)
#> [1] "UlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJS"
#> [2] "UlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUg=="
base64encode(long, linewidth = 76, newline = "\r\n")
#> [1] "UlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJS\r\nUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUg=="
Deux lignes pour 100 octets à la largeur 76, un vecteur par défaut, une seule chaîne jointe par CRLF quand vous ajoutez newline. Pas d'élément vide en queue : 114 octets, qui s'encodent en exactement deux lignes de 76, reviennent en deux lignes, pas trois.
openssl prend le pôle opposé. linebreaks = TRUE enveloppe à 64 caractères avec des coupures LF plaines, et ajoute encore un retour à la ligne tout à la fin :
wrapped <- openssl::base64_encode(charToRaw(strrep("a", 100)), linebreaks = TRUE)
nchar(wrapped)
#> [1] 139 # 136 caractères de données plus 3 coupures de ligne
nchar(strsplit(wrapped, "\n", fixed = TRUE)[[1]])
#> [1] 64 64 8
Comptez l'arithmétique : 136 caractères de données, deux coupures internes, une coupure finale, 139 au total. Et cette coupure finale est invisible pour le décompte de lignes le plus naturel que vous puissiez écrire, parce que strsplit() jette le morceau vide en queue, donc le vecteur dit trois lignes tandis que la chaîne en porte quatre. Si un jour vous faites un diff entre une sortie enveloppée OpenSSL et une enveloppée MIME et que les compteurs de caractères ne s'additionnent pas, c'est ce fantôme.
b64 n'enveloppe pas du tout ; il vous donne les deux opérations séparément, et la largeur des morceaux suit une règle, un multiple de quatre, parce que le moteur refuse de couper un groupe Base64 en deux :
enc <- b64::encode(strrep("a", 100))
ch <- b64::b64_chunk(enc, 76)
b64::b64_wrap(ch, "\r\n")
#> [1] "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh\r\nYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQ=="
b64::b64_chunk(enc, 75)
#> Error: Chunk size must be a multiple of 4.
Et le paquet base64, orienté fichiers, suit l'exemple d'OpenSSL : lignes de 64 caractères, retour à la ligne final, activé par défaut :
writeBin(charToRaw(strrep("b", 200)), "big.bin")
base64::encode("big.bin", "big.b64")
con <- file("big.b64")
lines <- readLines(con)
close(con)
nchar(lines)
#> [1] 64 64 64 64 12 0
Ce zéro final, c'est le retour à la ligne en queue, attrapé par readLines() comme une dernière ligne vide. Voici le champ entier d'un coup :
| Encodeur | Largeur | Coupe de ligne | Retour à la ligne final |
|---|---|---|---|
base64encode(x) |
une ligne | aucune | aucun |
base64encode(x, linewidth = 76, newline = "\r\n") |
76 | CRLF | aucun |
openssl::base64_encode(x, linebreaks = TRUE) |
64 | LF | oui |
b64::encode(x) |
une ligne | aucune (envelopper avec b64_chunk() et b64_wrap()) |
aucun |
base64::encode(in, out) |
64 | LF | oui |
base64url::base64_urlencode(x) |
une ligne | aucune | aucun |
Base64 URL-safe
Le Base64 standard passe les deux dernières places de son alphabet à + et /, et ce sont précisément les caractères que les URL n'aiment pas : le plus devient %2B, la barre oblique devient %2F, et chaque = de padding devient %3D. La variante URL-safe, définie dans la section 5 du RFC 4648, échange ces deux lettres contre - et _ et supprime d'habitude le padding, donc un jeton qui devrait se coller partout reste collable partout. R a trois portes vers ce monde.
Les moteurs b64 sont les plus complets : un moteur est un alphabet configuré et une politique de padding, et le paquet embarque les quatre qui vous servent. Offrez les mêmes trois octets au moteur standard et au moteur URL-safe et regardez l'alphabet faire son travail :
bytes <- as.raw(c(0xfb, 0xef, 0xbe))
b64::encode(bytes)
#> [1] "++++"
b64::encode(bytes, engine("url_safe"))
#> [1] "----"
b64::encode(as.raw(0x4d), engine("url_safe"))
#> [1] "TQ=="
b64::encode(as.raw(0x4d), engine("url_safe_no_pad"))
#> [1] "TQ"
Quatre moteurs, "standard", "standard_no_pad", "url_safe" et "url_safe_no_pad", et le même objet moteur marche dans les deux directions, ce qui garde votre code symétrique. Les variantes sans padding ne diffèrent que quand le padding apparaîtrait vraiment, comme dans l'exemple d'un octet ci-dessus.
Le paquet dédié base64url est la porte à une seule mission : chaîne en entrée, chaîne URL-safe en sortie, ne plie jamais, ne met jamais de padding :
base64url::base64_urlencode("hello world")
#> [1] "aGVsbG8gd29ybGQ"
Et jose exporte son propre base64url_encode() pour le travail avec les JWT, qui renvoie des vecteurs bruts du côté décodage comme vous pourriez l'espérer. Une règle lie les trois portes : l'alphabet avec lequel vous encodez est celui avec lequel vous devez décoder. Donnez la chaîne "----" à un décodeur standard et il échoue sur le premier tiret ; l'article sœur sur le décodage couvre comment chaque décodeur rencontre des entrées sales ou qui ne correspondent pas.
JWT : fabriquer le jeton
Le JSON Web Token est l'endroit où le Base64 URL-safe est devenu un outil du quotidien. Un JWT, ce sont trois parties base64url jointes par des points : un en-tête qui décrit la façon dont il a été signé, un payload de claims JSON, et une signature qui lie les deux. Côté encodage vous construisez les trois, et le paquet jose fait toute la cérémonie. Son API est bâtie autour des fonctions jwt_* (jwt_claim(), jwt_encode_hmac(), jwt_decode_hmac(), jwt_split()) ; la version 2.0 (avril 2026) ajoute le support ED25519 et rend le champ d'en-tête typ optionnel :
library(jose)
claim <- jwt_claim(iss = "app", sub = "1234567890", exp = Sys.time() + 3600)
token <- jwt_encode_hmac(claim, "0123456789abcdef")
sp <- jwt_split(token)
sp$header
#> $typ
#> [1] "JWT"
#>
#> $alg
#> [1] "HS256"
names(sp$payload)
#> [1] "iss" "sub" "exp" "iat"
Remarquez ce que jwt_claim() a fait en coulisses : iat (issued at) vaut par défaut l'heure actuelle, donc il est apparu dans le payload sans qu'on le demande, tandis que exp vaut par défaut rien, ce qui veut dire un jeton sans expiration, ce qui est d'habitude ce que vous ne voulez pas. Réglez exp délibérément, et la signature s'occupe du reste. jwt_split() est l'outil d'inspection : l'en-tête, le payload comme liste nommée, et la signature brute, sans vérification, exactement l'étape de jeter un œil que le côté décodage de cette paire d'articles décrit.
Côté vérification, jose gagne sa vie. jwt_decode_hmac() contrôle la signature et impose les claims de temps, et son style de refus est précis :
round <- jwt_decode_hmac(token, "0123456789abcdef")
round$sub
#> [1] "1234567890"
jwt_decode_hmac(token, "ffffffffffffffff")
#> Error: HMAC signature verification failed!
old <- jwt_claim(sub = "1234567890", exp = Sys.time() - 3600)
expired <- jwt_encode_hmac(old, "0123456789abcdef")
jwt_decode_hmac(expired, "0123456789abcdef")
#> Error: Token has expired on 2026-08-30 05:09:36
En cas de succès vous recevez les claims comme une liste ordinaire, donc round$sub et ses amis marchent tout seuls. Un claim nbf (not before) dans le futur a droit à son propre refus, Token is not valid before ..., et un jeton HMAC ne sera pas décodé par l'jwt_decode_sig() asymétrique, qui répond Unsupported algorithm: HMAC et veut une clé publique à la place. Deux notes finales. Premièrement, le payload est encodé, pas chiffré : n'importe qui peut lire chaque claim, donc rien de secret n'a sa place dedans. Deuxièmement, si vous chargez httr2 après jose, attention au message de masquage : httr2 exporte ses propres jwt_claim(), jwt_encode_hmac() et jwt_encode_sig(), bâties pour les identifiants client OAuth avec exp qui vaut par défaut cinq minutes plus tard, et elles masquent les versions jose pour le reste de la session.
Fichiers : du binaire en entrée, du texte en sortie
Encoder un fichier est le miroir du travail de fichiers du côté décodage, et le paquet b64 a le point d'entrée le plus clair, qui est aussi le plus rapide parce qu'il ne construit jamais une seule chaîne intermédiaire énorme :
writeBin(charToRaw("file payload bytes"), "payload.bin")
enc <- b64::encode_file("payload.bin")
enc
#> [1] "ZmlsZSBwYXlsb2FkIGJ5dGVz"
cat(enc, file = "payload.b64")
readLines("payload.b64")
#> Warning in readLines("payload.b64") :
#> incomplete final line found on 'payload.b64'
#> [1] "ZmlsZSBwYXlsb2FkIGJ5dGVz"
L'avertissement est une fonctionnalité déguisée : cat() n'ajoute pas de retour à la ligne final, donc le fichier se termine en pleine ligne, et readLines() vous le signale. Pensez à quel côté de ce fait vous êtes quand le fichier sera consommé par b64::decode_file(), qui panique sur un retour à la ligne final ; l'article de décodage a toute l'histoire. Écrivez avec cat() ou writeBin(charToRaw(enc), path) et le tranchant reste émoussé.
Le paquet base64 est l'option pure fichier à fichier, avec le duo de fonctions qui vont ensemble et l'enveloppement à la OpenSSL par défaut :
base64::encode("payload.bin", "payload2.b64")
readLines("payload2.b64")
#> [1] "ZmlsZSBwYXlsb2FkIGJ5dGVz" ""
Il renvoie le chemin de sortie, ce qui est pratique pour les logs. Et quand vous voulez voir la mécanique, le pipeline manuel marche avec n'importe quel encodeur du tableau : lisez le fichier en brut, encodez, écrivez le texte, terminé :
bytes <- readBin("payload.bin", what = "raw", n = file.size("payload.bin"))
enc <- base64encode(bytes)
writeLines(enc, "payload3.b64")
identical(enc, readLines("payload3.b64"))
#> [1] TRUE
URI data et documents autonomes
Le schéma d'URI data: (RFC 2397) est le client le plus visible du côté encodage : un document qui porte son propre contenu, un type MIME, le mot « base64 », et le payload, le tout dans un seul attribut. Les octets magiques du PNG rendent le schéma reconnaissable : tous les PNG en Base64 qui existent dans la nature commencent par les mêmes caractères, parce que l'en-tête de fichier 89 50 4E 47 s'encode toujours de la même façon :
png_head <- as.raw(c(0x89, 0x50, 0x4e, 0x47))
uri <- paste0("data:image/png;base64,", base64encode(png_head))
uri
#> [1] "data:image/png;base64,iVBORw=="
tag <- paste0("<img src=\"", uri, "\" />")
Si vous avez déjà grepé un fichier HTML à la recherche de iVBOR pour trouver des images embarquées, voilà pourquoi cette empreinte fonctionne. Les rapports R Markdown, les tableaux de bord en fichier unique et les pages scrapées utilisent tous la même forme, et en construire un suit le même schéma : lisez le fichier en brut, encodez, et collez derrière le préfixe MIME. Le coût, c'est le calcul de taille vu plus tôt, un tiers plus gros que l'original, qui reste dans votre HTML pour toujours, alors gardez les images embarquées légères.
API et requêtes web
Le côté décodage de cette paire d'articles croise des API qui vous remettent du Base64 ; ce côté croise des API qui en veulent. Le schéma est le même à chaque fois : encodez les octets, mettez la chaîne dans le corps JSON, envoyez. Le client HTTP moderne est httr2 :
library(httr2)
req <- request("https://httpbin.org/post")
req <- req_body_json(req, list(file = packed, name = "man.txt"))
req
#> <httr2_request>
#> POST https://httpbin.org/post
#> Body: JSON data
res <- req_perform(req)
res$status_code
#> [1] 200
Trois notes pour la route. Le constructeur de requête est request(), et il porte ce nom depuis la toute première version de httr2. Le corps de la réponse, quand vous lisez des réponses, arrive sous forme de vecteur brut, donc rawToChar() avant de parser. Et l'API peut parler un dialecte : certaines veulent l'alphabet URL-safe, certaines veulent le padding arraché, et une API JSON n'a aucun problème avec un = dans une valeur de chaîne, donc la question du padding ne devient une question d'URL que quand le Base64 voyage dans le chemin ou la requête. En cas de doute, lisez les exemples de l'API plutôt que la spec dans votre tête.
Bases de données, configuration et environnement
Le Base64 dans une base de données, c'est du binaire contrebandé à travers une colonne texte, et le côté encodage est l'emballage d'un blob avant qu'il entre. Voici l'aller-retour contre SQLite via DBI et RSQLite :
library(DBI)
library(RSQLite)
db <- dbConnect(SQLite(), ":memory:")
dbExecute(db, "CREATE TABLE files (name TEXT, payload TEXT)")
stored <- base64encode(charToRaw("stored in a database"))
dbExecute(db, paste0("INSERT INTO files VALUES ('note.txt', '", stored, "')"))
row <- dbGetQuery(db, "SELECT * FROM files")
rawToChar(base64decode(row$payload))
#> [1] "stored in a database"
L'alternative est de stocker les octets nativement en BLOB, auquel cas aucun Base64 n'est nécessaire du tout et la colonne revient en R comme vecteur brut. La variante Base64-dans-TEXT existe pour la portabilité : vous pouvez l'inspecter avec un éditeur de texte, en faire le diff, et chaque autre langage peut la lire sans pilote binaire. Le même raisonnement l'entraîne dans la configuration, où un certificat ou un secret est stocké comme chaîne dans du YAML, du JSON ou une variable d'environnement :
Sys.setenv("API_CERT" = base64encode(charToRaw("LTSSECRET")))
Sys.getenv("API_CERT")
#> [1] "TFRTU0VDUkVU"
Une mise en garde mérite sa place ici, parce que les fichiers de configuration sont l'endroit où vivent les secrets : le Base64 est un encodage, pas un chiffrement. Une valeur Base64 dans un fichier de config est lisible par n'importe qui qui peut lire le fichier. Elle survit au transport et aux éditeurs de texte ; elle ne protège rien.
L'e-mail
L'e-mail est l'endroit où l'enveloppement à 76 caractères est né, et les pièces jointes MIME le portent encore : du contenu Base64, coupé à 76 caractères avec CRLF entre les lignes, dans une partie qui déclare Content-Transfer-Encoding: base64. L'encodeur qui produit exactement cette forme est base64encode() avec ses arguments d'enveloppement réglés sur le contrat MIME :
body <- "The quick brown fox jumps over the lazy dog, and then it came back down again."
mime_part <- base64encode(charToRaw(body), linewidth = 76, newline = "\r\n")
strsplit(mime_part, "\r\n", fixed = TRUE)[[1]]
#> [1] "VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZywgYW5kIHRoZW4gaXQg"
#> [2] "Y2FtZSBiYWNrIGRvd24gYWdhaW4u"
R n'a pas de client de messagerie de premier ordre, mais le point tient chaque fois que vous construisez ou inspectez des parties MIME à la main, générez des fixtures .eml, ou parsez des pièces jointes depuis l'une d'elles : voilà la forme que le Base64 doit avoir, et le côté décodage de cette paire d'articles montre les décodeurs tolérants qui le déplient en chemin.
Gros payloads et le plafond des chaînes
Les chaînes R ont un plafond dur de 2^31 - 1 octets, et comme la forme encodée est environ un tiers plus grosse que l'original, un fichier d'environ 1,5 Go de données brutes pousserait son encodage en une seule ligne au-delà du mur. Le geste pratique est le même que celui que base64enc propose depuis sa sortie 2022 des longs vecteurs : gardez la sortie en lignes, pas en une seule chaîne :
big <- raw(10 * 1024 * 1024)
packed <- base64encode(big, linewidth = 76)
length(packed)
#> [1] 183961
sum(nchar(packed))
#> [1] 13981016
Dix mégaoctets de zéros deviennent 183961 lignes d'au plus 76 caractères, un vecteur parfaitement ordinaire que vous pouvez écrire ligne par ligne ou faire passer en flux dans un tuyau sans jamais tenir une seule chaîne énorme. Pour le cas du data frame, une colonne pleine de valeurs binaires, b64 est le champion de la vitesse : son moteur Rust encode la colonne entière en un seul appel vectorisé, ce qui est une différence dramatique par rapport à une boucle ligne par ligne. Si vous avez une colonne de valeurs encodées à produire, faites vous-même une petite comparaison avec system.time() ; l'écart entre une boucle par ligne et un seul appel vectorisé est généralement assez grand pour compter.
La ligne de commande
Tout n'a pas besoin d'une session R complète. L'outil Unix classique parle Base64 nativement, encodant sans aucun attribut sur toutes les plateformes, et décodant avec -d sur Linux et -D sur macOS et les BSD :
echo -n "Hello, world!" | base64
#> SGVsbG8sIHdvcmxkIQ==
echo -n "SGVsbG8sIHdvcmxkIQ==" | base64 -d
#> Hello, world!
Regardez le -n de la première ligne : sans lui, echo ajoute un retour à la ligne, et la sortie encode 14 octets au lieu de 13, finissant par o= au lieu de IQ==. Un base64 GNU enveloppe aussi pour vous (-w 76), ce qui est pratique quand vous pipez vers quelque chose qui attend une entrée en forme MIME. Et un Rscript d'une ligne fait le même travail avec les mêmes paquets que ceux que vous utilisez dans vos scripts :
Rscript -e 'library(base64enc); writeLines(base64encode(charToRaw("Man")))'
#> TWFu
Utilisez le shell pour les vérifications rapides et les tuyaux ; utilisez R quand le résultat doit vivre dans un data frame, un fichier ou un rapport. Et ne collez pas des chaînes de plusieurs mégaoctets dans le terminal : les arguments de ligne de commande butent contre ARG_MAX bien avant le Base64, alors faites passer par un fichier à la place.
Les pièges qui valent le coup d'être connus
Voici la liste courte des façons dont le côté encodage mord les développeurs R, toutes natives de l'écosystème plutôt que du Base64 en général :
- Une chaîne est un nom de fichier.
base64encode("Man")essaie d'ouvrir un fichier nommé Man. L'avertissement nomme le fichier ; l'erreur dit que la connexion a échoué. Avecbase64encode(), passez parcharToRaw()d'abord. - L'entrée vide s'encode de trois façons.
base64encode(raw(0))renvoiecharacter(0), tandis queb64::encode("")etbase64url::base64_urlencode("")renvoient"". Le code en aval sur les chaînes n'attend pas un vecteur de longueur nulle. - openssl enveloppe puis ajoute une ligne fantôme.
linebreaks = TRUEcoupe à 64 avec LF et appose un retour à la ligne final questrsplit()jette en silence, donc les décomptes naïfs de lignes et de caractères se trompent d'une ligne. - L'enveloppement est un contrat. Le MIME est 76 avec CRLF, le PEM est 64, les API JSON veulent d'habitude rien du tout. Choisissez la forme que l'autre côté attend, parce qu'un décodeur qui tolère un style d'enveloppement en rejettera un autre.
- L'alphabet URL-safe doit correspondre aux deux bouts.
"----"encodé URL-safe échoue dans un décodeur standard sur le premier tiret, et le padding devient%3Dau moment où la chaîne vit dans une URL. - b64_chunk exige des multiples de quatre. Toute autre largeur a droit à
Chunk size must be a multiple of 4., parce qu'un groupe Base64 ne peut pas être coupé en deux. - Un retour à la ligne final peut faire paniquer le décodeur. Les fichiers que vous écrivez pour que
b64::decode_file()les lise doivent finir sans retour à la ligne ;cat(), paswriteLines(). - Les claims de temps des JWT sont appliqués.
jwt_decode_hmac()refuse les jetons expirés et les claimsnbfdans le futur, ethttr2masque les fonctionsjwt_*dejosesi vous le chargez en second. - Le payload est visible. Les claims Base64 d'un JWT, d'une URI data ou d'un fichier de config sont lisibles par n'importe qui. L'encodage n'est pas un chiffrement.
- Le plafond des chaînes est un mur, pas une recommandation. Environ 1,5 Go de données brutes par chaîne R est l'endroit où l'encodage en une seule ligne cesse de tenir ; gardez les grosses sorties en lignes ou en flux.
Bonnes pratiques
- Convertissez avec
charToRaw()avant d'encoder quand vous utilisezbase64enc::base64encode(). Si vous avez besoin d'une entrée de caractères directe et vectorisée,b64::encode()est le paquet qui traite les chaînes comme des chaînes. - Partez de
base64enc::base64encode()par défaut pour le travail de tous les jours, allez versb64quand vous voulez de la vitesse, une vraie vectorisation ou les moteurs URL-safe, et utilisezopensslquand il est déjà dans le projet et que vous voulez une sortie en forme PEM. - Choisissez l'enveloppement par canal, pas par paquet : rien pour le JSON et les URL, 76 avec CRLF pour le MIME, 64 pour les blocs à la PEM, et restez cohérent pour que le côté décodeur du tuyau sache à quoi s'attendre.
- Utilisez l'alphabet URL-safe sans padding pour tout ce qui vivra dans une URL, un JWT ou un nom de fichier, et l'alphabet standard pour l'e-mail.
- Pour les JWT, réglez
exp(etiat) explicitement, vérifiez avecjwt_decode_hmac()avant de faire confiance à un jeton, et souvenez-vous que chaque claim est du texte public. - Testez vos chemins d'encodage avec un aller-retour,
identical(text, rawToChar(base64decode(base64encode(charToRaw(text))))); c'est une ligne et elle attrape d'un coup les erreurs d'alphabet, de padding et de charset. - Pour les fichiers, préférez
b64::encode_file()oubase64::encode()plutôt que de lire tout le fichier dans une seule chaîne, et écrivez votre sortie aveccat()quand un décodeur strict va la lire. - Gardez le ruban adhésif honnête : le Base64 fait voyager les octets, il ne les rend ni secrets ni plus petits. Chiffrez d'abord si la discrétion est le but, compressez d'abord si c'est la taille.
Comment le Base64 est entré dans R
Le format est arrivé bien avant que R n'en fasse quoi que ce soit. Il a été standardisé pour le protocole Privacy-Enhanced Mail en 1987 (RFC 989), adopté par le MIME en 1993 (RFC 1521, puis le RFC 2045 final en 1996, qui définit encore l'enveloppement à 76 caractères), remis en forme dans le RFC 3548 en 2003, et a reçu sa forme moderne dans le RFC 4648 en 2006, qui a ajouté l'alphabet URL-safe, l'option sans padding, et son petit frère Base32. L'histoire R a commencé en septembre 2012, quand le base64enc de Simon Urbanek est arrivé sur CRAN et est devenu discrètement la réponse par défaut à « comment je Base64 ça » pendant plus d'une décennie, gagnant checkUTF8() en 2015 et le support des longs vecteurs en 2022. Le monde du chiffrement est entré par openssl, l'enveloppe de longue haleine de Jeroen Ooms autour de l'OpenSSL système, dont le base64_encode() est l'option en forme PEM depuis. Le vieux paquet base64, lui aussi par Ooms, a été réédité en octobre 2024 explicitement comme enveloppe de compatibilité, sa propre description renvoyant désormais les nouvelles applications ailleurs. Puis b64 est arrivé en janvier 2024, un moteur Rust construit avec extendr qui a apporté la vectorisation et un régiment d'alphabets, et en avril 2026 jose a sorti la version 2.0, ajoutant le support ED25519 à l'API jwt_*, et faisant des jetons signés un citoyen de premier ordre. Le résultat est une boîte à outils avec un encodeur par métier : chaînes du quotidien, blocs PEM, vitesse, URL, fichiers et jetons.
Le coin des faits amusants
Parce qu'un guide complet doit finir sur un sourire :
- Tous les PNG en Base64 sur internet commencent par les mêmes caractères : l'en-tête magique 89 50 4E 47 s'encode en
iVBOR, donc grepé un fichier HTML pour cette empreinte trouve toutes les images embarquées. Les formats ont des empreintes, et celle-ci est un préfixe. base64encode("Man")n'encode pas le mot Man. Il part à la recherche d'un fichier nommé Man, avertit qu'il ne peut pas l'ouvrir, et abandonne. Le piège le plus spécifique à R de tout l'écosystème Base64, caché en plein jour dans la liste des arguments.- Une chaîne enveloppée par OpenSSL finit toujours avec un retour à la ligne en queue, donc la dernière ligne d'un bloc PEM n'est jamais la dernière ligne du fichier. L'enveloppement a un point final de phrase, que vous le vouliez ou non.
- La chaîne vide s'encode de trois façons différentes :
base64encrenvoie un vecteur de zéro chaînes,b64etbase64urlrenvoient la chaîne vide. R rencontre rien, et R donne trois réponses. - jose écrit l'en-tête JWT avec
typavantalg, tandis que la plupart des exemples écrits à la main mettentalgen premier. Le JSON ne se soucie pas de l'ordre des clés, et la vérification JWT le sait, mais votre diff de chaînes ne le saura pas. - La limite de 76 caractères du MIME est une décision de 1993 sur la longueur des lignes de message, portée par quatre RFC dans chaque pièce jointe que vous avez jamais envoyée. L'enveloppement que vous réglez aujourd'hui a été débattu avant que R n'ait un graphique en couleur.
b64décodera des alphabets que vous n'avez jamais vus, BinHex, l'UTF-7 modifié d'IMAP, bcrypt, crypt, et le duo URL-safe, avec un moteur chacun. Le même code Rust qui emballe votre champ JSON peut déballer une pièce jointe de Macintosh des années 1980.
En résumé
Choisissez votre encodeur selon le travail : base64enc pour les chaînes du quotidien, avec linewidth et newline quand le canal a une forme ; openssl quand vous voulez une sortie de 64 caractères à la PEM ou qu'il est déjà dans le projet ; b64 quand vous voulez de la vitesse, des vecteurs ou les moteurs URL-safe ; et les petits spécialistes base64 et base64url pour les corvées de fichiers et les chaînes URL-safe. Convertissez avec charToRaw() avant d'encoder avec base64enc::base64encode(), choisissez l'enveloppement par canal plutôt que par paquet, gardez l'alphabet URL-safe pour tout ce qui vivra dans une URL ou un JWT, signez et vérifiez les jetons avec jose, et testez chaque chemin avec un aller-retour. Le format lui-même ne pardonne que dans exactement deux endroits, l'alphabet et les retours à la ligne, et les encodeurs diffèrent surtout dans la façon dont ils vous disent honnêtement quand vous vous êtes trompé sur l'un des deux. Et quand l'autre direction appelle - quand une chaîne arrive et qu'il faut la démonter, vérifier ce que les octets veulent dire, et survivre aux décodeurs qui échouent en silence - l'article sœur couvre le décodage Base64 en R en détail.
Dernière mise à jour : 2026-09-08
Article associé : Décodage Base64 en R : un guide complet