Base64-codering in R: een complete gids
Je hebt bytes die moeten reizen, en de weg laat alleen tekst toe. Een JPEG die in een JSON-veld moet leven. Een certificaat dat in een omgevingsvariabele moet zitten. Een plot die in een zelfstandig HTML-rapport moet reizen. Base64 is de pakband voor het geheel: elke reeks bytes wordt een string van 64 onschuldige tekens die elk tekstkanaal overleeft dat je maar kunt bedenken. De startpagina van deze site behandelt het formaat in zijn geheel, dus hier is de korte versie: drie bytes erin, vier tekens eruit, uit A tot Z, a tot z, 0 tot 9, plus + en /, met een beetje =-padding aan het einde als de lading niet net uitkomt.
De R-specifieke wending: base R levert helemaal geen Base64-encoder mee. Er staat geen base64_encode() te wachten in een base-pakket, en er is geen ingebouwde functie van één regel waar je naar kunt grijpen. Je kiest een pakket, en het ecosysteem geeft je écht een keuze, met verschillende snelheden, verschillende gewoontes bij het afbreken en verschillende meningen over padding. Tegen het einde van dit artikel weet je welke encoder je in elke situatie moet pakken, en welke daarvan stilletjes iets anders doet dan encoderen.
Het encoderlandschap
Vijf pakketten doen het encoderen, en ze vallen op in de dagelijkse werkpaarden, de crypto-aangrenzende en de kleine specialisten. Hier is het gezelschap, zo actueel in 2026:
| Pakket | Versie (2026) | Encode-ingangen | Gewoontes bij het afbreken | Waarvoor je het pakt |
|---|---|---|---|---|
base64enc |
0.1-6 | base64encode() |
linewidth en newline, geheel aan jou om in te stellen |
dagelijkse strings, MIME-afbreken |
openssl |
2.4.2 | base64_encode() |
regels van 64 tekens, LF-breekpunten, regeleinde aan de staart | PEM-bestanden, bestaande crypto-stacks |
b64 |
0.1.7 | encode(), encode_file() |
breekt nooit af; b64_chunk() en b64_wrap() op verzoek |
snelheid, vectoren, URL-veilige engines |
base64 |
2.0.2 | encode() |
regels van 64 tekens plus een regeleinde aan de staart, standaard | bestandsnaar-bestandsklusjes, rapportafbeeldingen |
base64url |
1.4 | base64_urlencode() |
breekt nooit af, geen padding, character erin | URL-veilige strings |
Nog drie encoders verstoppen zich in pakketten die je misschien al laadt. jsonlite exporteert base64_enc() en base64url_enc(), dus als je JSON al ontledt, heb je misschien al een encoder bij de hand. jose exporteert base64url_encode() voor JWT-werk. En het veteraan-RCurl-pakket draagt nog steeds een base64()-wrapper rond libcurl die prima werkt en tot een eerdere era behoort. Het base64-pakket beschrijft zich tenslotte nu op de blik als een compatibiliteitswrapper en wijst nieuwe toepassingen naar base64enc, openssl of jsonlite.
Opzetten
Als R nog niet op de machine staat, levert je besturingssysteem hem: r-base op Debian en Ubuntu, R op Fedora, Homebrew of de officiële installer op macOS, een installer op Windows. Daarna de encoders, rechtstreeks vanuit CRAN:
install.packages("base64enc")
install.packages("openssl")
install.packages("b64")
install.packages("base64url")
De twee plekken waar installaties misgaan, zijn beide buildtijd. openssl compileert tegen je systeem-OpenSSL, dus een kale Linux-machine wil eerst de ontwikkelheaders (sudo apt install libssl-dev), of je slaat de compilatie op Debian en Ubuntu helemaal over met sudo apt install r-cran-openssl. En b64 is een Rust-engine die met extendr is ingepakt, dus een compilatie vanaf de bron wil de Rust-toolchain (sudo apt install cargo trekt rustc er ook bij). Windows en macOS krijgen voorgebouwde binaries van CRAN en dan geldt dit allemaal niet.
De eerste keer encoderen
Negentig procent van het encoderen past in drie regels, met dezelfde beroemde string die de decode-zijde als rooktest gebruikt:
library(base64enc)
packed <- base64encode(charToRaw("Man"))
packed
#> [1] "TWFu"
identical(packed, "TWFu")
#> [1] TRUE
In dat ritueel zijn drie dingen op te merken. Eerst is de invoer een raw vector: charToRaw() is de brug van je R-string naar de bytes die worden ingepakt, en de dagelijkse encoders in deze tabel - base64enc, openssl, b64 - nemen allemaal raw. Tweede is dat de uitvoer de tegengestelde richting is van de decoders: één tekenstring, want encoderen eindigt aan de tekstzijde van de grens. Derde: bekijk de lengteberekening in actie: drie bytes erin, vier tekens eruit, geen padding nodig omdat de lading net uitkomt. Als de lading niet net uitkomt, landen er één of twee =-tekens aan het einde.
En omdat een encoder die je niet vertrouwt erger is dan geen, is hier de ronde rit die bewijst dat de twee richtingen het eens zijn:
text <- "Hello, world!"
packed <- base64encode(charToRaw(text))
packed
#> [1] "SGVsbG8sIHdvcmxkIQ=="
identical(text, rawToChar(base64decode(packed)))
#> [1] TRUE
Strings, bytes en de bestandsnaamval
Nu de val, want elke R-ontwikkelaar valt er één keer in. base64encode() behandelt een character-argument als een bestandsnaam, niet als tekst om te coderen. Geef hem een string en hij gaat op zoek naar dat bestand:
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"
De waarschuwing is het verradersignaal: hij probeerde file("Man", "rb"), wat betekent "open een bestand genaamd Man om het als raw vector te lezen". Bij base64encode() is de discipline dus één reflex: eerst charToRaw(), altijd. Als een bestand écht is wat je bedoelt, is dat precies wat de functie doet, en is de uitvoer de ingepakte bytes van het bestand, wat soms precies is wat je wilt.
b64 neemt een andere positie in over dezelfde vraag: zijn encode() accepteert een character-vector direct, behandelt elk element als UTF-8-tekst, en is bovendien vectorgebaseerd:
b64::encode("Man")
#> [1] "TWFu"
b64::encode(c("Man", "M"))
#> [1] "TWFu" "TQ=="
Nog twee randen van deze grens zijn het weten waard. De lege invoer wordt op drie verschillende manieren gecodeerd, afhankelijk van wie je het vraagt:
base64encode(raw(0))
#> character(0)
b64::encode("")
#> [1] ""
base64url::base64_urlencode("")
#> [1] ""
base64enc antwoordt met een character-vector van lengte nul, niet met een lege string, dus downstream-code die een string verwacht en character(0) krijgt, faalt op verrassende plekken. En het tekenset-besluit woont ook aan de encode-zijde: charToRaw() pakt de string in de codering die hij momenteel draagt, dus UTF-8-tekst reist als UTF-8-bytes, en dat is wat de andere kant van de draad verwacht. Emoji inbegrepen, want moderne R slaat codepoints boven U+FFFF op als echte UTF-8:
emoji <- "\U0001F600"
nchar(emoji, type = "bytes")
#> [1] 4
round_trip <- base64decode(base64encode(charToRaw(emoji)))
identical(emoji, rawToChar(round_trip))
#> [1] TRUE
Regelafbreken: MIME, PEM en je eigen breedte
Lange Base64-strings worden in regels gebroken, want de oudste tekstkanalen ter wereld hadden kolomlimieten, en MIME heeft zich nooit de tijd genomen om die te vergeten. De twee historische vormen van afbreken die je zult tegenkomen, zijn MIME, dat breekt op 76 tekens met CRLF tussen de regels, en PEM, dat breekt op 64. Elke encoder heeft zijn eigen idee over welke daarvan, als er al één is, te gebruiken is, dus dit is het deel waar je het contract kiest voordat je encodeert.
Eerst de groottewiskunde, want afbreken gaat over weten hoe lang de uitvoer wordt. Elke drie bytes geven vier tekens, waardoor de lengte van de gecodeerde vorm een eenvoudig plafond is:
nchar(base64encode(charToRaw(strrep("a", 100))))
#> [1] 136
4 * ceiling(100 / 3)
#> [1] 136
Met dat bij de hand, de encoders. base64enc is het meest flexibel: standaard geeft hij één onafgebroken regel, en het linewidth-argument geeft je in plaats daarvan een vector van regels:
long <- charToRaw(strrep("R", 100))
base64encode(long, linewidth = 76)
#> [1] "UlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJS"
#> [2] "UlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUg=="
base64encode(long, linewidth = 76, newline = "\r\n")
#> [1] "UlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJS\r\nUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUg=="
Twee regels voor 100 bytes bij breedte 76, standaard een vector, één CRLF-verbonden string als je newline toevoegt. Er is geen leeg element aan de staart: 114 bytes, die coderen naar exact twee regels van 76, komen terug als twee regels, niet drie.
openssl neemt de andere pol. linebreaks = TRUE breekt af op 64 tekens met gewone LF-breekpunten, en voegt helemaal aan het einde nog een regeleinde toe:
wrapped <- openssl::base64_encode(charToRaw(strrep("a", 100)), linebreaks = TRUE)
nchar(wrapped)
#> [1] 139 # 136 datatekens plus 3 regeleindes
nchar(strsplit(wrapped, "\n", fixed = TRUE)[[1]])
#> [1] 64 64 8
Tel de rekenkunde: 136 datatekens, twee breekpunten erin, één breekpunt aan de staart, 139 totaal. En dat breekpunt aan de staart is onzichtbaar voor de meest natuurlijke regetelling die je kunt schrijven, want strsplit() laat een leeg stukje aan de staart vallen, zodat de vector drie regels zegt terwijl de string er vier meedraagt. Als je ooit een diff maakt van een OpenSSL-gebroken uitvoer tegenover een MIME-gebroken uitvoer en de tekentallen kloppen niet, dan is dit het spook.
b64 breekt helemaal niet af; hij geeft je de twee operaties apart, en de chunkbreedte heeft één regel, een veelvoud van vier, want de engine weigert een Base64-groep in tweeën te kappen:
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.
En het bestandsgerichte base64-pakket volgt OpenSSL's voorbeeld: regels van 64 tekens, een regeleinde aan de staart, standaard aan:
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
Die laatste nul is het regeleinde aan de staart, opgepakt door readLines() als lege laatste regel. Hier is het hele veld in één keer:
| Encoder | Breedte | Regeleinde | Regeleinde aan de staart |
|---|---|---|---|
base64encode(x) |
één regel | geen | geen |
base64encode(x, linewidth = 76, newline = "\r\n") |
76 | CRLF | geen |
openssl::base64_encode(x, linebreaks = TRUE) |
64 | LF | ja |
b64::encode(x) |
één regel | geen (afbreeken met b64_chunk() en b64_wrap()) |
geen |
base64::encode(in, out) |
64 | LF | ja |
base64url::base64_urlencode(x) |
één regel | geen | geen |
URL-veilige Base64
Standaard Base64 besteedt de laatste twee plekken van zijn alfabet uit aan + en /, en die zijn precies de tekens die URL's niet leuk vinden: plus wordt %2B, slash wordt %2F, en elk =-teken van padding wordt %3D. De URL-veilige variant, gedefinieerd in sectie 5 van RFC 4648, ruilt die twee letters in voor - en _ en laat meestal ook de padding weg, zodat een token dat overal geplakt zou kunnen worden, overal plakbaar blijft. R heeft drie deuren ernaar.
De engines van b64 zijn het meest compleet: een engine is een geconfigureerd alfabet en paddingbeleid, en het pakket levert de vier die je nodig hebt mee. Voed dezelfde drie bytes aan de standaard-engine en de URL-veilige en zie toe hoe het alfabet zijn werk doet:
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"
Vier engines, "standard", "standard_no_pad", "url_safe" en "url_safe_no_pad", en hetzelfde engine-object werkt in beide richtingen, waardoor je code symmetrisch blijft. De paddingloze varianten verschillen alleen als padding daadwerkelijk zou verschijnen, zoals in het één-byte-voorbeeld hierboven.
Het toegewijde base64url-pakket is de deur met één doel: character erin, URL-veilige string eruit, breekt nooit af, padt nooit:
base64url::base64_urlencode("hello world")
#> [1] "aGVsbG8gd29ybGQ"
En jose exporteert zijn eigen base64url_encode() voor JWT-werk, en levert aan de decode-zijde raw vectors terug zoals je zou hopen. Eén regel bindt alle drie de deuren: het alfabet waarmee je codeert, is het alfabet waarmee je moet decoderen. Geef een standaarddecoder de string "----" en hij faalt op het eerste streepje; het zusterartikel over decoderen dekt hoe elke decoder omgaat met vuile of afwijkende invoer.
JWT's: het token maken
De JSON Web Token is waar URL-veilige Base64 het dagelijkse werkpaard werd. Een JWT is drie base64url-delen die met punten aan elkaar zijn gekoppeld: een header die beschrijft hoe het is ondertekend, een payload van JSON-claims, en een handtekening die de twee aan elkaar bindt. Aan de encode-zijde bouw je alle drie, en het jose-pakket doet het hele ritueel. Zijn API is opgebouwd rond de jwt_*-functies (jwt_claim(), jwt_encode_hmac(), jwt_decode_hmac(), jwt_split()); de 2.0-release (april 2026) voegt ED25519-ondersteuning toe en maakt het typ-header-veld optioneel:
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"
Let op wat jwt_claim() achter de schermen deed: iat (uitgegeven op) staat standaard op de huidige tijd, dus het verscheen in de payload zonder dat ernaar gevraagd was, terwijl exp standaard op niets staat, wat betekent een token zonder vervaldatum, en dat is meestal niet wat je wilt. Stel exp bewust in, en de handtekening neemt de rest voor haar rekening. jwt_split() is het inspectietool: de header, de payload als benoemde lijst, en de raw handtekening, zonder verificatie, precies de blikstap die de decode-zijde van dit artikel-paar beschrijft.
De verificatie-zijde is waar jose zijn kost verdient. jwt_decode_hmac() controleert de handtekening en handhaaft de tijds-claims, en zijn weigerstijl is specifiek:
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
Bij succes krijg je de claims terug als een gewone lijst, dus round$sub en vrienden werken gewoon. Een toekomstige nbf (niet vóór)-claim verdient zijn eigen weigering, Token is not valid before ..., en een HMAC-token wordt niet gedecodeerd door de asymmetrische jwt_decode_sig(), die antwoordt met Unsupported algorithm: HMAC en in plaats daarvan een publieke sleutel wil. Twee laatste opmerkingen. Eerst is de payload gecodeerd, niet versleuteld: iedereen kan elke claim lezen, dus hoort er niets geheim in. Ten tweede: als je httr2 laadt na jose, let dan op de masking-melding: httr2 exporteert zijn eigen jwt_claim(), jwt_encode_hmac() en jwt_encode_sig(), gebouwd voor OAuth client credentials met exp standaard op vijf minuten vooruit, en die maskeren de jose-versies voor de rest van de sessie.
Bestanden: binair erin, tekst eruit
Een bestand encoderen is de spiegel van het bestandswerk aan de decode-zijde, en het b64-pakket heeft de helderste ingang, die ook de snelste is, want hij bouwt nooit één enorme tussenstring:
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"
De waarschuwing is een eigenschap in vermomming: cat() voegt geen regeleinde aan de staart toe, dus het bestand eindigt halverwege een regel, en readLines() zegt het je. Onthoud aan welke kant van dit feit je staat als het bestand gegeten wordt door b64::decode_file(), dat in paniek raakt bij een regeleinde aan de staart; het decode-artikel heeft het complete verhaal. Schrijf met cat() of writeBin(charToRaw(enc), path) en de rand blijft dof.
Het base64-pakket is de pure bestandsnaar-bestandsoptie, met het passende paar functies en OpenSSL-stijl afbreken als standaard:
base64::encode("payload.bin", "payload2.b64")
readLines("payload2.b64")
#> [1] "ZmlsZSBwYXlsb2FkIGJ5dGVz" ""
Hij geeft het uitvoerpad terug, wat handig is voor logging. En als je de machine wilt zien, werkt de handmatige pipeline met elke encoder in de tabel: lees het bestand als raw, codeer, schrijf de tekst, klaar:
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
Data-URI's en zelfstandige documenten
Het data:-URI-schema (RFC 2397) is de meest zichtbare consument aan de encode-zijde: een document dat zijn eigen inhoud meedraagt, een MIME-type, het woord "base64", en de payload, allemaal in één attribuut. De magische bytes van PNG maken het patroon herkenbaar: elke Base64-PNG in het wild begint met dezelfde tekens, want de bestandsheader 89 50 4E 47 codeert altijd op dezelfde manier:
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, "\" />")
Als je ooit een HTML-bestand hebt doorzocht met grep op iVBOR om ingebedde afbeeldingen te vinden, dan is dit waarom de vingerafdruk werkt. R Markdown-rapporten, dashboards in één bestand en scraped pagina's gebruiken allemaal dezelfde vorm, en er een bouwen volgt hetzelfde patroon: lees het bestand als raw, codeer, en plak het achter het MIME-prefix. De prijs is de groottewiskunde van hierboven, een derde groter dan het origineel, voor altijd in je HTML, dus houd de ingebedde afbeeldingen slank.
API's en webrequests
De decode-zijde van dit artikel-paar ontmoet API's die je Base64 aanreiken; deze zijde ontmoet API's die het willen. Het patroon is elke keer hetzelfde: codeer de bytes, zet de string in het JSON-lichaam, stuur hem. De moderne HTTP-client is 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
Drie opmerkingen voor onderweg. De request-constructor is request(), en die draagt die naam sinds de eerste release van httr2. Het responslichaam arriveert, als je responses leest, als raw vector, dus rawToChar() voordat je ontledt. En de API kan een dialect spreken: sommigen willen het URL-veilige alfabet, sommigen willen de padding ontdaan, en een JSON-API heeft geen probleem met = in een string-waarde, zodat de paddingvraag pas een URL-vraag wordt als de Base64 reist in het pad of de query. Bij twijfel: lees de voorbeelden van de API, niet de specificatie in je hoofd.
Databases, configuratie en omgeving
Base64 in een database is binair dat is gesmokkeld door een tekstkolom, en de encode-zijde is een blob inpakken voordat die erin gaat. Hier is de ronde rit tegen SQLite via DBI en 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"
Het alternatief is de bytes natief op te slaan als BLOB, in welk geval helemaal geen Base64 nodig is en de kolom naar R terugkomt als raw vector. De Base64-in-TEXT-variant bestaat voor draagbaarheid: je kunt het inspecteren met een teksteditor, er een diff van maken, en elke andere taal kan het lezen zonder binaire drivers. Zelfde argument draagt het mee naar configuratie, waar een certificaat of geheim is opgeslagen als string in YAML, JSON of een omgevingsvariabele:
Sys.setenv("API_CERT" = base64encode(charToRaw("LTSSECRET")))
Sys.getenv("API_CERT")
#> [1] "TFRTU0VDUkVU"
Eén waarschuwing hoort hier, want configuratiebestanden zijn waar geheimen wonen: Base64 is coderen, niet versleutelen. Een Base64-waarde in een configuratiebestand is leesbaar voor iedereen die het bestand kan lezen. Het overleeft transport en teksteditors; het beschermt niets.
E-mail is waar het 76-teken-afbreken is geboren, en MIME-bijlagen dragen het nog steeds: Base64-inhoud, gebroken op 76 tekens met CRLF tussen de regels, in een deel dat Content-Transfer-Encoding: base64 declareert. De encoder die precies die vorm produceert, is base64encode() met zijn afbreek-argumenten ingesteld op het MIME-contract:
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 heeft geen eerste-klasses mailclient, maar de stelling gaat op telkens je MIME-delen handmatig bouwt of inspecteert, .eml-fixtures genereert, of bijlagen uit zo'n bestand ontledt: dit is de vorm die de Base64 moet hebben, en de decode-zijde van dit artikel-paar toont de tolerante decoders die hem onderweg terug ontwarren.
Grote payloads en het string-plafond
R-strings hebben een hard plafond van 2^31 - 1 bytes, en omdat de gecodeerde vorm ongeveer een derde groter is dan het origineel, zou een bestand van ruwweg 1,5 GB rauwe data zijn één-regel-codering over de muur duwen. De praktische zet is dezelfde die base64enc biedt sinds zijn 2022-release met lange vectoren: houd de uitvoer aan als regels, niet als één string:
big <- raw(10 * 1024 * 1024)
packed <- base64encode(big, linewidth = 76)
length(packed)
#> [1] 183961
sum(nchar(packed))
#> [1] 13981016
Tien megabytes aan nullen worden 183961 regels van hooguit 76 tekens, een volkomen gewone vector die je regel voor regel weg kunt schrijven of door een pipe kunt streamen zonder ooit één enorme string te hoeven houden. Voor het data frame-geval, een kolom met veel binaire waarden, is b64 de snelheidskampioen: zijn Rust-engine codeert de hele kolom in één vectorgebaseerde aanroep, en dat is een groot verschil met per regel lopen. Als je een kolom met gecodeerde waarden moet produceren, voer dan zelf een snelle system.time()-vergelijking uit; het gat tussen een loop per regel en één vectorgebaseerde aanroep is meestal groot genoeg om ertoe te doen.
De opdrachtregel
Niet alles heeft een volledige R-sessie nodig. De klassieke Unix-tool spreekt Base64 natief, codeert zonder vlaggen op elk platform, en decodeert met -d op Linux en met -D op macOS en de BSD's:
echo -n "Hello, world!" | base64
#> SGVsbG8sIHdvcmxkIQ==
echo -n "SGVsbG8sIHdvcmxkIQ==" | base64 -d
#> Hello, world!
Let op de -n in de eerste regel: zonder die geeft echo een regeleinde mee, en de uitvoer codeert 14 bytes in plaats van 13, eindigend in o= in plaats van IQ==. Een GNU base64 breekt ook voor je af (-w 76), wat handig is als je via een pipe stuurt naar iets dat MIME-vormige invoer verwacht. En een Rscript van één regel doet hetzelfde werk met dezelfde pakketten die je in je scripts gebruikt:
Rscript -e 'library(base64enc); writeLines(base64encode(charToRaw("Man")))'
#> TWFu
Gebruik de shell voor snelle checks en pipes; gebruik R wanneer het resultaat moet leven in een data frame, een bestand of een rapport. En plak geen strings van meerdere megabytes in de terminal: opdrachtregel-argumenten raken ARG_MAX lang voordat Base64 dat doet, dus stuur via een bestand.
Valkuilen die het weten waard zijn
Hier is de korte lijst met de manieren waarop de encode-zijde R-ontwikkelaars beetneemt, en allemaal inheems aan het ecosysteem, niet aan Base64 in het algemeen:
- Een string is een bestandsnaam.
base64encode("Man")probeert een bestand genaamd Man te openen. De waarschuwing noemt het bestand; de fout zegt dat de connectie mislukte. Gebruik bijbase64encode()eerstcharToRaw(). - Lege invoer wordt op drie manieren gecodeerd.
base64encode(raw(0))geeftcharacter(0)terug, terwijlb64::encode("")enbase64url::base64_urlencode("")""teruggeven. Downstream-code voor strings verwacht geen vector van lengte nul. - openssl breekt af en voegt dan een spookregel toe.
linebreaks = TRUEbreekt op 64 met LF af en voegt een regeleinde aan de staart toe datstrsplit()stilletjes laat vallen, zodat naïeve regetellingen en tekentellingen één regel uit elkaar liggen. - Afbreken is een contract. MIME is 76 met CRLF, PEM is 64, JSON-API's willen meestal helemaal niets. Kies de vorm die de andere kant verwacht, want een decoder die één afbrekstijl accepteert, weigert er een andere.
- Het URL-veilige alfabet moet aan beide kanten matchen.
"----"URL-veilig gecodeerd, faalt in een standaarddecoder op het eerste streepje, en padding wordt%3Dop het moment dat de string in een URL woont. - b64_chunk eist veelvouden van vier. Elke andere breedte verdient
Chunk size must be a multiple of 4., want een Base64-groep kan niet in tweeën worden gekapt. - Een regeleinde aan de staart kan de decoder doen panikeren. Bestanden die je schrijft voor
b64::decode_file()om te lezen, moeten eindigen zonder regeleinde;cat(), nietwriteLines(). - JWT-tijdsclaims worden gehandhaafd.
jwt_decode_hmac()weigert verlopen tokens en toekomstigenbf-claims, enhttr2maskeert dejwt_*-functies vanjoseals je hem als tweede laadt. - De payload is zichtbaar. Base64-claims in een JWT, een data-URI of een configuratiebestand zijn leesbaar voor iedereen. Coderen is geen versleuteling.
- Het string-plafond is een muur, geen richtlijn. Ruwweg 1,5 GB rauwe data per R-string is waar een één-regel-codering ophuilt te passen; houd grote uitvoer aan als regels of stream hem.
Beste praktijken
- Zet bij
base64enc::base64encode()eerst om metcharToRaw()voordat je encodeert. Als je directe character-invoer en vectorisering nodig hebt, isb64::encode()het pakket dat strings als strings behandelt. - Standaard gebruik je
base64enc::base64encode()voor dagelijks werk, grijp naarb64als je snelheid, échte vectorisering of de URL-veilige engines wilt, en gebruikopensslals het al in het project zit en je PEM-vormige uitvoer wilt. - Kies het afbreken per kanaal, niet per pakket: niets voor JSON en URL's, 76 met CRLF voor MIME, 64 voor PEM-stijl blokken, en blijf consequent, zodat de decoder-kant van de pipe weet wat te verwachten.
- Gebruik het URL-veilige alfabet zonder padding voor alles dat in een URL, een JWT of een bestandsnaam moet leven, en het standaardalfabet voor e-mail.
- Voor JWT's stel
exp(eniat) expliciet in, verifieer metjwt_decode_hmac()voordat je een token vertrouwt, en onthoud dat elke claim publieke tekst is. - Test je encode-paden met een ronde rit,
identical(text, rawToChar(base64decode(base64encode(charToRaw(text))))); het is één regel en het vangt alfabet-, padding- en tekensetfouten in één keer. - Voor bestanden: geef de voorkeur aan
b64::encode_file()ofbase64::encode()boven het hele bestand in één string lezen, en schrijf je uitvoer metcat()als een strikte decoder hem gaat lezen. - Houd de pakband eerlijk: Base64 maakt bytes reizen, het maakt ze niet geheim en het maakt ze niet kleiner. Versleutel eerst als geheimheid het doel is, compresseer eerst als grootte dat is.
Hoe Base64 bij R terechtkwam
Het formaat arriveerde lang voordat R ermee iets deed. Het werd gestandaardiseerd voor het Privacy-Enhanced Mail-protocol in 1987 (RFC 989), overgenomen door MIME in 1993 (RFC 1521, toen de definitieve RFC 2045 in 1996, die nog steeds het 76-teken-afbreken definieert), opgeschoond in RFC 3548 in 2003, en in RFC 4648 in 2006 zijn moderne vorm gegeven, die het URL-veilige alfabet, de optie zonder padding, en de kleinere broer Base32 toevoegde. Het R-verhaal begon in september 2012, toen base64enc van Simon Urbanek op CRAN landde en stilletjes het standaardantwoord werd op "hoe Base64 ik dit" voor ruim een decennium, met checkUTF8() toegevoegd in 2015 en ondersteuning voor lange vectoren in 2022. De cryptowereld kwam binnen via openssl, het langlopende wrapper-pakket van Jeroen Ooms rond de systeem-OpenSSL, waarvan base64_encode() sindsdien de PEM-vormige optie is. Het oude base64-pakket, ook van Ooms, werd in oktober 2024 expliciet opnieuw uitgegeven als compatibiliteitswrapper, en de eigen beschrijving wijst nieuwe toepassingen nu ergens anders naartoe. Dan kwam b64 in januari 2024, een Rust-engine gebouwd met extendr die vectorisering en een stal alfabetten bracht, en in april 2026 bracht jose versie 2.0 uit, die ED25519-ondersteuning toevoegde aan de jwt_*-API, waardoor ondertekende tokens een eerste-klas-burger werden. Het resultaat is een gereedschapskist met één encoder per taak: dagelijkse strings, PEM-blokken, snelheid, URL's, bestanden en tokens.
Grappig hoekje
Omdat een complete gids moet eindigen met een glimlach:
- Elke Base64-PNG op internet begint met dezelfde tekens: de magische header 89 50 4E 47 codeert naar
iVBOR, dus als je een HTML-bestand doorzocht met grep op die vingerafdruk, vind je elke ingebedde afbeelding. Formaten hebben vingerafdrukken, en deze is een voorvoegsel. base64encode("Man")codeert het woord Man niet. Hij gaat op zoek naar een bestand genaamd Man, waarschuwt dat hij het niet kan openen, en geeft het op. De meest R-specifieke val in het Base64-ecosysteem, in alle openlijkheid verborgen in de argumentenlijst.- Een OpenSSL-gebroken string eindigt altijd met een regeleinde aan de staart, dus de laatste regel van een PEM-blok is nooit de laatste regel van het bestand. Het afbreken heeft een punt aan het einde van de zin, of je dat wilt of niet.
- De lege string wordt op drie verschillende manieren gecodeerd:
base64encgeeft een vector van nul strings,b64enbase64urlgeven de lege string. R ontmoet niets, en R geeft drie antwoorden. - jose schrijft de JWT-header met
typvóóralg, terwijl de meeste handgeschreven voorbeeldenalgeerst zetten. JSON geeft niet om de volgorde van sleutels, en JWT-verificatie weet dat, maar je string-diff doet dat niet. - De 76-tekenslimiet van MIME is een beslissing uit 1993 over de lengte van berichtregels, die via vier RFC's is gekomen in elke e-mailbijlage die je ooit hebt verstuurd. Het afbreken dat je vandaag instelt, werd besproken voordat R een kleurplot had.
b64decodeert alfabetten die je nooit hebt gezien, BinHex, IMAP modified UTF-7, bcrypt, crypt en het URL-veilige paar, elk met één engine. Dezelfde Rust-code die je JSON-veld inpakt, kan een Macintosh-bijlage uit de jaren tachtig uitpakken.
Samenvatting
Kies je encoder naar de taak: base64enc voor dagelijkse strings, met linewidth en newline als het kanaal een vorm heeft; openssl als je PEM-stijl uitvoer van 64 tekens wilt of het al in het project zit; b64 als je snelheid, vectoren of de URL-veilige engines wilt; en de kleine specialisten base64 en base64url voor bestandsklusjes en URL-veilige strings. Zet om met charToRaw() voordat je encodeert met base64enc::base64encode(), kies het afbreken per kanaal in plaats van per pakket, houd het URL-veilige alfabet voor alles dat in een URL of een JWT moet leven, onderteken en verifieer tokens met jose, en test elk pad met een ronde rit. Het formaat zelf is ongenadig op precies twee plekken, het alfabet en de regeleindes, en de encoders verschillen vooral in hoe eerlijk ze je vertellen als je er één van die twee fout doet. En als de andere richting roept - wanneer een string aankomt en je hem uit elkaar moet halen, controleren wat de bytes betekenen, en overleven van de decoders die stilletjes falen - dan dekt het zusterartikel Base64-decoderen in R in detail.
Laatst bijgewerkt: 2026-10-06
Gerelateerd artikel: Base64-decodering in R: een complete gids