Decodificação Base64 em R: um guia completo
Uma string pousa na sua sessão R. Ela parece uma sopa de letras embaralhada: letras, dígitos, de vez em quando um sinal de mais ou uma barra, e um par de sinais de igual pendurado no final. A pessoa que a enviou jura que ela já foi uma frase perfeitamente comum, um JPEG ou um documento JSON. Essa string é Base64, e esta página é a receita para transformá-la de volta no que ela era.
Uma revisão rápida, porque a página inicial deste site explica o formato em toda a profundidade: o Base64 escreve três bytes de entrada como quatro caracteres tirados de um alfabeto de 64 símbolos, e um ou dois caracteres = no final marcam onde os dados de verdade terminaram. Essa troca de quatro por três é o motivo pelo qual o texto codificado fica cerca de um terço maior que o original, e decodificar é simplesmente rodar a troca ao contrário. Guarde a forma do problema em mente e vamos abrir alguns envelopes.
Aqui está o detalhe que torna o R um pouco diferente de muitas outras linguagens: o R base não tem Base64 nenhum. Não existe um base64_decode() escondido em algum pacote base, e não existe builtin de uma linha para você recorrer. Você precisa trazer um pacote. A boa notícia é que o ecossistema oferece vários, cada um com a sua personalidade, e até o fim deste artigo você vai saber exatamente qual usar e qual desconfiar.
Os decodificadores em um relance
Cinco pacotes fazem o trabalho pesado, e eles se dividem em dois grandes acampamentos: os lenientes, que dão de ombros para entrada suja, e os estritos, que tratam o RFC como um contrato. Aqui está o elenco, atualizado até 2026:
| Pacote | Versão (2026) | Pontos de entrada de decodificação | Personalidade |
|---|---|---|---|
base64enc |
0.1-6 | base64decode() |
Leniente por padrão, ganhou um modo strict em fevereiro de 2026 |
openssl |
2.4.2 | base64_decode() |
Pula espaços em branco, mas falha em silêncio de maneiras que merecem uma olhada séria |
b64 |
0.1.7 | decode(), decode_as_string() |
Estrito, vetorizado, escrito em Rust, rápido |
base64 |
2.0.2 | decode() |
Wrapper prático de arquivo para arquivo em cima do openssl |
base64url |
1.4 | base64_urldecode() |
Alfabeto seguro para URL, sem padding, tolerante em silêncio |
Três coadjuvantes merecem uma menção. O pacote jsonlite exporta os seus próprios auxiliares, base64_enc, base64_dec e o par seguro para URL, então se você já faz parse de JSON, pode ter um decodificador à mão. O pacote jose traz o base64url_decode() para trabalho com JWT. E o pacote antiquado RCurl ainda carrega uma função base64() que embrulha o libcurl: ela funciona, é orientada a caracteres, e tem o ar de um avô fiel, e não o de uma ferramenta para código novo.
Instalando o elenco
Se o R ainda não está na máquina, o seu sistema operacional o fornece: r-base no Debian e no Ubuntu, R no Fedora, um pacote ou instalador no macOS e no Windows. Depois os pacotes, direto do CRAN:
install.packages("base64enc")
install.packages("openssl")
install.packages("b64")
install.packages("base64url")
Duas notas de compilação, porque são nestes lugares que as instalações saem dos trilhos. O pacote openssl compila contra o OpenSSL do seu sistema, então uma caixa Linux pura pode querer primeiro os cabeçalhos de desenvolvimento:
sudo apt install libssl-dev
O pacote b64 é um motor em Rust embrulhado com extendr, então compilá-lo do source quer a toolchain do Rust (sudo apt install cargo também puxa o rustc). No Windows e no macOS você recebe binários pré-compilados do CRAN e nada disso se aplica. Se você prefere um gerenciador de pacotes, pak::pkg("base64enc") ou remotes::install_cran("b64") fazem o mesmo trabalho com menos opiniões sobre repositórios.
A sua primeira decodificação: o rito de três linhas
Noventa por cento da vida de decodificação cabe em três linhas. Aqui está o teste de fumaça canônico, usando a famosa string TWFu:
library(base64enc)
packed <- "TWFu"
bytes <- base64decode(packed)
bytes
#> [1] 4d 61 6e
rawToChar(bytes)
#> [1] "Man"
Três coisas valem a pena notar naquela pequena cerimônia. Primeira, base64decode() sempre te entrega um vetor raw, nunca uma string. Isso é um recurso, não um acidente: o Base64 pode carregar uma frase, um JPEG ou um certificado, e nenhum deles deve ser tratado de forma diferente antes de você saber o que tem em mãos. Segunda, o salto dos bytes de volta para o texto é um passo separado e deliberado através do rawToChar(), e é nesse passo que mora a decisão do charset (mais sobre isso adiante). Terceira, TWFu decodifica para a palavra "Man": três letras, zero drama. Guarde essa string no bolso. Se um pedaço de código de decodificação transforma TWFu em Man, a máquina é honesta.
Porque você sempre vai acabar decodificando algo que codificou você mesmo, aqui está o round trip completo que prova que as duas direções concordam:
text <- "Hello, world!"
packed <- base64encode(charToRaw(text))
packed
#> [1] "SGVsbG8sIHdvcmxkIQ=="
identical(text, rawToChar(base64decode(packed)))
#> [1] TRUE
A fronteira do vetor raw
O R cruza a fronteira byte/texto com duas funções minúsculas, e uma vez que você as conhece, o resto deste artigo parece óbvio. charToRaw() transforma uma string nos seus bytes, rawToChar() faz o contrário, e entre as duas você tem a caixa de ferramentas raw inteira: length() para contar bytes, head() para dar uma espiada, writeBin() e readBin() para movê-los para e de arquivos. Todo decodificador neste artigo para naquela fronteira de propósito.
bytes <- base64decode("SGVsbG8s")
length(bytes)
#> [1] 6
head(bytes)
#> [1] 48 65 6c 6c 6f 2c
rawToChar(bytes)
#> [1] "Hello,"
Então quando você vê um resultado como [1] 48, leia como "um byte, valor 72, a letra H", e pare ali até saber qual charset os bytes estão vestindo. Essa pausa é toda a disciplina da decodificação.
Entrada suja: como os decodificadores divergem
Esta é a seção que te salva às 2 da manhã, porque os decodificadores não concordam sobre o que acontece quando a entrada está um pouco suja. Base64 do mundo real chega com espaços no meio, padding descartado por um copy-paste nervoso, um símbolo errante de uma área de transferência que o comeu, ou lixo pendurado no final. Aqui está como cada decodificador responde, diante da mesma família de infratores:
| Entrada | base64enc (padrão) | base64enc (strict = TRUE) | openssl | b64 |
|---|---|---|---|---|
"SGVs bG8s IHdvcmxkIQ==" (um espaço no meio) |
"Hello, world!" (espaço pulado) | erro: caractere inválido, posição informada | "Hello, world!" (espaço pulado) | erro |
"SGVsbG8sIHdvcmxkIQ" (padding descartado) |
"Hello, world!" (padding ausente tolerado) | erro: padding ausente | erro: falha ao decodificar | erro: padding inválido |
"SGVsbG8s!IHdvcmxkIQ==" (um "!" no meio) |
"Hello, world!" (caractere ruim pulado) | erro: caractere inválido | vetor raw vazio, sem erro | erro |
"SGVsbG8sIHdvcmxkIQ==xx" (lixo no final) |
"Hello, world!" (lixo ignorado) | erro: conteúdo no final | vetor raw vazio, sem erro | erro |
"TQ==" (limpo) |
M | M | M | M |
Leia aquela tabela duas vezes, porque ela contém a história inteira. O base64decode() padrão é o fiscal de fronteira amigável: ele pula caracteres fora do alfabeto, tolera padding ausente e ignora conteúdo no final, então strings formatadas como e-mail e como área de transferência simplesmente passam. O modo strict = TRUE, que chegou com a release 0.1-5 em fevereiro de 2026 depois de um longo silêncio, é o perito forense: ele valida a string inteira, aponta a posição exata do infrator e recusa qualquer coisa que não seja de manual. b64 é estrito por padrão e nunca tem sucesso pela metade. E o openssl fica no meio desconfortável: ele pula espaços em branco sem problema e lança erro corretamente quando o padding falta, mas quando encontra um caractere genuinamente ilegal ou lixo no final ele devolve um vetor raw vazio e não diz absolutamente nada. Aquele resultado vazio em silêncio é o comportamento mais perigoso de todo este artigo, então vamos assistir a ele acontecer:
dirty <- "SGVsbG8s!IHdvcmxkIQ=="
openssl::base64_decode(dirty)
#> raw(0) # vazio. sem erro. sem aviso. sem pista.
Se você escreve código contra o openssl, cheque o comprimento do que você recebeu antes de confiar nele. É um pequeno hábito que previne uma classe inteira de mistérios do tipo "para onde foram os meus dados".
Pelo lado do acampamento estrito, aqui está o base64enc sendo preciso, com as mensagens de erro exatas que você vai ver no seu próprio 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)
Uma singularidade para esperar: toda chamada strict, sucesso ou falha, solta uma linha de status de nível C no console, algo como v=30000, pad=0, org='u'. É um write direto do código compilado, não um warning do R, então o suppressWarnings() não vai tocá-la. É inofensivo, mas se os seus testes comparam saída de console, você agora sabe por que existe uma linha extra.
Especificidades do R: NA, vetores e concatenação em silêncio
O Base64 tem as suas excentricidades; o R adiciona as dele. A primeira morde através do NA. Quando você passa NA_character_ para um decodificador, o R converte silenciosamente para a string "NA" antes de o decodificador vê-la, e "NA" é um grupo Base64 perfeitamente válido. O resultado não é um erro e não é um vetor vazio. É o byte 0x34, a letra "4":
base64decode(NA_character_)
#> [1] 34
rawToChar(base64decode(NA_character_))
#> [1] "4"
Então uma coluna de valores ausentes não decodifica para valores ausentes. Ela decodifica para uma coluna da letra 4, e o seu código a jusante processa com boa vontade. Se a sua entrada pode conter NA, filtre primeiro:
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"
A segunda excentricidade é a entrada vetorizada, e os decodificadores tomam rumos completamente diferentes. base64decode() trata um vetor de caracteres como vários pedaços de uma única string e os concatena, então dois elementos voltam como um único vetor raw. openssl::base64_decode() nem chega até lá: ele colapsa o seu argumento e então esbarra num teste lógico de dois elementos, o que produz uma das mensagens de erro mais honestas do R. E b64::decode() é o único que é verdadeiramente vetorizado, devolvendo um blob decodificado por entrada:
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"
Conclusão: faça loop ou vetorize de propósito, e nunca assuma que um vetor de entradas te dá um vetor de saídas.
O alfabeto seguro para URL
O Base64 padrão gasta as duas últimas vagas do seu alfabeto no + e no /, e são exatamente esses caracteres que as URLs não gostam. O mais vira %2B, a barra vira %2F, e o padding vira %3D, então um token que deveria ser colável em qualquer lugar começa a vestir sinais de porcentagem. A variante segura para URL, definida na seção 5 do RFC 4648, troca aqueles dois caracteres por - e _ e normalmente descarta o padding também. O R tem três portas para esse mundo.
Porta um são os motores do b64. Um motor é só um alfabeto configurado e uma política de padding, e o pacote traz os quatro que você precisa:
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"
Os motores são "standard" (o padrão), "standard_no_pad", "url_safe" e "url_safe_no_pad", e o mesmo objeto de motor funciona nas duas direções, o que mantém o seu código simétrico. Eles também são estritos com o alfabeto: alimente "----" para o motor standard e ele responde "Invalid byte 45, offset 0.", o que é um alívio.
Porta dois é o pacote dedicado base64url, pequeno e de propósito único: o alfabeto seguro para URL, sem padding, sempre. Uma ressalva: o decodificador dele é leniente, então ele decodifica com prazer o prefixo válido de uma string corrompida e para ali sem reclamar. Se um valor seguro para URL veio de uma fonte não confiável, prefira b64.
base64url::base64_urlencode("hello world")
#> [1] "aGVsbG8gd29ybGQ"
base64url::base64_urldecode("aGVsbG8g!!!")
#> [1] "hello " # o resto foi descartado em silêncio
Porta três é o jose, que exporta base64url_encode() e base64url_decode() para trabalho com JWT, e devolve vetores raw do jeito que você esperaria.
E quando você precisa só de um one-off ocasional e quer ficar dentro do base64enc, cirurgia em string mais um conserto de padding é o suficiente:
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"
JWTs: espiar e confiar
O motivo pelo qual a maioria das pessoas encontra o Base64 seguro para URL é o JSON Web Token. Um JWT é três partes base64url juntadas por pontos: um header descrevendo como ele foi assinado, um payload de claims, e uma assinatura que torna a coisa toda confiável. Decodificar as duas primeiras partes no R é um assunto de cinco linhas. Note o fixed = TRUE no strsplit(): o ponto é um curinga de regex, e sem aquela flag o R divide alegremente o seu token em caracteres únicos, o que é o bug Base64 mais comum em código R no mundo real.
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"
Dois avisos honestos. Primeiro, decodificar um JWT é espiar, não confiar: a terceira parte é a assinatura, e ela só significa alguma coisa quando verificada contra a chave do emissor. Para isso, o pacote jose cuida da família inteira. A sua release 2.0 (abril de 2026) adiciona suporte a ED25519 e torna o header typ opcional; tutoriais antigos mostrando os auxiliares 1.x jwk_read()/jwk_write() (ou os seus aliases read_jwk()/write_jwk()) continuam válidos, porque a 2.0 ainda traz aqueles mesmos auxiliares de leitura/escrita de 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 a assinatura e também aplica os claims exp e nbf, lançando um erro se o token estiver expirado. E segundo: base64url::base64_urldecode() e companhia decodificam o header e o payload sem problema, mas a parte da assinatura é binária, então decodifique-a com uma função que devolve raw (como b64::decode() com o motor "url_safe_no_pad") em vez de transformar em string.
Charsets: que texto são aqueles bytes?
Todo decodificador neste artigo para na fronteira do vetor raw de propósito, porque a resposta para "que texto era aquele?" depende do charset em que os bytes foram empacotados. A boa notícia de cara: se o remetente usou UTF-8, que é a maior parte da web moderna, a sua vida é curta e feliz. O base64enc até traz um portão para isso:
packed <- base64encode(charToRaw("caf\u00e9"))
bytes <- base64decode(packed)
checkUTF8(bytes, quiet = TRUE)
#> [1] TRUE
rawToChar(bytes)
#> [1] "café"
checkUTF8() pergunta "esses bytes podem ser lidos como UTF-8?" sem se comprometer. Com quiet = TRUE ele devolve TRUE ou FALSE; por padrão ele é ainda mais direto e lança um erro em bytes inválidos, apontando o infrator, como em INVALID byte 0xff at 0x0 (0, line 1): invalid start byte (FE/FF). Ele também relata FALSE para strings que contêm bytes NUL, que nunca podem fazer parte de uma string R UTF-8 válida. Use-o como um portão, e o resto segue.
Aqui está o motivo de o portão importar. Num locale UTF-8, rawToChar() não lança erro quando os bytes não são UTF-8 válido. Ele te entrega uma string que o R marca com Encoding() == "unknown", e dependendo do locale e da build do R a mesma chamada pode em vez disso morrer com um erro de "invalid multibyte sequence", o que é pelo menos honesto. De qualquer jeito, rawToChar() sem proteção sobre bytes estranhos é assim que o mojibake nasce:
broken <- as.raw(c(0xc3, 0x28)) # um par UTF-8 truncado
checkUTF8(broken, quiet = TRUE)
#> [1] FALSE
rawToChar(broken) # sem erro. esse é o problema.
#> [1] "\xc3("
E quando o remetente usou outra coisa completamente diferente, Latin-1 ou Windows-1252 ou Shift-JIS, a receita é decodificar para raw e depois traduzir com iconv(), que entende os charsets nomeados:
latin1_bytes <- as.raw(c(0x63, 0x61, 0x66, 0xe9)) # "café" em Latin-1
iconv(rawToChar(latin1_bytes), from = "latin1", to = "UTF-8")
#> [1] "café"
Emoji merecem uma palavra, porque o R tem uma história aqui. O R moderno armazena code points acima de U+FFFF, onde os emoji moram, como UTF-8 verdadeiro, então um round trip funciona e a contagem de bytes confere com o que toda outra linguagem espera. Releases mais antigos do R armazenavam aqueles mesmos caracteres como pares de surrogates, um esquema muitas vezes chamado de CESU-8, então um único emoji cruzava a fronteira como seis bytes de UTF-8 inválido. Se você herda código R antigo cujos emoji chegam quebrados no fio, essa é a principal suspeita: cheque nchar(x, type = "bytes") e compare com o que a linguagem do remetente produziria.
emoji <- "\U0001F600"
nchar(emoji, type = "bytes")
#> [1] 4
round_trip <- base64decode(base64encode(charToRaw(emoji)))
identical(emoji, rawToChar(round_trip))
#> [1] TRUE
A regra prática: assuma UTF-8, verifique com checkUTF8(), e só recorra ao iconv() quando você tiver conhecimento positivo de outro charset. Nunca chute.
Arquivos: de texto quebrado em linhas para bytes de verdade
Strings são fáceis; arquivos é onde o Base64 ganha o seu pão. Comece pelo pipeline manual que funciona com qualquer pacote: leia o texto codificado, decodifique, escreva os bytes.
cat("SGVsbG8sIHdvcmxkIQ==", file = "hello.b64")
bytes <- base64decode(paste(readLines("hello.b64"), collapse = ""))
writeBin(bytes, "hello.txt")
readLines("hello.txt")
#> [1] "Hello, world!"
Depois a camada de conveniência. O base64enc pode ler e escrever por você: o argumento file aponta para um arquivo contendo texto Base64, e output aponta para onde os bytes decodificados devem cair. O valor de retorno é o número de bytes escritos, um cheque de sanidade grátis e gostoso:
n <- base64decode(file = "hello.b64", output = "hello.txt")
n
#> [1] 13
readLines("hello.txt")
#> [1] "Hello, world!"
Entrada quebrada em linhas, o tipo que sobreviveu a e-mail ou à couraça PEM, não é problema para os decodificadores lenientes: eles olham direto através das quebras de linha onde quer que caiam, desde que o que reste seja Base64 válido. O MIME quebra em 76 caracteres com CRLF entre as linhas; blocos PEM quebram em 64. O motor estrito, claro, rejeita as quebras de cara.
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.
O pacote b64 tem o par de funções de arquivo com os nomes mais claros, e como é rápido como Rust é nele que você recorre quando o arquivo é grande. Existe, porém, uma borda afiada: decode_file() lê o arquivo byte a byte e não perdoa. Um newline final - o tipo que o writeLines() adiciona mas o cat() nunca adiciona - faz o motor em Rust paniquear, aparecendo como um erro capturável do tipo User function panicked: decode_file_. Escreva o texto codificado com cat() ou writeBin(charToRaw(enc), path) e a borda fica obtusa.
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"
O pacote base64 é o terceiro caminho: puramente orientado a arquivos, com um par de funções correspondentes:
base64::encode("payload.bin", "payload.b64")
base64::decode("payload.b64", "payload.out")
readLines("payload.out")
#> [1] "file payload bytes"
Uma forma de arquivo a mais que você vai encontrar na prática: uma coluna de data frame cheia de blobs codificados, muito comum em dumps de API. Para algumas centenas de linhas, uma chamada vetorizada simples é suficiente:
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"
APIs e respostas web
APIs JSON adoram enfiar binário dentro de texto, e o Base64 é a mala favorita delas. O padrão no R é o mesmo toda vez: busque, faça parse, decodifique e decida o que os bytes são. O cliente HTTP moderno é o httr2, e o lado JSON é o 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
Três notas para o caminho. O construtor de request é o request(), e ele tem esse nome desde a primeira release do httr2. O corpo da resposta chega como um vetor raw, então rawToChar() antes de fazê-lo parse como JSON. E a API pode falar um dialeto: verifique se o Base64 dela é seguro para URL, ou se está sem padding, antes de alimentá-lo num decodificador que espera a versão de manual. Algumas APIs até fazem dupla codificação, Base64 de Base64, o que você reconhece quando uma decodificação te dá mais Base64 em vez dos bytes esperados.
Data URIs e documentos autocontidos
O esquema data: de URI (RFC 2397) deixa um documento carregar o seu próprio conteúdo: um tipo MIME, a palavra "base64" quando o payload está codificado, e o próprio payload. Você vai encontrar esses URI em todo HTML que raspa, e decodificar um é uma operação de string em duas etapas seguida de uma decodificação comum:
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
A mesma forma aparece em CSS, em anotações de PDF, e em relatórios R Markdown autocontidos, onde um gráfico foi achatado numa tag <img> para o HTML viajar sem arquivos à parte. Se você está renderizando documentos assim e quer extrair as imagens embutidas, este é o truque inteiro: encontre a string data:, corte na primeira vírgula, decodifique.
Bancos de dados, e-mail e configuração
O Base64 aparece em bancos de dados sempre que alguém quis binário dentro de uma coluna de texto, e o lado da decodificação é uma coluna de strings de volta para bytes. Aqui está o round trip contra SQLite através do DBI e do RSQLite: armazene o valor codificado, consulte de volta, decodifique, e você tem os seus 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"
O SQLite também pode armazenar binário nativamente como BLOB, no qual caso nenhum Base64 é necessário e a coluna volta para o R como um vetor raw, como em class(dbGetQuery(db, "SELECT payload FROM blobs")$payload[[1]]) == "raw". A variante Base64-em-TEXT existe para portabilidade: você pode inspecioná-la com um editor de texto, e toda outra linguagem pode lê-la sem drivers binários.
E-mail é de onde o Base64 quebrado em linhas vem. Partes MIME quebram em 76 caracteres com CRLF entre as linhas, e qualquer sistema de correio que você já leu carregou anexos exatamente assim. O R não tem cliente de correio de primeira classe, mas quando você recebe um arquivo .eml, a parte base64 de um anexo é só texto quebrado em linhas: os decodificadores lenientes vão desembrulhar por você enquanto decodificam. O pacote mime te ajuda a identificar o que você está segurando:
mime::guess_type("photo.jpg")
#> [1] "image/jpeg"
part <- "SGVsbG8s\r\nCnRoaXMgaXMgYW4g\r\nZW1haWwgYXR0YWNobWVudA=="
rawToChar(base64decode(part))
#> [1] "Hello,\nthis is an email attachment"
Arquivos de configuração completam o passeio. Quando um certificado ou um blob está armazenado codificado em Base64 numa configuração YAML ou JSON, ou numa variável de ambiente, o R lê como uma string comum e você decodifica sob demanda:
config_line <- "api_cert: TFRTU0VDUkVU"
cert_b64 <- sub("^api_cert: ", "", config_line)
raws <- base64decode(cert_b64)
rawToChar(raws)
#> [1] "LTSSECRET"
Payloads grandes
Strings do R têm um teto rígido de 2^31 - 1 bytes, e uma string Base64 grande o suficiente pode esbarrar nele, porque a forma codificada é cerca de um terço maior que o original. O pacote base64enc lida com isso desde a sua release de 2022: dê a base64encode() uma largura de linha e ele devolve um vetor de linhas em vez de uma string enorme, e no lado da decodificação o argumento file = lê o arquivo como linhas e decodifica sem forçar você a segurar uma string gigante numa variável.
Para arquivos na casa das centenas de megabytes, o padrão prático é ler em chunks alinhados. Grupos Base64 são independentes, então qualquer chunk cujo comprimento seja múltiplo de quatro caracteres decodifica por conta própria; você só precisa de um buffer pequeno para carregar a ponta irregular:
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)
O pacote b64 é o campeão de velocidade neste território: o seu motor em Rust decodifica um arquivo de 50 megabytes em uma fração de segundo, e o seu decode() vetorizado lida com uma coluna de muitos valores codificados numa única chamada, o que é uma diferença dramática de fazer loop linha por linha. Se você tem muitas strings, rode uma comparação rápida com system.time() você mesmo; a diferença entre um loop por linha e uma chamada vetorizada costuma ser grande o suficiente para importar.
A linha de comando
Nem tudo precisa de uma sessão R completa. As ferramentas Unix clássicas falam Base64 nativamente, e o R pode lhes entregar trabalho ou receber trabalho delas. No Linux, base64 -d decodifica (macOS e outros sistemas BSD usam -D):
echo "SGVsbG8sIHdvcmxkIQ==" | base64 -d
#> Hello, world!
E um Rscript de uma linha faz o mesmo trabalho com os mesmos pacotes que você usa nos seus scripts:
Rscript -e 'library(base64enc); writeLines(rawToChar(base64decode("TWFu")))'
#> Man
Use o shell para cheques rápidos e pipes; use R quando o resultado precise morar num data frame, num arquivo ou num relatório. Uma cautela: argumentos de linha de comando têm um limite de tamanho (ARG_MAX), então não cole strings de vários megabytes no terminal. Passe-as por um arquivo em vez disso.
Armadilhas que valem a pena conhecer
Aqui está a lista curta das formas como isso morde desenvolvedores R, todas nativas do ecossistema e não do Base64 em geral:
- openssl falha em silêncio.
base64_decode()devolve um vetor raw vazio, sem erro ou aviso, quando encontra um caractere fora do alfabeto ou lixo no final. Cheque sempre olength()do resultado antes de acreditar nele. - NA não é um valor ausente para um decodificador.
NA_character_é convertido para a string"NA", que decodifica para o byte0x34. FiltreNAantes de decodificar. - Vetores não se comportam como você espera.
base64decode()concatena a sua entrada num único vetor raw;openssl::base64_decode()morre comthe condition has length > 1em entrada de múltiplos elementos; só ob64::decode()é genuinamente vetorizado. - rawToChar é um corruptor silencioso. Bytes de UTF-8 inválido não lançam erro num locale UTF-8; eles viram uma string marcada
"unknown". Proteja comcheckUTF8()primeiro. - O ponto num JWT é um curinga de regex.
strsplit(jwt, ".")semfixed = TRUEdivide em cada caractere. Este é o bug Base64 mais comum em código R. - Arquivos b64 são implacáveis. Um newline final no arquivo codificado faz
b64::decode_file()paniquear. Escreva comcat(), não comwriteLines(). - Leniente não é leniente o suficiente para confiança. O modo padrão de
base64decode()pula caracteres ruins em qualquer lugar, então uma string corrompida pode decodificar para lixo com cara plausível sem erro. Usestrict = TRUEnas fronteiras de confiança. - Mensagens de erro do b64 falam Rust. Espere frases como
Both cases of Either erroredquando você lhe passar um tipo que ele não quer. A mensagem é inútil de propósito; é um sistema de tipos do Rust dando de ombros.
Boas práticas
- Padrão para o trabalho do dia a dia:
base64enc::base64decode(), e ativestrict = TRUEsempre que a entrada cruza uma fronteira de confiança: APIs não confiáveis, uploads de usuário, qualquer coisa assinada. - Recorra ao
b64quando você precisa de velocidade, vetorização verdadeira ou dos motores seguro para URL e sem padding, e aceite que ele é estrito por design. - Se
openssljá está no seu projeto, use, mas trate todo resultado como suspeito até checar o seu comprimento. - Decida o charset de propósito: assuma UTF-8, verifique com
checkUTF8(), e só useiconv()quando o remetente te disser o contrário. - Para JWTs, espie com
strsplit()maisfixed = TRUE, mas confie só depois que ojoseverificar a assinatura. - Mantenha
TWFunos seus testes: são três bytes de teste de fumaça para qualquer caminho de decodificação que você escrever. - Lembre do que o Base64 não é: ele não é criptografia, e não é compressão. É fita adesiva, e qualquer pessoa com este artigo pode reverter tudo o que ele faz. Decodifique à vontade, confie seletivamente.
Uma breve história do Base64 no R
O formato é antigo. Foi padronizado para o protocolo Privacy-Enhanced Mail em 1987 (RFC 989), adotado pelo MIME em 1993 (RFC 1521, depois o final RFC 2045 em 1996), arrumado no RFC 3548 em 2003, e recebeu a sua forma moderna, incluindo o alfabeto seguro para URL, no RFC 4648 em 2006. A história do R é muito mais curta e rápida. O pacote base64enc, de Simon Urbanek, chegou ao CRAN pela primeira vez em setembro de 2012 e tem sido o cavalo de batalha desde então, ganhando checkUTF8() em 2015 e suporte a vetores longos em 2022. Em outubro de 2024 o antigo pacote base64 foi reeditado explicitamente como wrapper de compatibilidade, com a sua própria descrição agora apontando aplicações novas para base64enc, openssl ou jsonlite. Depois veio o b64 no início de 2024 (release atual 0.1.7, julho de 2025), um motor em Rust construído com extendr que trouxe vetorização verdadeira e um estábulo de alfabetos. Em fevereiro de 2026, o base64enc soltou o seu aguardado modo strict, e em abril de 2026 o pacote jose se redesenhou em torno de funções jwt_*. Dez anos para o que outras linguagens entregaram numa biblioteca só, mas o resultado é uma caixa de ferramentas onde todo decodificador tem um trabalho claro e uma personalidade clara.
Fatos divertidos
Porque um guia completo deveria terminar com um sorriso:
base64decode(NA_character_)devolve a letra"4", porqueNA_character_viaja para o decodificador como a string"NA", e"NA"é um grupo Base64 válido. A surpresa de um byte mais específica do R na linguagem.- Alimente
openssl::base64_decode()com uma string com um caractere ilegal e ele devolveraw(0)em silêncio total. O silêncio mais perigoso do ecossistema. - Toda chamada strict para
base64decode()solta uma linha de status de nível C comov=30000, pad=0, org='u'. Não é um aviso. É o código C limpando a garganta. b64pode decodificar alfabetos que você nunca encontrou: BinHex, UTF-7 modificado do IMAP, e os alfabetos personalizados de bcrypt e crypt. O R pode ler um anexo de Macintosh dos anos 1980 e um hash de senha moderno com o mesmo motor.- Strings do R param em 2^31 - 1 bytes, e é por isso que o
base64encaprendeu a devolver um vetor de linhas para entrada longa. O formato bateu na parede da linguagem, e o pacote cresceu uma escada. - A palavra "base64" codifica para
YmFzZTY0. Um formato que pode se descrever é o equivalente técnico de um espelho que fala em Morse. - Um único byte NUL ainda custa quatro caracteres:
"AA==". No Base64, nada é sempre algo. - A sua build do R tem um apelido. O R 4.5.0 se chama "How About a Twenty-Six", porque as versões do R têm nome de quadrinhos e filmes do Peanuts, e as tiras de onde elas se inspiram são décadas mais antigas que as releases que elas batizam. Até a versão com a qual você está decodificando tem senso de humor.
Resumo final
Escolha o seu decodificador pela companhia que você mantém: base64enc para trabalho do dia a dia, com strict = TRUE nas bordas; openssl se ele já está no seu projeto, desde que você cheque os seus resultados; b64 quando você quer velocidade, vetores e os motores seguros para URL; e os pequenos especialistas base64 e base64url para tarefas de arquivo e strings seguras para URL. Guarde os seus bytes com checkUTF8() antes de chamá-los de texto, use iconv() quando o charset é conhecido por ser outra coisa, e lembre que o vetor raw no meio é um recurso: ele te força a decidir o que os bytes significam em vez de deixar uma biblioteca chutar. Decodifique tudo, confie só no que se verifica. E quando você precisar ir na outra direção, empacotando os seus próprios bytes numa string para a estrada em vez de desempacotar uma, o artigo irmão cobre a codificação Base64 no R em detalhes.
Última atualização: 2026-09-08
Artigo relacionado: Codificação Base64 em R: um guia completo