Base64-decodering in R: een complete gids
Een string belandt in je R-sessie. Het lijkt op een door elkaar geschudde alfabetsoep: letters, cijfers, af en toe een plus of een slash, en een paar equals-tekens die aan het einde ophangen. De persoon die het stuurde zweert dat het ooit een volstrekt gewone zin was, een JPEG of een JSON-document. Die string is Base64, en deze pagina is het recept om het terug te brengen naar wat het was.
Even een herhaling, want de startpagina van deze site legt het formaat in volle diepte uit: Base64 schrijft drie invoerbytes weg als vier tekens uit een alfabet van 64 symbolen, en één of twee =-tekens aan de staart markeren waar de echte data ophield. Die ruil van vier voor drie is de reden waarom gecodeerde tekst ongeveer een derde groter is dan het origineel, en decoderen draait de ruil simpelweg om. Houd de vorm van het probleem in je hoofd en laten we een paar enveloppen openen.
Hier is de wending die R een beetje anders maakt dan vele andere talen: base R heeft helemaal geen Base64. Er zit geen base64_decode() verborgen in een base-pakket, en er is geen ingebouwde functie van één regel waar je naar kunt grijpen. Je moet zelf een pakket meenemen. Het goede nieuws is dat het ecosysteem er verschillende aanbiedt, elk met zijn eigen persoonlijkheid, en tegen het einde van dit artikel weet je precies naar welke je grijpt en welke je met wantrouwen behandelt.
De decoders in één oogopslag
Vijf pakketten doen het zware werk, en ze vallen in twee grote kampen: de tolerante, die over vuile invoer alleen maar de schouders ophalen, en de strikte, die de RFC als een contract behandelen. Hier is het gezelschap, actueel per 2026:
| Pakket | Versie (2026) | Decode-ingangen | Persoonlijkheid |
|---|---|---|---|
base64enc |
0.1-6 | base64decode() |
Standaard tolerant, kreeg in februari 2026 een strict-modus bij |
openssl |
2.4.2 | base64_decode() |
Slaat witruimte over, maar faalt op stille manieren die een strenge blik verdienen |
b64 |
0.1.7 | decode(), decode_as_string() |
Strikt, vectorgebaseerd, geschreven in Rust, snel |
base64 |
2.0.2 | decode() |
Handige bestandsnaar-bestands-wrapper rond openssl |
base64url |
1.4 | base64_urldecode() |
URL-veilig alfabet, geen padding, stilletjes vergevingsgezind |
Drie verdere kandidaten verdienen een vermelding. Het jsonlite-pakket exporteert zijn eigen helpers, base64_enc, base64_dec en het URL-veilige paar, dus als je JSON al ontledt, heb je misschien al een decoder in huis. Het jose-pakket levert base64url_decode() mee voor JWT-werk. En het ouderwetse RCurl-pakket draagt nog steeds een base64()-functie die libcurl inpakt: het werkt, het is karaktergeoriënteerd, en het heeft het gevoel van een trouwe grootvader, niet van een tool voor nieuwe code.
Het gezelschap installeren
Als R nog niet op de machine staat, levert je besturingssysteem hem: r-base op Debian en Ubuntu, R op Fedora, een pakket of installer op macOS en Windows. Daarna de pakketten, rechtstreeks vanuit CRAN:
install.packages("base64enc")
install.packages("openssl")
install.packages("b64")
install.packages("base64url")
Twee build-opmerkingen, want op deze plekken gaan installaties mis. Het openssl-pakket compileert tegen je systeem-OpenSSL, dus een kale Linux-machine wil misschien eerst de ontwikkelheaders:
sudo apt install libssl-dev
Het b64-pakket is een Rust-engine die met extendr is ingepakt, dus een build vanaf de bron wil de Rust-toolchain (sudo apt install cargo trekt rustc er ook bij). Op Windows en macOS krijg je voorgebouwde binaries van CRAN en dan geldt dit allemaal niet. Als je liever een pakketbeheerder gebruikt, doen pak::pkg("base64enc") of remotes::install_cran("b64") hetzelfde werk, met minder meningen over pakketbronnen.
Je eerste keer decoderen: het ritueel van drie regels
Negentig procent van het decoderen past in drie regels. Hier is de standaard rooktest, met de beroemde TWFu-string:
library(base64enc)
packed <- "TWFu"
bytes <- base64decode(packed)
bytes
#> [1] 4d 61 6e
rawToChar(bytes)
#> [1] "Man"
In dat kleine rituelletje zijn drie dingen de moeite om op te merken. Eerst levert base64decode() altijd een raw vector aan, nooit een string. Dat is een eigenschap, geen toeval: Base64 kan een zin, een JPEG of een certificaat dragen, en geen van die mag anders behandeld worden voordat je weet wat je hebt. Tweede is dat de sprong van bytes terug naar tekst een aparte, bewuste stap is via rawToChar(), en dat is de stap waar het tekenset-besluit woont (meer daarover later). Derde is dat TWFu decodeert naar het woord "Man": drie letters, nul drama. Houd die string bij de hand. Als een stuk decodeercode TWFu naar Man tovert, is de machine eerlijk.
Omdat je uiteindelijk altijd iets decodeert dat je zelf hebt gecodeerd, is hier de volledige 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
De grens van de raw vector
R kruist de byte/tekstgrens met twee kleine functies, en zodra je ze kent voelt de rest van dit artikel vanzelfsprekend. charToRaw() zet een string om in zijn bytes, rawToChar() doet het omgekeerde, en daartussen heb je de hele raw-gereedschapskist: length() om bytes te tellen, head() om te kijken, writeBin() en readBin() om ze naar en vanuit bestanden te verplaatsen. Elke decoder in dit artikel stopt op die grens, opzettelijk.
bytes <- base64decode("SGVsbG8s")
length(bytes)
#> [1] 6
head(bytes)
#> [1] 48 65 6c 6c 6f 2c
rawToChar(bytes)
#> [1] "Hello,"
Dus als je een resultaat als [1] 48 ziet, lees het dan als "één byte, waarde 72, de letter H", en stop daar tot je weet welke tekenset de bytes dragen. Die pauze is de hele discipline van decoderen.
Vuile invoer: hoe de decoders het niet eens zijn
Dit is het deel dat je redt om 2 uur 's nachts, want de decoders zijn het niet eens over wat er gebeurt als de invoer een beetje vuil is. Echte Base64 uit de praktijk arriveert met spaties erin, padding die door een nerveus copy-paste is weggelaten, een vreemd symbool van een klembord dat het heeft opgegeten, of rommel die aan het einde meeloopt. Hier is hoe elke decoder antwoordt, op dezelfde familie overtreders:
| Invoer | base64enc (standaard) | base64enc (strict = TRUE) | openssl | b64 |
|---|---|---|---|---|
"SGVs bG8s IHdvcmxkIQ==" (een spatie erin) |
"Hello, world!" (spatie overgeslagen) | fout: ongeldig teken, positie wordt opgegeven | "Hello, world!" (spatie overgeslagen) | fout |
"SGVsbG8sIHdvcmxkIQ" (padding weggelaten) |
"Hello, world!" (ontbrekende padding geaccepteerd) | fout: ontbrekende padding | fout: decoderen mislukt | fout: ongeldige padding |
"SGVsbG8s!IHdvcmxkIQ==" (een "!" erin) |
"Hello, world!" (slecht teken overgeslagen) | fout: ongeldig teken | lege raw vector, geen fout | fout |
"SGVsbG8sIHdvcmxkIQ==xx" (rommel aan het einde) |
"Hello, world!" (rommel genegeerd) | fout: inhoud aan de staart | lege raw vector, geen fout | fout |
"TQ==" (schoon) |
M | M | M | M |
Lees die tabel twee keer, want hij bevat het hele verhaal. De standaard base64decode() is de vriendelijke douanier: hij slaat tekens buiten het alfabet over, accepteert ontbrekende padding en negeert inhoud aan de staart, dus strings in de vorm van e-mail of van een klembord gaan gewoon door. De strict = TRUE-modus, die in februari 2026 na een lange stilte met release 0.1-5 arriveerde, is de forensische onderzoeker: hij valideert de hele string, noemt de exacte positie van de overtreding en weigert alles wat niet in het leerboek staat. b64 is standaard strikt en slaagt nooit deels. En openssl zit in het ongemakkelijke midden: hij slaat graag witruimte over en geeft terecht een fout bij ontbrekende padding, maar als hij een echt illegaal teken of rommel aan de staart tegenkomt, levert hij een lege raw vector en zegt helemaal niets. Die stille lege uitkomst is het gevaarlijkste gedrag in dit hele artikel, dus laten we het gebeuren:
dirty <- "SGVsbG8s!IHdvcmxkIQ=="
openssl::base64_decode(dirty)
#> raw(0) # leeg. geen fout. geen waarschuwing. geen aanwijzing.
Als je code schrijft tegen openssl, controleer dan de lengte van wat je krijgt voordat je het vertrouwt. Het is een kleine gewoonte die een hele klasse aan "waar is mijn data gebleven"-mysterieën voorkomt.
Voor het strenge kamp is hier base64enc in zijn precisie, met de exacte foutmeldingen die je in je eigen console ziet:
base64enc::base64decode("TWF u", strict = TRUE)
#> v=30000, pad=0, org='u'
#> Error: Invalid character (' ') at position 4 in base64 string (not allowed in strict mode)
base64enc::base64decode("TWF", strict = TRUE)
#> v=10000, pad=3, org=''
#> Error: Missing padding (1 characters) at the end of the base64 string (not allowed in strict mode)
base64enc::base64decode("TWF=xx", strict = TRUE)
#> v=20000, pad=0, org='xx'
#> Error: Trailing content 'xx' after padding at position 4 in base64 string (not allowed in strict mode)
Eén rareiteit om te verwachten: elke strikte aanroep, of ze nu slaagt of faalt, praat een statusregel op C-niveau naar de console, iets als v=30000, pad=0, org='u'. Het is een directe schrijfoverdracht van de gecompileerde code, geen R-waarschuwing, dus suppressWarnings() raakt het niet aan. Het is onschuldig, maar als je tests console-uitvoer vergelijken, weet je nu waarom er een extra regel is.
R-eigenaardigheden: NA, vectoren en stille concatenatie
Base64 heeft zijn eigenaardigheden; R voegt de zijne eraan toe. De eerste bijt door NA heen. Als je NA_character_ aan een decoder geeft, zet R het stilzwijgend om naar de string "NA" voordat de decoder hem ooit ziet, en "NA" is een volkomen geldige Base64-groep. Het resultaat is geen fout en geen lege vector. Het is de byte 0x34, de letter "4":
base64decode(NA_character_)
#> [1] 34
rawToChar(base64decode(NA_character_))
#> [1] "4"
Een kolom met ontbrekende waarden decodeert dus niet naar ontbrekende waarden. Hij decodeert naar een kolom met de letter 4, en je downstream-code verwerkt hem vrolijk door. Als je invoer NA kan bevatten, filter dan eerst:
values <- c("TWFu", NA_character_, "TQ==")
clean <- values[!is.na(values)]
out <- vapply(clean, function(x) rawToChar(base64decode(x)), character(1))
unname(out)
#> [1] "Man" "M"
De tweede eigenaardigheid is vector-invoer, en de decoders slaan er heel verschillende wegen op in. base64decode() behandelt een character-vector als verschillende stukken van één string en voegt ze samen, zodat twee elementen terugkomen als één raw vector. openssl::base64_decode() komt daar niet eens: het vouwt zijn argument samen en botst daarna op een logische test met twee elementen, die een van R's eerlijkste foutmeldingen oplevert. En b64::decode() is de enige die écht vectorgebaseerd is, met één gedecodeerde blob per invoer:
base64decode(c("TWFu", "TQ=="))
#> [1] 4d 61 6e 4d
openssl::base64_decode(c("TWFu", "TQ=="))
#> Error in if (is.na(text)) : the condition has length > 1
out <- b64::decode(c("TWFu", "TQ=="))
rawToChar(out[[1]])
#> [1] "Man"
rawToChar(out[[2]])
#> [1] "M"
Conclusie: loop of vectoriseer bewust, en ga nooit ervan uit dat een vector van invoer een vector van uitvoer oplevert.
Het URL-veilige alfabet
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 padding wordt %3D, zodat een token dat overal geplakt zou kunnen worden, begint percenttekens te dragen. De URL-veilige variant, gedefinieerd in sectie 5 van RFC 4648, ruilt die twee tekens in voor - en _ en laat meestal ook de padding weg. R heeft drie deuren naar die wereld.
Deur één is de engines van b64. Een engine is niets meer dan een geconfigureerd alfabet en paddingbeleid, en het pakket levert de vier die je nodig hebt mee:
library(b64)
std <- encode(as.raw(c(0xfb, 0xef, 0xbe)))
std
#> [1] "++++"
url <- encode(as.raw(c(0xfb, 0xef, 0xbe)), engine("url_safe"))
url
#> [1] "----"
decoded <- decode(url, engine("url_safe"))[[1]]
toString(decoded)
#> [1] "fb, ef, be"
De engines zijn "standard" (de standaard), "standard_no_pad", "url_safe" en "url_safe_no_pad", en hetzelfde engine-object werkt in beide richtingen, waardoor je code symmetrisch blijft. Ze zijn ook strikt over hun alfabet: voer "----" in bij de standaard-engine en hij antwoordt met "Invalid byte 45, offset 0.", wat een verlichting is.
Deur twee is het toegewijde base64url-pakket, klein en met één doel: het URL-veilige alfabet, geen padding, altijd. Eén kanttekening: zijn decoder is tolerant, dus hij decodeert met plezier het geldige voorvoegsel van een beschadigde string en stopt daar zonder te klagen. Als een URL-veilige waarde van een onbetrouwbare bron komt, geef dan de voorkeur aan b64.
base64url::base64_urlencode("hello world")
#> [1] "aGVsbG8gd29ybGQ"
base64url::base64_urldecode("aGVsbG8g!!!")
#> [1] "hello " # de rest is stilletjes weggelaten
Deur drie is jose, dat base64url_encode() en base64url_decode() exporteert voor JWT-werk, en raw vectors teruglevert zoals je zou hopen.
En als je af en toe iets eenmaligs nodig hebt en binnen base64enc wilt blijven, is string-chirurgie plus een paddingreparatie genoeg:
url <- "----"
fixed <- gsub("_", "/", gsub("-", "+", url), fixed = TRUE)
padded <- switch(as.character(nchar(fixed) %% 4L),
"0" = fixed,
"2" = paste0(fixed, "=="),
"3" = paste0(fixed, "="))
rawToChar(base64decode(padded))
#> [1] "\xfb\xef\xbe"
JWT's: kijken en vertrouwen
De reden dat de meeste mensen URL-veilige Base64 tegenkomen, is de JSON Web Token. Een JWT is drie base64url-delen die met punten aan elkaar zijn gekoppeld: een header die beschrijft hoe het is ondertekend, een payload van claims, en een handtekening die het geheel betrouwbaar maakt. Het decoderen van de eerste twee delen in R is een kwestie van vijf regels. Let op de fixed = TRUE in strsplit(): het punt is een regex-wildcard, en zonder die vlag splijt R vrolijk je token op in losse tekens, en dat is de meest voorkomende Base64-bug in R-code in het wild.
jwt <- jose::jwt_encode_hmac(jose::jwt_claim(sub = "1234567890", iat = 1516239022, name = "John Doe"), "0123456789abcdef")
parts <- strsplit(jwt, ".", fixed = TRUE)[[1]]
header <- base64url::base64_urldecode(parts[1])
header
#> [1] "{\"typ\":\"JWT\",\"alg\":\"HS256\"}"
payload <- jsonlite::fromJSON(base64url::base64_urldecode(parts[2]))
payload$name
#> [1] "John Doe"
Twee eerlijke kanttekeningen. Eerst: een JWT decoderen is kijken, niet vertrouwen: het derde deel is de handtekening, en die zegt alleen iets als hij wordt gecontroleerd tegen de sleutel van de uitgever. Daarvoor behandelt het jose-pakket het hele gezin. De 2.0-release (april 2026) voegt ED25519-ondersteuning toe en maakt de typ-header optioneel; oudere tutorials die de 1.x jwk_read()/jwk_write()-helpers (of hun read_jwk()/write_jwk()-aliassen) tonen, blijven geldig, want 2.0 levert diezelfde JWK-lees- en schrijfhelpers nog steeds mee:
library(jose)
claims <- jwt_decode_hmac(jwt, "0123456789abcdef")
claims$name
#> [1] "John Doe"
claims$sub
#> [1] "1234567890"
sp <- jwt_split(jwt)
sp$header
#> $alg
#> [1] "HS256"
#>
#> $typ
#> [1] "JWT"
sp$payload$name
#> [1] "John Doe"
jwt_decode_hmac() verifieert de handtekening en handhaaft ook de exp- en nbf-claims, en geeft een fout als het token verlopen is. En ten tweede: base64url::base64_urldecode() en vrienden decoderen de header en payload zonder probleem, maar het handtekeningdeel is binair, dus decodeer het met een functie die raw teruggeeft (zoals b64::decode() met de "url_safe_no_pad"-engine) in plaats van naar een string.
Tekensets: welke tekst zijn die bytes?
Elke decoder in dit artikel stopt opzettelijk aan de grens van de raw vector, want het antwoord op "welke tekst was dat?" hangt af van de tekenset waarin de bytes zijn verpakt. Het goede nieuws vooraf: als de afzender UTF-8 gebruikte, en dat is het grootste deel van het moderne web, is je leven kort en gelukkig. base64enc levert er zelfs een sluis voor mee:
packed <- base64encode(charToRaw("caf\u00e9"))
bytes <- base64decode(packed)
checkUTF8(bytes, quiet = TRUE)
#> [1] TRUE
rawToChar(bytes)
#> [1] "café"
checkUTF8() vraagt "kunnen deze bytes als UTF-8 worden gelezen?" zonder zich vast te leggen. Met quiet = TRUE geeft hij TRUE of FALSE terug; standaard is hij nog directer en geeft een fout bij ongeldige bytes, en noemt de overtreding, zoals in INVALID byte 0xff at 0x0 (0, line 1): invalid start byte (FE/FF). Hij rapporteert ook FALSE voor strings die NUL-bytes bevatten, die nooit deel kunnen zijn van een geldige UTF-8 R-string. Gebruik het als sluis, en de rest volgt.
Hier is waarom de sluis belangrijk is. In een UTF-8-locale gooit rawToChar() geen fout als de bytes geen geldige UTF-8 zijn. Hij geeft je een string die R merkt met Encoding() == "unknown", en afhankelijk van locale en R-build kan dezelfde aanroep in plaats daarvan sterven met een "invalid multibyte sequence"-fout, die tenminste eerlijk is. In elk geval is een onbewaakte rawToChar() op vreemdere bytes hoe mojibake wordt geboren:
broken <- as.raw(c(0xc3, 0x28)) # een afgebroken UTF-8-paar
checkUTF8(broken, quiet = TRUE)
#> [1] FALSE
rawToChar(broken) # geen fout. dat is precies het probleem.
#> [1] "\xc3("
En als de afzender helemaal iets anders gebruikte, Latin-1 of Windows-1252 of Shift-JIS, dan is het recept om naar raw te decoderen en dan te vertalen met iconv(), dat de benoemde tekensets begrijpt:
latin1_bytes <- as.raw(c(0x63, 0x61, 0x66, 0xe9)) # "café" in Latin-1
iconv(rawToChar(latin1_bytes), from = "latin1", to = "UTF-8")
#> [1] "café"
Emoji verdienen een woordje, want R heeft hier een verhaal. Moderne R slaat codepoints boven U+FFFF, waar emoji wonen, op als echte UTF-8, dus een ronde rit werkt en het aantal bytes komt overeen met wat elke andere taal verwacht. Oudere R-releases slaarden diezelfde tekens op als surrogate-paren, een schema dat vaak CESU-8 wordt genoemd, zodat één emoji de grens overstak als zes bytes ongeldige UTF-8. Als je oude R-code overneemt waarvan de emoji kapot op de draad arriveren, is dat de hoofdverdachte: controleer nchar(x, type = "bytes") en vergelijk met wat de taal van de afzender zou produceren.
emoji <- "\U0001F600"
nchar(emoji, type = "bytes")
#> [1] 4
round_trip <- base64decode(base64encode(charToRaw(emoji)))
identical(emoji, rawToChar(round_trip))
#> [1] TRUE
De vuistregel: neem UTF-8 aan, verifieer met checkUTF8(), en grijp pas naar iconv() als je positieve kennis hebt van een andere tekenset. Raad nooit.
Bestanden: van omwikkelde tekst naar echte bytes
Strings zijn makkelijk; bestanden zijn waar Base64 zijn kost verdient. Begin met de handmatige pipeline die met elk pakket werkt: lees de gecodeerde tekst, decodeer, schrijf de bytes.
cat("SGVsbG8sIHdvcmxkIQ==", file = "hello.b64")
bytes <- base64decode(paste(readLines("hello.b64"), collapse = ""))
writeBin(bytes, "hello.txt")
readLines("hello.txt")
#> [1] "Hello, world!"
Dan de gemaklaag. base64enc kan voor je lezen en schrijven: het file-argument wijst naar een bestand met Base64-tekst, en output naar waar de gedecodeerde bytes moeten landen. De retourwaarde is het aantal geschreven bytes, een heerlijk gratis sanitycheck:
n <- base64decode(file = "hello.b64", output = "hello.txt")
n
#> [1] 13
readLines("hello.txt")
#> [1] "Hello, world!"
Omwikkelde invoer, het soort dat e-mail of PEM-pantsering heeft overleefd, is geen probleem voor de tolerante decoders: zij kijken er recht doorheen, waar de regeleinden ook zitten, zolang wat overblijft geldige Base64 is. MIME breekt af op 76 tekens met CRLF tussen de regels; PEM-blokken breken af op 64. De strikte engine weigert het afbreken natuurlijk stellig.
wrapped <- paste0("VGhlIHF1aWNrIGJ", "\r\n",
"yb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw==")
rawToChar(base64decode(wrapped))
#> [1] "The quick brown fox jumps over the lazy dog"
rawToChar(openssl::base64_decode(wrapped))
#> [1] "The quick brown fox jumps over the lazy dog"
b64::decode(wrapped)
#> Error: Invalid byte 13, offset 15.
Het b64-pakket heeft het bestandspaar met de helderste namen, en doordat het Rust-snel is, is het de keuze als het bestand groot is. Er is wel één scherpe rand: decode_file() leest het bestand byte voor byte en is ongenadig. Een regeleinde aan de staart - het soort dat writeLines() toevoegt maar cat() nooit doet - doet de Rust-engine in paniek raken, en dat komt bovendrijven als vangbare fout van de vorm User function panicked: decode_file_. Schrijf de gecodeerde tekst met cat() of writeBin(charToRaw(enc), path) en de rand blijft dof.
writeBin(charToRaw("file payload bytes"), "payload.bin")
enc <- b64::encode_file("payload.bin")
cat(enc, file = "payload.b64")
bytes <- b64::decode_file("payload.b64")
rawToChar(bytes)
#> [1] "file payload bytes"
Het base64-pakket is de derde ingang: puur bestandsgeriënteerd, met een passend paar functies:
base64::encode("payload.bin", "payload.b64")
base64::decode("payload.b64", "payload.out")
readLines("payload.out")
#> [1] "file payload bytes"
Nog één bestandsvorm die je in de praktijk tegenkomt: een data frame-kolom vol gecodeerde blobs, heel gangbaar in API-dumps. Voor een paar honderd regels is een eenvoudige vectorgebaseerde aanroep voldoende:
df <- data.frame(content = c(b64::encode("alpha"), b64::encode("beta"),
b64::encode("gamma")))
decoded <- b64::decode(df$content)
df$decoded <- vapply(decoded, function(x) rawToChar(x), character(1))
df$decoded
#> [1] "alpha" "beta" "gamma"
API's en webresponses
JSON-API's houden ervan binair in tekst te proppen, en Base64 is hun favoriete koffer. Het patroon in R is elke keer hetzelfde: ophalen, ontleden, decoderen, en bepalen wat de bytes zijn. De moderne HTTP-client is httr2, en de JSON-zijde is jsonlite:
library(httr2)
library(jsonlite)
res <- req_perform(request("https://httpbin.org/get?attachment=TWFuIGlzIGhlcmU%3D&name=man.txt"))
data <- fromJSON(rawToChar(res$body))
bytes <- base64decode(data$args$attachment)
writeBin(bytes, data$args$name)
length(bytes)
#> [1] 11
Drie opmerkingen voor onderweg. De request-constructor is request(), en die draagt die naam sinds de eerste release van httr2. Het responslichaam arriveert als raw vector, dus rawToChar() voordat je het als JSON ontledt. En de API kan een dialect spreken: controleer of zijn Base64 URL-veilig is, of van padding is ontdaan, voordat je hem aan een decoder voert die de leerboeksversie verwacht. Sommige APIs encoderen zelfs dubbel, Base64 van Base64, en dat herken je als één decodeerpoging je méér Base64 oplevert in plaats van de verwachte bytes.
Data-URI's en zelfstandige documenten
Het data:-URI-schema (RFC 2397) laat een document zijn eigen inhoud meedragen: een MIME-type, het woord "base64" als de payload gecodeerd is, en de payload zelf. Je komt deze constant tegen in HTML die je scrapet, en er één decoderen is een string-operatie in twee stappen, gevolgd door een gangbare decodeerpoging:
img_tag <- "<img src=\"data:image/png;base64,iVBORw0KGgoAAA\" />"
uri <- regmatches(img_tag, regexec("src=\"([^\"]*)\"", img_tag))[[1]][2]
uri
#> [1] "data:image/png;base64,iVBORw0KGgoAAA"
payload <- sub("^data:[^,]*,", "", uri)
bytes <- base64decode(payload)
length(bytes)
#> [1] 10
Dezelfde vorm duikt op in CSS, in PDF-annotaties en in zelfstandige R Markdown-rapporten, waar een plot is platgeperst in een <img>-tag, zodat het HTML reist zonder bijbestanden. Als je zulke documenten rendert en de ingebedde afbeeldingen wilt extraheren, is dit de hele truc: vind de data:-string, knip bij de eerste komma, decodeer.
Databases, e-mail en configuratie
Base64 duikt op in databases telkens iemand binair in een tekstkolom wilde, en de decode-zijde is een kolom strings terug naar bytes. Hier is de ronde rit tegen SQLite via DBI en RSQLite: bewaar de gecodeerde waarde, haal die terug met een query, decodeer, en je hebt je bytes:
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"
SQLite kan binair ook natief bewaren als BLOB, in welk geval helemaal geen Base64 nodig is en de kolom naar R terugkomt als raw vector, zoals in class(dbGetQuery(db, "SELECT payload FROM blobs")$payload[[1]]) == "raw". De Base64-in-TEXT-variant bestaat voor draagbaarheid: je kunt het inspecteren met een teksteditor, en elke andere taal kan het lezen zonder binaire drivers.
E-mail is waar omwikkelde Base64 vandaan komt. MIME-delen breken af op 76 tekens met CRLF tussen de regels, en elk mailsysteem dat je ooit hebt gelezen heeft bijlagen exact zo vervoerd. R heeft geen eerste-klasses mailclient, maar als je inderdaad een .eml-bestand ontvangt, is het base64-deel van een bijlage gewoon omwikkelde tekst: de tolerante decoders ontwarren het voor je terwijl ze decoderen. Het mime-pakket helpt je te identificeren wat je in handen hebt:
mime::guess_type("photo.jpg")
#> [1] "image/jpeg"
part <- "SGVsbG8s\r\nCnRoaXMgaXMgYW4g\r\nZW1haWwgYXR0YWNobWVudA=="
rawToChar(base64decode(part))
#> [1] "Hello,\nthis is an email attachment"
Configuratiebestanden voltooien de rondleiding. Als een certificaat of blob Base64-gecodeerd is opgeslagen in een YAML- of JSON-configuratie, of in een omgevingsvariabele, leest R het als een gewone string en decodeer je op verzoek:
config_line <- "api_cert: TFRTU0VDUkVU"
cert_b64 <- sub("^api_cert: ", "", config_line)
raws <- base64decode(cert_b64)
rawToChar(raws)
#> [1] "LTSSECRET"
Grote payloads
R-strings hebben een hard plafond van 2^31 - 1 bytes, en een Base64-string die groot genoeg is, kan er tegenaan lopen, omdat de gecodeerde vorm ongeveer een derde groter is dan het origineel. Het base64enc-pakket lost dit op sinds zijn 2022-release: geef base64encode() een regelbreedte en hij geeft een vector van regels terug in plaats van één enorme string, en aan de decode-zijde leest het file =-argument het bestand op als regels en decodeert, zonder je te dwingen één gigantische string in een variabele te houden.
Voor bestanden in de honderden megabytes is het praktische patroon om in uitgelijnde chunks te lezen. Base64-groepen zijn onafhankelijk, dus elke chunk waarvan de lengte een veelvoud van vier tekens is, decodeert op zich; je hebt alleen een kleine buffer nodig voor het scheve einde:
chunk <- 65536L
con <- file("huge.b64", "rb")
on.exit(close(con))
leftover <- ""
out <- raw(0)
while (TRUE) {
piece <- readChar(con, chunk, useBytes = TRUE)
if (length(piece) == 0) break
piece <- gsub("[\r\n]", "", piece)
ready <- paste0(leftover, piece)
take <- floor(nchar(ready) / 4) * 4
if (take > 0) {
out <- c(out, base64decode(substr(ready, 1, take)))
leftover <- substr(ready, take + 1, nchar(ready))
} else {
leftover <- ready
}
}
if (nchar(leftover) > 0) out <- c(out, base64decode(leftover))
length(out)
Het b64-pakket is de snelheidskampioen in dit terrein: zijn Rust-engine decodeert een bestand van 50 megabyte in een fractie van een seconde, en zijn vectorgebaseerde decode() verwerkt een kolom met veel gecodeerde waarden in één aanroep, en dat is een groot verschil met per regel lopen. Als je veel strings hebt, 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-tools spreken Base64 natief, en R kan hen werk aanreiken of werk van hen overnemen. Op Linux decodeert base64 -d (macOS en andere BSD-systemen gebruiken -D):
echo "SGVsbG8sIHdvcmxkIQ==" | base64 -d
#> Hello, world!
En een Rscript van één regel doet hetzelfde werk met dezelfde pakketten die je in je scripts gebruikt:
Rscript -e 'library(base64enc); writeLines(rawToChar(base64decode("TWFu")))'
#> Man
Gebruik de shell voor snelle checks en pipes; gebruik R wanneer het resultaat moet leven in een data frame, een bestand of een rapport. Eén waarschuwing: opdrachtregel-argumenten hebben een grootelimiet (ARG_MAX), dus plak geen strings van meerdere megabytes in de terminal. Stuur ze in plaats daarvan via een bestand.
Valkuilen die het weten waard zijn
Hier is de korte lijst met de manieren waarop dit R-ontwikkelaars beetneemt, en allemaal inheems aan het ecosysteem, niet aan Base64 in het algemeen:
- openssl faalt stil.
base64_decode()levert een lege raw vector, zonder fout of waarschuwing, als het een teken buiten het alfabet of rommel aan de staart tegenkomt. Controleer altijdlength()van het resultaat voordat je het gelooft. - NA is voor een decoder geen ontbrekende waarde.
NA_character_wordt omgezet naar de string"NA", die decodeert naar de byte0x34. FilterNAvoordat je decodeert. - Vectoren gedragen zich niet zoals je verwacht.
base64decode()voegt zijn invoer samen tot één raw vector;openssl::base64_decode()sterft metthe condition has length > 1op invoer met meerdere elementen; alleenb64::decode()is écht vectorgebaseerd. - rawToChar is een stille corrupter. Ongeldige UTF-8-bytes geven in een UTF-8-locale geen fout; ze worden een string die met
"unknown"is gemarkeerd. Gebruik eerstcheckUTF8()als sluis. - Het punt in een JWT is een regex-wildcard.
strsplit(jwt, ".")zonderfixed = TRUEbreekt op elk teken. Dit is de meest voorkomende Base64-bug in R-code. - b64-bestanden zijn ongenadig. Een regeleinde aan de staart in het gecodeerde bestand doet
b64::decode_file()in paniek raken. Schrijf metcat(), niet metwriteLines(). - Tolerant is niet tolerant genoeg om op te vertrouwen. De standaardmodus van
base64decode()slaat slechte tekens over waar dan ook, dus een beschadigde string kan decoderen naar overtuigend uitziende onzin, zonder fout. Gebruikstrict = TRUEop vertrouwensgrenzen. - b64-foutmeldingen spreken Rust. Verwacht zinnen als
Both cases of Either erroredals je het een type geeft dat het niet wil. De melding is met opzet onhelpend; het is een Rust-typesysteem dat de schouders ophalt.
Beste praktijken
- Standaard gebruik je
base64enc::base64decode()voor dagelijks werk, en zetstrict = TRUEaan waar de invoer een vertrouwensgrens overschrijdt: onbetrouwbare API's, uploads van gebruikers, alles dat is ondertekend. - Grijp naar
b64als je snelheid, échte vectorisering, of de URL-veilige en paddingloze engines nodig hebt, en accepteer dat het van ontwerp af strikt is. - Als
opensslal in je project zit, gebruik het, maar behandel elk resultaat als verdacht totdat je de lengte hebt gecontroleerd. - Beslis de tekenset bewust: neem UTF-8 aan, verifieer met
checkUTF8(), en gebruikiconv()alleen als de afzender je anders vertelt. - Voor JWT's kijk met
strsplit()plusfixed = TRUE, maar vertrouw pas nadatjosede handtekening heeft geverifieerd. - Houd
TWFuin je tests: het is drie bytes aan rooktest voor elk decode-pad dat je schrijft. - Onthoud wat Base64 niet is: het is geen versleuteling, en het is geen compressie. Het is pakband, en iedereen met dit artikel kan alles wat het doet omkeren. Decodeer vrij, vertrouw selectief.
Een korte geschiedenis van Base64 in R
Het formaat is oud. 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), opgeschoond in RFC 3548 in 2003, en in RFC 4648 in 2006 zijn moderne vorm gegeven, inclusief het URL-veilige alfabet. Het R-verhaal is veel korter en beweegt veel sneller. Het base64enc-pakket, van Simon Urbanek, landde in september 2012 op CRAN en is sindsdien het werkpaard, met checkUTF8() toegevoegd in 2015 en ondersteuning voor lange vectoren in 2022. In oktober 2024 werd het oude base64-pakket expliciet opnieuw uitgegeven als compatibiliteitswrapper, en verwijst de eigen beschrijving nu nieuwe toepassingen naar base64enc, openssl of jsonlite. Dan kwam b64 begin 2024 (huidige release 0.1.7, juli 2025), een Rust-engine gebouwd met extendr die échte vectorisering en een stal alfabetten bracht. In februari 2026 bracht base64enc zijn lang-verwachte strikte modus uit, en in april 2026 ontwierp het jose-pakket zichzelf opnieuw rond jwt_*-functies. Tien jaar voor wat andere talen in één bibliotheek leverden, maar het resultaat is een gereedschapskist waar elke decoder een duidelijke taak en een duidelijke persoonlijkheid heeft.
Leuke weetjes
Omdat een complete gids moet eindigen met een glimlach:
base64decode(NA_character_)geeft de letter"4"terug, wantNA_character_reist naar de decoder als de string"NA", en"NA"is een geldige Base64-groep. De meest R-specifieke een-byte-verrassing in de taal.- Voed
openssl::base64_decode()een string met één illegaal teken en het geeftraw(0)terug met totale stilte. De gevaarlijkste stilte in het ecosysteem. - Elke strikte aanroep van
base64decode()praat een statusregel op C-niveau, zoalsv=30000, pad=0, org='u'. Het is geen waarschuwing. Het is de C-code die zijn keel ruimt. b64kan alfabetten decoderen die je nooit hebt ontmoet: BinHex, IMAP modified UTF-7, en de aangepaste alfabetten van bcrypt en crypt. R kan een Macintosh-bijlage uit de jaren tachtig en een moderne wachtwoordhash lezen met dezelfde engine.- R-strings stoppen bij 2^31 - 1 bytes, en daarom leerde
base64enceen vector van regels terug te geven voor lange invoer. Het formaat raakte de muur van de taal, en het pakket groeide een ladder. - Het woord "base64" encodeert naar
YmFzZTY0. Een formaat dat zichzelf kan beschrijven, is het technische equivalent van een spiegel die in Morse spreekt. - Één NUL-byte kost nog steeds vier tekens:
"AA==". In Base64 is niets altijd iets. - Je R-build heeft een bijnaam. R 4.5.0 heet "How About a Twenty-Six", want R-versies zijn vernoemd naar Peanuts-strips en films, en de strips waaruit ze lenen zijn decennia ouder dan de releases die ze naamgeven. Zelfs de versie waarmee je decodeert heeft een gevoel voor humor.
Samenvatting
Kies je decoder naar het gezelschap dat je kiest: base64enc voor dagelijks werk, met strict = TRUE aan de randen; openssl als het al in je project zit, mits je je resultaten controleert; b64 als je snelheid, vectoren en de URL-veilige engines wilt; en de kleine specialisten base64 en base64url voor bestandsklusjes en URL-veilige strings. Bewaar je bytes met checkUTF8() voordat je ze tekst noemt, gebruik iconv() als bekend is dat de tekenset iets anders is, en onthoud dat de raw vector in het midden een eigenschap is: hij dwingt je om te beslissen wat de bytes betekenen, in plaats van een bibliotheek te laten raden. Decodeer alles, vertrouw alleen wat verifieert. En als je de andere kant op moet, je eigen bytes in een string inpakken voor de rit in plaats van er een uit te pakken, dan dekt het zusterartikel Base64-encoderen in R in detail.
Laatst bijgewerkt: 2026-10-06
Gerelateerd artikel: Base64-codering in R: een complete gids