Decodifica Base64 in R: una guida completa
Una stringa atterra nella tua sessione R. Sembra una zuppa di caratteri mescolati: lettere, cifre, ogni tanto un più o una barra, e una coppia di segni di uguale appesi alla fine. Chi te l'ha mandata giura che un tempo era una frase perfettamente ordinaria, una JPEG o un documento JSON. Quella stringa è Base64, e questa pagina è la ricetta per riportarla a ciò che era.
Un rapido ripasso, perché la home page di questo sito spiega il formato in tutta la sua profondità: Base64 scrive tre byte in ingresso come quattro caratteri tratti da un alfabeto di 64 simboli, e uno o due caratteri = finali segnano dove finivano i dati veri. È questo scambio di quattro contro tre a far sì che il testo codificato sia circa un terzo più grande dell'originale, e la decodifica esegue semplicemente lo scambio al contrario. Tieni a mente la forma del problema e apriamo qualche busta.
Ecco la svolta che rende R un po' diversa da molte altre lingue: il R di base non ha il Base64 in assoluto. Non c'è un base64_decode() nascosto in un pacchetto base, e non c'è una funzione integrata di una riga a cui ricorrere. Devi portarti un pacchetto. La buona notizia è che l'ecosistema ne offre diversi, ciascuno con la sua personalità, e a fine articolo saprai esattamente quale prendere e quale diffidare.
I decoder in un colpo d'occhio
Cinque pacchetti fanno il lavoro pesante, e si dividono in due grandi schieramenti: quelli indulgenti, che non fanno una piega davanti a input sporchi, e quelli severi, che trattano la RFC come un contratto. Ecco il cast, aggiornato al 2026:
| Pacchetto | Versione (2026) | Punti d'ingresso per la decodifica | Personalità |
|---|---|---|---|
base64enc |
0.1-6 | base64decode() |
Indulgente di default, ha aggiunto una modalità strict nel febbraio 2026 |
openssl |
2.4.2 | base64_decode() |
Salta gli spazi bianchi, ma fallisce in silenzio in modi che meritano uno sguardo severo |
b64 |
0.1.7 | decode(), decode_as_string() |
Severo, vettorizzato, scritto in Rust, veloce |
base64 |
2.0.2 | decode() |
Wrapper comodo da file a file attorno a openssl |
base64url |
1.4 | base64_urldecode() |
Alfabeto sicuro per URL, senza padding, indulgente in silenzio |
Tre outsider meritano una menzione. Il pacchetto jsonlite esporta i suoi helper, base64_enc, base64_dec e la coppia sicura per URL, quindi se già fai il parsing di JSON, potresti già avere un decoder a portata di mano. Il pacchetto jose include base64url_decode() per il lavoro con i JWT. E l'antico pacchetto RCurl porta ancora una funzione base64() che avvolge libcurl: funziona, è orientata ai caratteri, e ha l'aria di un nonno fedele più che di uno strumento per codice nuovo.
Installare il cast
Se R non è ancora sulla macchina, il tuo sistema operativo lo distribuisce: r-base su Debian e Ubuntu, R su Fedora, un pacchetto o un installer su macOS e Windows. Poi i pacchetti, direttamente da CRAN:
install.packages("base64enc")
install.packages("openssl")
install.packages("b64")
install.packages("base64url")
Due note sulla compilazione, perché sono i punti in cui le installazioni vanno storto. Il pacchetto openssl viene compilato contro il tuo OpenSSL di sistema, quindi una macchina Linux nuda potrebbe volere prima gli header di sviluppo:
sudo apt install libssl-dev
Il pacchetto b64 è un motore Rust avvolto con extendr, quindi compilarlo da fonte vuole la toolchain Rust (sudo apt install cargo porta con sé anche rustc). Su Windows e macOS ottieni binari precompilati da CRAN e nessuna di queste cose si applica. Se preferisci un gestore di pacchetti, pak::pkg("base64enc") o remotes::install_cran("b64") fanno lo stesso lavoro con meno opinioni sui repository.
La tua prima decodifica: il rito delle tre righe
Novanta percento della vita di decodificazione sta in tre righe. Ecco il test di fumo canonico, usando la famosa stringa TWFu:
library(base64enc)
packed <- "TWFu"
bytes <- base64decode(packed)
bytes
#> [1] 4d 61 6e
rawToChar(bytes)
#> [1] "Man"
In quella piccola cerimonia, tre cose meritano attenzione. Prima, base64decode() ti consegna sempre un vettore raw, mai una stringa. È una caratteristica, non un incidente: il Base64 può trasportare una frase, una JPEG o un certificato, e nessuno di questi dovrebbe essere trattato in modo diverso prima di sapere cosa hai in mano. Secondo, il salto dai byte di nuovo al testo è un passo separato e deliberato attraverso rawToChar(), ed è proprio in quel passo che vive la decisione sul charset (ne riparliamo più avanti). Terzo, TWFu si decodifica nella parola "Man": tre lettere, zero drammi. Tieni quella stringa in tasca. Se un pezzo di codice di decodifica trasforma TWFu in Man, la macchina è onesta.
Perché prima o poi decoderai sempre qualcosa che hai codificato tu, ecco l'andata e ritorno completa che dimostra che le due direzioni concordano:
text <- "Hello, world!"
packed <- base64encode(charToRaw(text))
packed
#> [1] "SGVsbG8sIHdvcmxkIQ=="
identical(text, rawToChar(base64decode(packed)))
#> [1] TRUE
Il confine del vettore raw
R attraversa il confine byte/testo con due funzioni piccoline, e una volta che le conosci il resto di questo articolo ti sembra ovvio. charToRaw() trasforma una stringa nei suoi byte, rawToChar() fa il contrario, e in mezzo hai l'intero kit raw: length() per contare i byte, head() per dare un'occhiata, writeBin() e readBin() per spostarli dentro e fuori dai file. Ogni decoder di questo articolo si ferma a quel confine apposta.
bytes <- base64decode("SGVsbG8s")
length(bytes)
#> [1] 6
head(bytes)
#> [1] 48 65 6c 6c 6f 2c
rawToChar(bytes)
#> [1] "Hello,"
Quindi quando vedi un risultato come [1] 48, leggilo come "un byte, valore 72, la lettera H", e fermati lì finché non sai quale charset stanno indossando i byte. Quella pausa è tutta la disciplina della decodifica.
Input sporchi: come i decoder non vanno d'accordo
Questa è la sezione che ti salva alle 2 di notte, perché i decoder non vanno d'accordo su cosa succede quando l'input è un po' sporco. Il Base64 del mondo reale arriva con spazi dentro, con padding caduto per un copy-paste nervoso, con un simbolo vagante di un clipboard che l'ha inghiottito, o con spazzatura in coda. Ecco come risponde ogni decoder, sulla stessa famiglia di imputati:
| Input | base64enc (default) | base64enc (strict = TRUE) | openssl | b64 |
|---|---|---|---|---|
"SGVs bG8s IHdvcmxkIQ==" (uno spazio dentro) |
"Hello, world!" (spazio saltato) | errore: carattere non valido, posizione indicata | "Hello, world!" (spazio saltato) | errore |
"SGVsbG8sIHdvcmxkIQ" (padding rimosso) |
"Hello, world!" (padding mancante tollerato) | errore: padding mancante | errore: decodifica fallita | errore: padding non valido |
"SGVsbG8s!IHdvcmxkIQ==" (un "!" dentro) |
"Hello, world!" (carattere errato saltato) | errore: carattere non valido | vettore raw vuoto, nessun errore | errore |
"SGVsbG8sIHdvcmxkIQ==xx" (spazzatura in coda) |
"Hello, world!" (spazzatura ignorata) | errore: contenuto in coda | vettore raw vuoto, nessun errore | errore |
"TQ==" (pulito) |
M | M | M | M |
Leggi quella tabella due volte, perché contiene la storia intera. Il base64decode() di default è l'agente doganale cordiale: salta i caratteri fuori dall'alfabeto, tollera il padding mancante e ignora il contenuto in coda, quindi stringhe con la forma di email o di clipboard passano senza problemi. La modalità strict = TRUE, arrivata con la release 0.1-5 nel febbraio 2026 dopo un lungo silenzio, è l'esaminatore forense: valida la stringa intera, indica la posizione esatta dell'imputato e rifiuta tutto ciò che non è da manuale. b64 è severo di default e non riesce mai a metà. E openssl si siede nel mezzo scomodo: salta gli spazi bianchi volentieri ed è corretto nel dare errore sul padding mancante, ma quando incontra un carattere davvero illegale o spazzatura in coda restituisce un vettore raw vuoto e non dice assolutamente nulla. Quel risultato vuoto e silenzioso è il comportamento più pericoloso di tutto questo articolo, quindi guardiamolo succedere:
dirty <- "SGVsbG8s!IHdvcmxkIQ=="
openssl::base64_decode(dirty)
#> raw(0) # vuoto. nessun errore. nessun avviso. nessun indizio.
Se scrivi codice contro openssl, controlla la lunghezza di ciò che hai ottenuto prima di fidartene. È un'abitudine piccola che evita un'intera classe di misteri del tipo "dove è finito il mio dato".
Per lo schieramento severo, ecco base64enc in tutta la sua precisione, con i messaggi di errore esatti che vedrai nel tuo console:
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)
Una stranezza da aspettarsi: ogni chiamata strict, successo o fallimento, borbotta una riga di stato a livello C nel console, qualcosa come v=30000, pad=0, org='u'. È una scrittura diretta dal codice compilato, non un warning di R, quindi suppressWarnings() non la tocca. È inoffensiva, ma se i tuoi test confrontano l'output del console, ora sai perché c'è una riga in più.
Le peculiarità di R: NA, vettori e concatenazione silenziosa
Il Base64 ha le sue peculiarità; R aggiunge le sue. La prima morde attraverso NA. Quando passi NA_character_ a un decoder, R lo converte in silenzio nella stringa "NA" prima che il decoder lo veda, e "NA" è un gruppo Base64 perfettamente valido. Il risultato non è un errore e non è un vettore vuoto. È il byte 0x34, la lettera "4":
base64decode(NA_character_)
#> [1] 34
rawToChar(base64decode(NA_character_))
#> [1] "4"
Quindi una colonna di valori mancanti non si decodifica in valori mancanti. Si decodifica in una colonna di lettere 4, e il tuo codice a valle la elabora allegramente. Se il tuo input può contenere NA, filtra prima:
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"
La seconda peculiarità è l'input vettorizzato, e i decoder prendono svolte completamente diverse. base64decode() tratta un vettore di caratteri come diversi pezzi di una stringa e li concatena, quindi due elementi tornano come un solo vettore raw. openssl::base64_decode() non arriva nemmeno così lontano: riduce il suo argomento e poi incappa in un test logico a due elementi, che produce uno dei messaggi di errore più onesti di R. E b64::decode() è l'unico davvero vettorizzato, che restituisce un blob decodificato per ogni input:
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"
La morale: fai il loop o vettorializza in modo deliberato, e non dare mai per scontato che un vettore di input ti dia un vettore di output.
L'alfabeto sicuro per URL
Il Base64 standard spende le ultime due caselle del suo alfabeto su + e /, e sono esattamente i caratteri che gli URL non amano. Il più diventa %2B, la barra diventa %2F e il padding diventa %3D, quindi un token che dovrebbe essere incollabile ovunque inizia a portare segni di percentuale. La variante sicura per URL, definita nella sezione 5 della RFC 4648, scambia quei due caratteri con - e _ e di solito elimina anche il padding. R ha tre porte verso quel mondo.
La porta uno sono i motori di b64. Un motore è semplicemente un alfabeto configurato e una politica di padding, e il pacchetto include i quattro che ti servono:
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"
I motori sono "standard" (il default), "standard_no_pad", "url_safe" e "url_safe_no_pad", e lo stesso oggetto motore funziona in entrambe le direzioni, il che mantiene il tuo codice simmetrico. Sono anche severi sul loro alfabeto: offri "----" al motore standard e risponde "Invalid byte 45, offset 0.", il che è un sollievo.
La porta due è il pacchetto dedicato base64url, piccolo e a scopo unico: l'alfabeto sicuro per URL, senza padding, sempre. Un'avvertenza: il suo decoder è indulgente, quindi decodificherà volentieri il prefisso valido di una stringa corrotta e si fermerà lì senza lamentarsi. Se un valore sicuro per URL viene da una fonte non fidata, preferisci b64.
base64url::base64_urlencode("hello world")
#> [1] "aGVsbG8gd29ybGQ"
base64url::base64_urldecode("aGVsbG8g!!!")
#> [1] "hello " # il resto è stato scartato in silenzio
La porta tre è jose, che esporta base64url_encode() e base64url_decode() per il lavoro con i JWT, e restituisce vettori raw come ci si spera.
E quando ti serve solo l'occasionale colpo unico e vuoi restare dentro base64enc, chirurgia su stringa più una correzione del padding bastano:
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: sbirciare e fidarsi
Il motivo per cui la maggior parte delle persone incontra il Base64 sicuro per URL è il JSON Web Token. Un JWT è tre parti base64url unite da punti: un header che descrive come è stato firmato, un payload di claim e una firma che rende affidabile il tutto. Decodificare le prime due parti in R è una faccenda di cinque righe. Nota il fixed = TRUE nel strsplit(): il punto è un jolly regex, e senza quel flag R splitta allegramente il tuo token in caratteri singoli, che è il bug Base64 più comune nel codice R in circolazione.
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"
Due avvertenze oneste. Prima, decodificare un JWT è sbirciare, non fidarsi: la terza parte è la firma, e ha un senso solo se verificata contro la chiave dell'emittente. Per quello, il pacchetto jose gestisce tutta la famiglia. La sua release 2.0 (aprile 2026) aggiunge il supporto ED25519 e rende il header typ opzionale; i tutorial più vecchi che mostrano gli helper jwk_read()/jwk_write() della 1.x (o i loro alias read_jwk()/write_jwk()) restano validi, perché la 2.0 include ancora quegli stessi helper di lettura/scrittura JWK:
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() verifica la firma e applica anche i claim exp e nbf, sollevando un errore se il token è scaduto. E secondo: base64url::base64_urldecode() e soci decodificano header e payload senza problemi, ma la parte della firma è binaria, quindi decodificala con una funzione che restituisce raw (come b64::decode() con il motore "url_safe_no_pad") invece che in una stringa.
Charset: che testo sono quei byte?
Ogni decoder di questo articolo si ferma al confine del vettore raw apposta, perché la risposta a "che testo era?" dipende dal charset in cui i byte erano impacchettati. La buona notizia subito: se il mittente ha usato UTF-8, che è la maggior parte del web moderno, la tua vita è corta e felice. base64enc include perfino un cancello per questo:
packed <- base64encode(charToRaw("caf\u00e9"))
bytes <- base64decode(packed)
checkUTF8(bytes, quiet = TRUE)
#> [1] TRUE
rawToChar(bytes)
#> [1] "café"
checkUTF8() chiede "questi byte si possono leggere come UTF-8?" senza impegnarsi. Con quiet = TRUE restituisce TRUE o FALSE; di default è ancora più diretto e solleva un errore sui byte non validi, indicando l'imputato, come in INVALID byte 0xff at 0x0 (0, line 1): invalid start byte (FE/FF). Segnala anche FALSE per le stringhe che contengono byte NUL, che non possono mai far parte di una stringa R UTF-8 valida. Usalo come cancello, e il resto segue.
Ecco perché il cancello conta. In una locale UTF-8, rawToChar() non lancia un errore quando i byte non sono UTF-8 validi. Ti consegna una stringa che R marca con Encoding() == "unknown", e a seconda della locale e della build di R la stessa chiamata può invece morire con un errore di "invalid multibyte sequence", che è almeno onesto. In ogni caso, un rawToChar() senza guardia su byte sconosciuti è così che nasce il mojibake:
broken <- as.raw(c(0xc3, 0x28)) # una coppia UTF-8 troncata
checkUTF8(broken, quiet = TRUE)
#> [1] FALSE
rawToChar(broken) # nessun errore. ed è questo il problema.
#> [1] "\xc3("
E quando il mittente ha usato qualcosa di completamente diverso, Latin-1 o Windows-1252 o Shift-JIS, la ricetta è decodificare in raw e poi tradurre con iconv(), che capisce i charset con nome:
latin1_bytes <- as.raw(c(0x63, 0x61, 0x66, 0xe9)) # "café" in Latin-1
iconv(rawToChar(latin1_bytes), from = "latin1", to = "UTF-8")
#> [1] "café"
Gli emoji meritano una parola, perché R ha una storia qui. Il R moderno conserva i punti di codice sopra U+FFFF, dove vivono gli emoji, come vero UTF-8, quindi un'andata e ritorno funziona e il conteggio dei byte coincide con ciò che ogni altra lingua si aspetta. Le release più vecchie di R conservavano gli stessi caratteri come coppie di surrogate, uno schema spesso chiamato CESU-8, quindi un singolo emoji attraversava il confine come sei byte di UTF-8 non valido. Se erediti vecchio codice R i cui emoji arrivano spezzati sulla rete, quello è il sospettato numero uno: controlla nchar(x, type = "bytes") e confronta con ciò che la lingua del mittente produrrebbe.
emoji <- "\U0001F600"
nchar(emoji, type = "bytes")
#> [1] 4
round_trip <- base64decode(base64encode(charToRaw(emoji)))
identical(emoji, rawToChar(round_trip))
#> [1] TRUE
La regola generale: dai per scontato UTF-8, verifica con checkUTF8(), e rivolgiti a iconv() solo quando hai una conoscenza certa di un altro charset. Non indovinare mai.
File: dal testo avvolto ai byte veri
Le stringhe sono facili; i file sono il punto in cui il Base64 si guadagna da vivere. Parti dalla pipeline manuale che funziona con qualsiasi pacchetto: leggi il testo codificato, decodifica, scrivi i byte.
cat("SGVsbG8sIHdvcmxkIQ==", file = "hello.b64")
bytes <- base64decode(paste(readLines("hello.b64"), collapse = ""))
writeBin(bytes, "hello.txt")
readLines("hello.txt")
#> [1] "Hello, world!"
Poi lo strato di comodità. base64enc può leggere e scrivere per te: l'argomento file punta a un file che contiene testo Base64, e output punta a dove devono finire i byte decodificati. Il valore di ritorno è il numero di byte scritti, un ottimo controllo di correttezza in omaggio:
n <- base64decode(file = "hello.b64", output = "hello.txt")
n
#> [1] 13
readLines("hello.txt")
#> [1] "Hello, world!"
L'input avvolto, il tipo che ha superato email o armatura PEM, non è un problema per i decoder indulgenti: guardano dritti oltre i cambi di riga, ovunque cadano, purché ciò che resta sia Base64 valido. Il MIME va a capo a 76 caratteri con CRLF tra le righe; i blocchi PEM vanno a capo a 64. Il motore severo, naturalmente, rifiuta i cambi di riga senza appello.
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.
Il pacchetto b64 ha la coppia file con i nomi più chiari, e perché è veloce come il Rust è quella a cui ricorrere quando il file è grosso. C'è però un bordo tagliente: decode_file() legge il file byte per byte ed è senza perdono. Un cambio di riga finale - il tipo che writeLines() aggiunge ma cat() non aggiunge mai - fa andare in panico il motore Rust, che emerge come un errore intercettabile della forma User function panicked: decode_file_. Scrivi il testo codificato con cat() o writeBin(charToRaw(enc), path) e il bordo resta smussato.
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"
Il pacchetto base64 è la terza via: orientata puramente ai file, con una coppia di funzioni gemelle:
base64::encode("payload.bin", "payload.b64")
base64::decode("payload.b64", "payload.out")
readLines("payload.out")
#> [1] "file payload bytes"
Un'altra forma di file che incontrerai nella pratica: una colonna di data frame piena di blob codificati, molto comune negli dump delle API. Per poche centinaia di righe, una semplice chiamata vettorizzata basta:
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 e risposte web
Le API JSON amano stipare il binario nel testo, e il Base64 è la loro valigia preferita. Il pattern in R è sempre lo stesso: recupera, fai il parsing, decodifica e decidi cosa sono i byte. Il client HTTP moderno è httr2, e il lato JSON è 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
Tre note per la strada. Il costruttore della richiesta è request(), e ha quel nome dalla prima release di httr2. Il corpo della risposta arriva come vettore raw, quindi rawToChar() prima di fare il parsing come JSON. E l'API può parlare un dialetto: controlla se il suo Base64 è sicuro per URL, o privato del padding, prima di passarlo a un decoder che si aspetta la versione da manuale. Alcune API fanno perfino la doppia codifica, Base64 di Base64, che riconosci quando una decodifica ti dà più Base64 al posto dei byte attesi.
Data URI e documenti autosufficienti
Lo schema URI data: (RFC 2397) permette a un documento di portare con sé il proprio contenuto: un tipo MIME, la parola "base64" quando il payload è codificato, e il payload stesso. Ne incontrerai continuamente nell'HTML che estrai, e decodificarne uno è un'operazione su stringa in due passi seguita da una decodifica ordinaria:
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
La stessa forma appare nel CSS, nelle annotazioni PDF e nei report R Markdown autosufficienti, dove un grafico è stato appiattito in un tag <img> così che l'HTML viaggi senza file laterali. Se stai renderizzando documenti del genere e vuoi estrarre le immagini incorporate, questo è tutto il trucco: trova la stringa data:, taglia alla prima virgola, decodifica.
Database, email e configurazione
Il Base64 appare nei database ogni volta che qualcuno ha voluto binario dentro una colonna di testo, e il lato decodifica è una colonna di stringhe riportata a byte. Ecco l'andata e ritorno contro SQLite attraverso DBI e RSQLite: memorizza il valore codificato, richiedilo indietro, decodificalo, e hai i tuoi byte:
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 può anche conservare il binario nativamente come BLOB, in cui caso il Base64 non serve affatto e la colonna torna in R come vettore raw, come in class(dbGetQuery(db, "SELECT payload FROM blobs")$payload[[1]]) == "raw". La variante Base64-in-TEXT esiste per portabilità: puoi ispezionarla con un editor di testo, e ogni altra lingua può leggerla senza driver binari.
L'email è il posto da cui viene il Base64 avvolto. Le parti MIME vanno a capo a 76 caratteri con CRLF tra le righe, e qualsiasi sistema di posta che hai mai letto ha trasportato allegati esattamente così. R non ha un client di posta di prima classe, ma quando ricevi un file .eml, la parte base64 di un allegato è solo testo avvolto: i decoder indulgenti lo svolgono per te mentre decodificano. Il pacchetto mime ti aiuta a identificare cosa hai in mano:
mime::guess_type("photo.jpg")
#> [1] "image/jpeg"
part <- "SGVsbG8s\r\nCnRoaXMgaXMgYW4g\r\nZW1haWwgYXR0YWNobWVudA=="
rawToChar(base64decode(part))
#> [1] "Hello,\nthis is an email attachment"
I file di configurazione completano il tour. Quando un certificato o un blob è conservato codificato in Base64 in una configurazione YAML o JSON, o in una variabile d'ambiente, R lo legge come una stringa ordinaria e tu decodifichi su richiesta:
config_line <- "api_cert: TFRTU0VDUkVU"
cert_b64 <- sub("^api_cert: ", "", config_line)
raws <- base64decode(cert_b64)
rawToChar(raws)
#> [1] "LTSSECRET"
Payload grossi
Le stringhe di R hanno un soffitto duro di 2^31 - 1 byte, e una stringa Base64 abbastanza grande può andarci a sbattere, perché la forma codificata è circa un terzo più grande dell'originale. Il pacchetto base64enc gestisce il problema dalla sua release del 2022: dai a base64encode() una larghezza di riga e restituisce un vettore di righe al posto di una stringa enorme, e sul lato decodifica l'argomento file = legge il file come righe e decodifica senza costringerti a tenere una stringa gigantesca in una variabile.
Per file nelle centinaia di megabyte, il pattern pratico è leggere in chunk allineati. I gruppi Base64 sono indipendenti, quindi qualsiasi chunk la cui lunghezza sia un multiplo di quattro caratteri si decodifica da solo; ti serve solo un piccolo buffer per portare la coda irregolare:
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)
Il pacchetto b64 è il campione di velocità in questo territorio: il suo motore Rust decodifica un file da 50 megabyte in una frazione di secondo, e il suo decode() vettorizzato gestisce una colonna di molti valori codificati in una chiamata, che è una differenza drastica rispetto al loop riga per riga. Se hai molte stringhe, fai tu un confronto rapido con system.time(); il divario tra un loop riga per riga e una chiamata vettorizzata è di solito abbastanza grande da contare.
La riga di comando
Non serve una sessione R completa per tutto. I classici tool Unix parlano Base64 nativamente, e R può dare lavoro loro o prenderlo da loro. Su Linux, base64 -d decodifica (macOS e altri sistemi BSD usano -D):
echo "SGVsbG8sIHdvcmxkIQ==" | base64 -d
#> Hello, world!
E un Rscript di una riga fa lo stesso lavoro con gli stessi pacchetti che usi nei tuoi script:
Rscript -e 'library(base64enc); writeLines(rawToChar(base64decode("TWFu")))'
#> Man
Usa la shell per controlli rapidi e pipe; usa R quando il risultato deve vivere in un data frame, un file o un report. Un'avvertenza: gli argomenti della riga di comando hanno un limite di dimensione (ARG_MAX), quindi non incollare stringhe da molti megabyte nel terminale. Invece, fattelle passare attraverso un file.
Le trappole che conviene conoscere
Ecco l'elenco breve dei modi in cui questo morde gli sviluppatori R, tutti nativi dell'ecosistema e non del Base64 in generale:
- openssl fallisce in silenzio.
base64_decode()restituisce un vettore raw vuoto, senza errore né warning, quando incontra un carattere fuori dall'alfabeto o spazzatura in coda. Controlla semprelength()del risultato prima di crederci. - NA non è un valore mancante per un decoder.
NA_character_converte nella stringa"NA", che si decodifica nel byte0x34. FiltraNAprima di decodificare. - I vettori non si comportano come ti aspetti.
base64decode()concatena il suo input in un solo vettore raw;openssl::base64_decode()muore conthe condition has length > 1su input a più elementi; solob64::decode()è genuinamente vettorizzato. - rawToChar è un corrotto silenzioso. I byte UTF-8 non validi non sollevano un errore in una locale UTF-8; diventano una stringa marcata
"unknown". Metti il cancello concheckUTF8()prima. - Il punto in un JWT è un jolly regex.
strsplit(jwt, ".")senzafixed = TRUEsplitta su ogni carattere. Questo è il bug Base64 più comune nel codice R. - I file di b64 sono senza perdono. Un a capo finale nel file codificato fa andare in panico
b64::decode_file(). Scrivi concat(), non conwriteLines(). - Indulgente non è indulgente abbastanza per fidarsi. La modalità default di
base64decode()salta i caratteri errati ovunque, quindi una stringa corrotta può decodificarsi in spazzatura dall'aspetto plausibile senza errori. Usastrict = TRUEai confini di fiducia. - I messaggi di errore di b64 parlano Rust. Aspettati frasi come
Both cases of Either erroredquando gli passi un tipo che non vuole. Il messaggio è inutile apposta; è un sistema di tipi Rust che si stringe di spalle.
Buone pratiche
- Per il lavoro di tutti i giorni usa
base64enc::base64decode()come default, e attivastrict = TRUEovunque l'input attraversi un confine di fiducia: API non fidate, upload degli utenti, qualsiasi cosa firmata. - Rivolgiti a
b64quando ti servono velocità, vera vettorizzazione o i motori sicuri per URL e senza padding, e accetta che è severo per design. - Se
opensslè già nel tuo progetto, usalo, ma tratta ogni risultato come sospetto finché non ne hai controllato la lunghezza. - Decidi il charset in modo deliberato: dai per scontato UTF-8, verifica con
checkUTF8()e usaiconv()solo quando il mittente ti ha detto il contrario. - Per i JWT, sbircia con
strsplit()piùfixed = TRUE, ma fidati solo dopo chejoseha verificato la firma. - Tieni
TWFunei tuoi test: è un test di fumo di tre byte per qualsiasi percorso di decodifica che scrivi. - Ricorda cosa il Base64 non è: non è cifratura, e non è compressione. È nastro imballaggio, e chiunque con questo articolo può invertire tutto ciò che fa. Decodifica liberamente, fidati selettivamente.
Una breve storia del Base64 in R
Il formato è vecchio. È stato standardizzato per il protocollo Privacy-Enhanced Mail nel 1987 (RFC 989), adottato dal MIME nel 1993 (RFC 1521, poi la definitiva RFC 2045 nel 1996), rimesso in ordine nella RFC 3548 nel 2003, e ha assunto la sua forma moderna, incluso l'alfabeto sicuro per URL, nella RFC 4648 del 2006. La storia di R è molto più corta e più veloce. Il pacchetto base64enc, di Simon Urbanek, è atterrato per la prima volta su CRAN nel settembre 2012 e da allora è il cavallo da lavoro, aggiungendo checkUTF8() nel 2015 e il supporto per i vettori lunghi nel 2022. Nell'ottobre 2024 il vecchio pacchetto base64 è stato ripubblicato esplicitamente come wrapper di compatibilità, con la sua descrizione che ora indirizza le nuove applicazioni a base64enc, openssl o jsonlite. Poi è arrivato b64 all'inizio del 2024 (release corrente 0.1.7, luglio 2025), un motore Rust costruito con extendr che ha portato la vera vettorizzazione e una scuderia di alfabeti. Nel febbraio 2026, base64enc ha rilasciato la sua tanto attesa modalità strict, e nell'aprile 2026 il pacchetto jose si è ridisegnato attorno alle funzioni jwt_*. Dieci anni per ciò che le altre lingue hanno spedito in una libreria, ma il risultato è una cassetta degli attrezzi dove ogni decoder ha un compito chiaro e una personalità chiara.
Fatti curiosi
Perché una guida completa dovrebbe finire con un sorriso:
base64decode(NA_character_)restituisce la lettera"4", perchéNA_character_viaggia verso il decoder come la stringa"NA", e"NA"è un gruppo Base64 valido. La sorpresa a un byte più specifica di R in tutta la lingua.- Alimenta
openssl::base64_decode()con una stringa che ha un carattere illegale e restituisceraw(0)con un silenzio totale. Il silenzio più pericoloso dell'ecosistema. - Ogni chiamata strict a
base64decode()borbotta una riga di stato a livello C comev=30000, pad=0, org='u'. Non è un warning. È il codice C che si schiarisce la voce. b64può decodificare alfabeti che non hai mai incontrato: BinHex, IMAP modified UTF-7, e gli alfabeti custom di bcrypt e crypt. R può leggere un allegato Macintosh degli anni '80 e un hash di password moderno con lo stesso motore.- Le stringhe di R si fermano a 2^31 - 1 byte, ed è per questo che
base64encha imparato a restituire un vettore di righe per input lunghi. Il formato ha colpito il muro della lingua, e il pacchetto ha fatto crescere una scala. - La parola "base64" si codifica in
YmFzZTY0. Un formato che sa descriversi è l'equivalente tecnico di uno specchio che parla in Morse. - Un singolo byte NUL costa ancora quattro caratteri:
"AA==". Nel Base64, il nulla è sempre qualcosa. - La tua build di R ha un soprannome. R 4.5.0 si chiama "How About a Twenty-Six", perché le versioni di R prendono il nome da fumetti e film di Peanuts, e i fumetti a cui si ispirano sono decenni più vecchi delle release che battezzano. Anche la versione con cui stai decodificando ha un senso dell'umorismo.
Per concludere
Scegli il tuo decoder in base alla compagnia che frequenti: base64enc per il lavoro di tutti i giorni, con strict = TRUE ai bordi; openssl se è già nel tuo progetto, a patto di controllare i tuoi risultati; b64 quando vuoi velocità, vettori e i motori sicuri per URL; e i piccoli specialisti base64 e base64url per le faccende dei file e le stringhe sicure per URL. Difendi i tuoi byte con checkUTF8() prima di chiamarli testo, usa iconv() quando il charset è conosciuto come qualcos'altro, e ricorda che il vettore raw in mezzo è una caratteristica: ti costringe a decidere cosa significano i byte invece di lasciare che una libreria indovini. Decodifica tutto, fidati solo di ciò che si verifica. E quando devi andare nella direzione opposta, impacchettare i tuoi byte in una stringa per il viaggio invece di svuotarne una, l'articolo gemello copre la codifica Base64 in R nel dettaglio.
Ultimo aggiornamento: 2026-09-08
Articolo correlato: Codifica Base64 in R: una guida completa