Decodifica Base64 in Ruby: una guida completa
Da qualche parte, tra te e i dati originali, c'è un muro di caratteri: lettere maiuscole e minuscole, cifre, magari un più o una barra o un trattino, e forse un segno di uguale parcheggiato in coda. Il tuo editor non ha la più vaga idea di che tipo di file si tratti. Il tuo database l'ha infilato in una colonna di testo. È arrivato in un header HTTP, in un URL, in una chiave YAML o in un ticket di assistenza con un allegato .b64. Lo riconosci in un istante - Base64 - e ora ti servono indietro i byte. In Ruby, a quello che ti serve arrivi con una sola istruzione require e una chiamata di metodo.
Qualora il formato ti fosse nuovo, ecco la versione in trenta secondi. Base64 riscrive i dati grezzi tre byte alla volta: ogni gruppo di tre byte diventa quattro caratteri presi da un alfabeto di 64 simboli, e quando l'input non è divisibile esattamente per tre, in coda vengono aggiunti uno o due caratteri = come riempimento, così che l'output atterri sempre su un multiplo di quattro. La decodifica è il viaggio di ritorno - quattro caratteri dentro, tre byte fuori - quindi il risultato è sempre più piccolo dell'input, circa tre quarti delle sue dimensioni. La home page di questo sito percorre ogni dettaglio del formato, quindi questa guida spende le sue energie dove compete: dalla parte Ruby del lavoro.
La buona notizia: ogni installazione di Ruby include la cassetta completa per la decodifica. Il modulo Base64 non richiede nulla da installare, e i suoi tre decoder sono così piccoli da poterne leggere l'intero sorgente in un sol seduto. La nota di cautela: il decoder che raggiungi per primo è anche quello che non si lamenta mai, ed è una qualità preziosa per l'email e una catastrofe per la sicurezza. A fine guida saprai esattamente cosa accetta ogni decoder, come trasformare i byte che restituisce in un testo che Ruby ti lascerà usare, e cosa fare con ogni payload che uno sviluppatore Ruby decodifica davvero: JWT, header di autenticazione, data URI, corpi email, armatura PEM, file, blob di configurazione e anche quelli enormi.
Incontra la cassetta degli attrezzi
Tutto parte da un require. Non c'è alcun passaggio di installazione, nessuna stranezza di piattaforma, nessuna estensione nativa da compilare:
require "base64"
puts Base64::VERSION
# => 0.2.0 su un Ruby 3.3 stock, ad esempio
Ecco il lato decodifica dell'intera cassetta degli attrezzi in un'unica tabella, nell'ordine di frequenza con cui userai ogni metodo:
| Decoder | Trattamento dei caratteri estranei | Regole sul riempimento | Quando qualcosa non torna |
|---|---|---|---|
Base64.decode64(str) |
ignora tutto ciò che non fa parte dell'alfabeto standard, inclusi a capo e spazi | accetta qualunque cosa, anche un riempimento sbagliato | niente - non solleva mai eccezioni, restituisce semplicemente ciò che è riuscito a decodificare |
Base64.strict_decode64(str) |
rigetta qualsiasi carattere fuori dall'alfabeto standard | deve essere presente ed esattamente corretto | solleva ArgumentError |
Base64.urlsafe_decode64(str) |
accetta l'alfabeto URL-safe e quello standard, rigetta tutto il resto | facoltativo, ma se presente deve essere corretto | solleva ArgumentError |
Se ti piace sapere cosa fanno i tuoi strumenti sotto il cofano, l'intero lato decodifica del modulo è un sottile involucro attorno a due template del meccanismo pack/unpack del core, che è implementato in C dentro il core di Ruby:
# l'intero lato decodifica del modulo, in forma condensata
def decode64(str)
str.unpack1("m")
end
def strict_decode64(str)
str.unpack1("m0")
end
Il template m è il lettore tollerante, m0 è quello rigoroso, e quella singola differenza di un carattere spiega tutto il divario di personalità tra i primi due decoder. Dato che il lavoro pesante avviene alla velocità del core, il modulo resta in Ruby puro e continua a masticare megabyte in pochi millisecondi.
decode64: il camaleonte
Base64.decode64 è il decoder che dice di sì a tutto. Dagli un payload pulito e lo decodifica. Dagli un blob in stile MIME pieno di a capo e alza le spalle. Dagli una stringa che non è Base64 in assoluto e ti restituisce quello che riesce a strappare fuori, senza un singolo avviso:
require "base64"
Base64.decode64("aGVsbG8gd29ybGQ=")
# => "hello world"
Base64.decode64("Zm9vCmJh\ncgptYW4=\n")
# => "foo\nbar\nman"
Quella seconda riga è l'intera personalità in un solo esempio. Il decoder salta tutto ciò che non fa parte dell'alfabeto standard - a capo, spazi, l'insolito carattere di controllo - e decodifica il resto. È esattamente il comportamento che il Base64 MIME dovrebbe avere, ed è per questo che decode64 è lo strumento giusto per tutto ciò che ha viaggiato via email.
Il rovescio della medaglia è ciò che lo rende pericoloso. Poiché il decoder non si lamenta mai, non ti dice mai nemmeno quando l'input era sbagliato:
Base64.decode64("not base64 at all!")
# => dieci byte di spazzatura dall'aspetto perfettamente plausibile
Base64.decode64("====")
# => ""
Il primo esempio individua i caratteri che capitano di essere lettere valide dell'alfabeto, li decodifica e ti restituisce byte che potresti essere tentato di scrivere direttamente su un file. Il secondo esempio restituisce una stringa vuota per una stringa di quattro caratteri di riempimento. Niente viene sollevato, niente viene registrato nei log. Se il tuo input non è fidato, quel silenzio è una funzione che vuoi spegnere - ed è esattamente a questo che servono i prossimi due decoder.
Un'altra eccentricità che vale la pena conoscere, perché è il genere di cosa che resta nascosta in produzione per mesi: la decodifica si ferma al primo carattere =. Ciò che viene dopo il riempimento non è un errore; semplicemente non viene mai letto:
Base64.decode64("aGVsbG8=Zm9vYmFy")
# => "hello" la parte "Zm9vYmFy" è invisibile al decoder
strict_decode64: il guardiano
Base64.strict_decode64 è il decoder con il blocchetto in mano. Accetta solo l'alfabeto standard (da A a Z, da a a z, da 0 a 9, più e barra), esige che il riempimento, quando c'è, sia esattamente corretto, e si rifiuta di produrre un singolo byte se anche una sola regola viene violata:
Base64.strict_decode64("aGVsbG8gd29ybGQ=")
# => "hello world"
Base64.strict_decode64("aGVsbG8gd29ybGQ")
# => solleva ArgumentError
Base64.strict_decode64("Zm9vCmJh\ncgptYW4=")
# => solleva ArgumentError
L'ultima riga è quella che conta: lo stesso payload che decode64 aveva decodificato con piacere ora fa partire un'eccezione per via di un singolo a capo. Riempimento mancante, riempimento in eccesso, un trattino, un trattino basso, uno spazio - tutto è un crimine, e l'intero payload sprofonda insieme:
begin
Base64.strict_decode64("aGVsbG8")
rescue ArgumentError => e
puts e.message
end
# => invalid base64
Il guardiano sorveglia persino gli angoli del formato che non ti verrebbe in mente di controllare. Quando una stringa Base64 termina con il riempimento, alcuni bit dell'ultimo carattere non vengono mai usati, e l'RFC dice che un encoder conforme deve impostarli a zero. Ruby lo verifica:
Base64.strict_decode64("QQ==")
# => "A"
Base64.strict_decode64("QR==")
# => solleva ArgumentError (i bit di riempimento non sono zero)
La seconda stringa si decodificherebbe nello stesso byte della prima, se il decoder fosse distratto. Ruby non è distratto. Nella pratica questo rende strict_decode64 la scelta giusta di default per ogni input che non hai codificato tu: trasforma refusi, troncamenti e l'alfabeto sbagliato in errori chiassosi e intercettabili, invece di corruzioni silenziose.
urlsafe_decode64: il diplomatico
Base64.urlsafe_decode64 esiste per i payload che viaggiano in posti dove + e / sono parole riservate: URL, token, identificatori di database. Internamente traduce l'alfabeto URL-safe (trattino e trattino basso) di nuovo in quello standard, normalizza il riempimento e passa il risultato al decoder rigoroso:
Base64.urlsafe_decode64("SGVsbG8gd29ybGQ")
# => "Hello world"
Base64.urlsafe_decode64("SGVsbG8gd29ybGQ==")
# => solleva ArgumentError (quindici caratteri richiedono un carattere di riempimento, non due)
Il primo esempio mostra la sua caratteristica più utile: un input senza riempimento va benissimo. Se la stringa non ha riempimento e la sua lunghezza non è un multiplo di quattro, il decoder aggiunge al posto tuo i caratteri = mancanti - che è esattamente ciò che producono i JSON Web Token, il più grande consumatore di Base64 URL-safe. Se invece il riempimento c'è, deve essere corretto, come nel decoder rigoroso.
C'è un'eccentricità che la documentazione non urla ai quattro venti: il diplomatico parla entrambe le lingue. Poiché il metodo riscrive trattini e trattini bassi prima di fare la decodifica rigorosa, accetta anche stringhe dall'alfabeto standard:
Base64.urlsafe_decode64("aGVsbG8=")
# => "hello" l'alfabeto standard è accettato anch'esso
Questa tolleranza è comoda, ma significa che non puoi usare questo metodo per stabilire da quale alfabeto provenga un payload. Se per te conta, ispeziona i caratteri tu stesso prima di decodificare.
E a differenza di decode64, il diplomatico non ha pietà per gli spazi vuoti. Un a capo in qualsiasi punto di un payload URL-safe fa partire un'eccezione ArgumentError, quindi se il tuo input viene da un file con righe avvolte, rimuovi prima gli a capo.
I byte non sono testo: il passaggio della codifica
Ecco il passaggio che fa inciampare anche gli sviluppatori esperti, perché Ruby lo rende visibile. Una stringa Base64 decodificata è sempre etichettata con la codifica ASCII-8BIT (nota anche come BINARY), che i dati originali fossero un PNG, un payload JWT o una lettera d'amore in UTF-8:
bin = Base64.decode64(Base64.strict_encode64("h\u{e9}llo"))
puts bin.encoding
# => ASCII-8BIT
puts bin.bytes
# => [104, 195, 169, 108, 108, 111]
Se il payload è binario - un'immagine, un file zip, un hash - lo lasci esattamente così com'è e lo scrivi con File.binwrite. Nessuna conversione, nessuna domanda. Se il payload è testo, i byte sono quasi certamente UTF-8, e devi dirlo a Ruby:
text = Base64.decode64(payload)
text.force_encoding("UTF-8")
if text.valid_encoding?
puts text
else
puts "not valid UTF-8 after all"
end
Le due chiamate fanno lavori diversi. force_encoding si limita a ri-etichettare i byte; valid_encoding? poi verifica che formino un UTF-8 vero. Esegui in quest'ordine, perché validare prima una stringa BINARY non ha nulla da validare. E una piccola trappola di confronto da ricordare per la vita: Ruby considera una stringa BINARY uguale a una stringa UTF-8 solo quando entrambe sono ASCII puro, quindi ri-etichetta prima di confrontare il testo decodificato con l'originale:
decoded = Base64.decode64("aMOpbGxv")
puts decoded == "h\u{e9}llo"
# => false stessi byte, etichette diverse
decoded.force_encoding("UTF-8")
puts decoded == "h\u{e9}llo"
# => true
JWT: leggere un token senza la chiave
Un JSON Web Token è tre stringhe Base64 attorcigliate insieme da punti: header, payload, firma. Le prime due sono Base64 URL-safe senza riempimento di documenti JSON, il che significa che un token è leggibile da chiunque lo veda - incluso tu, senza alcuna libreria:
require "base64"
require "json"
token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIn0.dW5zaWduZWQ"
header_part, payload_part = token.split(".")[0, 2]
JSON.parse(Base64.urlsafe_decode64(payload_part))
# => {"sub"=>"1234567890", "name"=>"Alice"}
Per il lavoro vero userai la gem jwt, che si occupa della parte che ti protegge davvero - la firma - e delle validazioni dei claim:
# Nel Gemfile: gem "jwt"
require "jwt"
token = JWT.encode(
{ sub: "1234567890", name: "Alice", exp: Time.now.to_i + 3600 },
"my-secret-key",
"HS256"
)
payload, header = JWT.decode(token, "my-secret-key", true, algorithm: "HS256")
puts payload["name"]
# => Alice
Due note di sicurezza stanno qui, perché entrambe sono costate a qualcuno incidenti veri. Prima, il payload non è cifrato: decodificarlo è leggere, non violare, e la firma è l'unica protezione, quindi non trattare mai un payload decodificato come input fidato. Secondo, fissa l'algoritmo in JWT.decode esattamente come mostrato. Ometterlo lascia che sia l'header stesso del token a decidere come verificare, e quell'unica punta di flessibilità è ciò che sfruttano i famosi attacchi di confusione algoritmica JWT.
Basic auth: la password nascosta in piena vista
Lo header di autenticazione più antico del web è il Base64 in persona. L'autenticazione Basic di HTTP invia le credenziali in forma codificata come user:password, dopo la parola Basic - e lo header accompagna ogni richiesta, quindi compare in ogni log che ti toccherà di debuggare. Decodificarne uno è un lavoro di rimozione e divisione:
require "base64"
header_value = "Basic YWxpY2U6czNjcjN0IQ=="
b64 = header_value.sub("Basic ", "")
decoded = Base64.decode64(b64)
user, password = decoded.split(":", 2)
puts user
# => alice
puts password
# => s3cr3t!
Il limite di 2 in split conta: una password può legittimamente contenere dei due punti, e vuoi sempre tagliare solo al primo. La stessa libreria standard di Ruby costruisce questo header al contrario, dentro Net::HTTP, usando direttamente il template pack del core:
require "net/http"
request = Net::HTTP::Get.new("https://example.org/api")
request.basic_auth("alice", "s3cr3t!")
puts request["Authorization"]
# => Basic YWxpY2U6czNjcjN0IQ==
E la nota di sicurezza che va detta anche se è ovvia: il Base64 è un traduttore, non un lucchetto. La Basic auth è accettabile solo su HTTPS. La codifica esiste perché le credenziali possano viaggiare sul cavo come testo stampabile, non perché siano segrete.
Data URI: l'immagine che non è un file
Una data URI nasconde un file intero dentro un URL: un tipo di contenuto, la parola base64, una virgola e i byte codificati. I browser le renderizzano nei tag img e nel CSS, e le app HTML a file singolo le adorano perché non c'è una seconda richiesta da fare. Costruirne una in Ruby è questione di una riga:
require "base64"
png = File.binread("logo.png")
data_uri = "data:image/png;base64,#{Base64.strict_encode64(png)}"
Decodificarne una è l'operazione inversa, con due dettagli che fanno inciampare. La virgola è il separatore, quindi dividi esattamente una volta, e la parte del tipo di contenuto può essere qualsiasi cosa, compreso il nulla:
data_uri = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
media_part, b64 = data_uri.split(",", 2)
puts media_part
# => data:image/png;base64
bytes = Base64.strict_decode64(b64)
File.binwrite("restored.png", bytes)
Usa strict_decode64 qui, non decode64: il payload di una data URI è una singola riga pulita, e vuoi un errore chiassoso se è corrotto. Tieni a mente anche la tassa sulle dimensioni - ogni immagine che inlinedi cresce di circa un terzo - quindi le data URI sono perfette per favicon, piccoli loghi e font, e un'idea pessima per le foto di testata.
Email: righe da sessanta caratteri e la gem mail
Il Base64 è stato inventato per l'email, e le cicatrici si vedono. SMTP è stato progettato per righe corte di testo a sette bit, quindi il Base64 MIME avvolge il suo output in righe corte, e un decoder conforme deve ignorare gli a capo. Il decode64 di Ruby si comporta esattamente così, quindi un corpo MIME avvolto è cibo facile:
body = "Zm9vCmJh\ncgptYW4=\n"
Base64.decode64(body)
# => "foo\nbar\nman"
Raramente scriverai quello a mano. La gem mail fa l'intero lavoro MIME per te: gli allegati vengono codificati in Base64 automaticamente, le righe vengono avvolte a 60 caratteri, comodamente sotto il limite di 76 caratteri di MIME, e gli header corretti vengono allegati:
# Nel Gemfile: gem "mail"
require "mail"
message = Mail.new do |m|
m.from = "dev@example.org"
m.to = "ops@example.org"
m.subject = "Binary report"
m.add_file("report.bin")
end
puts message.encoded
# la parte dell'allegato porta Content-Transfer-Encoding: base64
Lo stesso trucco si nasconde dentro gli header delle email. Una riga di oggetto non in ASCII arriva come parola codificata RFC 2047: un charset, la lettera B e Base64 tra due punti interrogativi. Decodificarne una a mano è un piccolo esercizio di chirurgia delle stringhe:
header_value = "=?UTF-8?B?w7wgc2VjcmV0cw==?="
charset, kind, b64 = header_value.sub(/\A=\?/, "").sub(/\?=$/, "").split("?")
text = Base64.decode64(b64).force_encoding(charset)
puts text
# => ü secrets
PEM: chiavi e certificati in armatura
Chiavi e certificati passano la maggior parte della loro vita dentro l'armatura PEM: una riga BEGIN, un blocco di Base64 e una riga END. L'armatura viene dagli anni '80 - il Privacy-Enhanced Mail è dove inizia tutta la stirpe del Base64 - ma è ancora il formato che i tuoi file .crt e .key indossano oggi.
Decodificare un file PEM a mano è solo togliere l'armatura e lasciare che il decoder tollerante mastichi gli a capo:
require "base64"
pem = File.read("server.key")
body = pem.lines
.reject { |line| line.start_with?("-----") || line.strip.empty? }
.join
key_bytes = Base64.decode64(body)
Per l'uso vero di solito salti il passaggio manuale e passi l'intera stringa PEM a OpenSSL, che legge l'armatura da sé:
require "openssl"
key = OpenSSL::PKey.read(File.read("server.key"))
puts key.class
# => OpenSSL::PKey::RSA, o quello che la chiave si rivelerà di essere
L'unico dettaglio di interoperabilità che vale la pena conoscere: le righe PEM sono classicamente lunghe 64 caratteri, e il decoder ignora gli a capo in ogni caso, quindi un avvolgimento a 60 caratteri o una singola riga enorme si decodificano allo stesso modo.
File e la convenzione .b64
Il formato di file più comune nel mondo del Base64 è un semplice file di testo con estensione .b64 (o a volte .base64) che contiene un payload codificato. Leggerne uno è un viaggio di andata e ritorno in tre passaggi:
require "base64"
encoded = File.read("payload.b64")
bytes = Base64.decode64(encoded)
File.binwrite("payload.bin", bytes)
Usa File.binwrite in uscita - un PNG o un zip decodificati sono binari, e la scrittura in modalità testo li corromperebbe sulle piattaforme che traducono le terminazioni di riga. Se il tuo file .b64 viene da uno strumento che avvolge le righe, decode64 gestisce gli a capo gratis. Se vuoi validare invece di tollerare, leggi il file in modalità binaria e rimuovi gli a capo prima di una decodifica rigorosa:
encoded = File.binread("payload.b64")
clean = encoded.delete("\r\n")
bytes = Base64.strict_decode64(clean)
La lettura binaria conta su Windows, dove la modalità testo riscrive le terminazioni di riga CRLF come LF - esattamente il genere di mutazione che non vuoi che accada dentro una stringa che stai per validare.
Base64 URL-safe: i payload che viaggiano nei link
Questa è la prospettiva del decoder sulla variante URL-safe, perché la scelta che fai qui cambia quale dei tre decoder raggiungerai. Il Base64 URL-safe (RFC 4648, sezione 5) scambia i due caratteri che agli URL non piacciono - + diventa -, / diventa _ - e di solito rimuove anche il riempimento. In Ruby lo incontrerai nei parametri di query, nei valori dei cookie, negli identificatori API, negli ID video in stile YouTube e, ovviamente, nei JWT.
Ecco come si comportano i tre decoder sugli stessi input, perché le differenze sono esattamente il posto dove nascono i bug:
| Input | decode64 | strict_decode64 | urlsafe_decode64 |
|---|---|---|---|
aGVsbG8= (standard, con riempimento) |
"hello" |
"hello" |
"hello" |
aGVsbG8 (senza riempimento) |
"hello" |
ArgumentError |
"hello" |
SGVsbG8gd29ybGQ- (trattino nell'ultimo gruppo) |
"Hello world" (con un byte di meno!) |
ArgumentError |
12 byte, la risposta corretta |
aGVsbG8=\n (a capo finale) |
"hello" |
ArgumentError |
ArgumentError |
aGVs!bG8= (punto esclamativo fuori posto) |
"hello" |
ArgumentError |
ArgumentError |
La terza riga è quella che morde. Un payload URL-safe decodificato col decoder standard perde in silenzio l'ultimo byte invece di far partire qualsiasi eccezione, perché decode64 ignora semplicemente il trattino. Se un payload può venire da un URL, decodificalo con urlsafe_decode64.
Una nota pratica: se ti servisse mai spostare un payload URL-safe in un contesto che capisce solo l'alfabeto standard (una libreria, un sistema esterno), il classico trucco di interoperabilità - tradurre l'alfabeto e aggiungere il riempimento da sé - fa tre righe:
def standardize_urlsafe(b64)
b64 = b64.tr("-_", "+/")
b64 += "=" * ((4 - b64.length % 4) % 4)
b64
end
Base64.strict_decode64(standardize_urlsafe("SGVsbG8gd29ybGQ"))
# => "Hello world"
Ti servirà raramente - urlsafe_decode64 aggiunge già il riempimento per te - ma è il pattern da riconoscere nel codice degli altri, e il pattern da usare quando l'alfabeto standard è ciò che si aspetta l'altra parte.
Configurazioni, variabili d'ambiente e database
Il Base64 compare nelle configurazioni ogni volta che i dati binari devono stare dentro un documento di testo. Un file .env, una configurazione YAML o un blob di impostazioni JSON non possono trasportare in sicurezza byte grezzi, quindi i byte vengono codificati, e qualcosa nella tua applicazione deve decodificarli all'avvio:
require "base64"
b64 = ENV.fetch("APP_LOGO")
bytes = Base64.decode64(b64)
File.binwrite("logo.png", bytes)
YAML merita una menzione speciale, perché il formato ha un tag binario nativo. Quando fai il dump di una stringa BINARY, Psych la scrive come scalare !binary contenente Base64, e caricandola ti restituisce i byte intatti - senza alcuna codifica manuale:
require "yaml"
yaml_text = YAML.dump({ "logo" => File.binread("logo.png") })
puts yaml_text.lines.first(2)
# => "---"
# => "logo: !binary |-"
data = YAML.load(yaml_text)
puts data["logo"].encoding
# => ASCII-8BIT
Nei database la regola pratica è: se il tuo database ha un tipo binario vero, usalo. Il Base64 in una colonna TEXT è il pattern che usi quando il layer di storage parla solo di stringhe - alcuni document store, API a forma di JSON, o uno schema legacy che non puoi cambiare - e il prezzo è la tassa di un terzo sulle dimensioni della colonna, più la disciplina di decodificare in entrata e ricodificare in uscita a ogni confine.
Input grandi, memoria stabile
Il modulo è basato su buffer: una chiamata di decodifica legge l'intera stringa in un colpo e restituisce il risultato intero. Non c'è un decoder in streaming nella libreria standard, quindi il consiglio onesto per i payload grandi è pianificare la memoria. La buona notizia è che la decodifica rende le cose solo più piccole - l'output è al massimo tre quarti dell'input - quindi la stringa di input è l'unica grande allocazione che fai.
Se un payload è abbastanza grande da farti preoccupare, puoi decodificarlo in gruppi di quattro caratteri, perché i gruppi di quattro del Base64 sono autosufficienti e l'ultimo gruppo parziale porta con sé il proprio riempimento:
require "base64"
def decode_in_chunks(b64)
b64.scan(/.{1,4}/).reduce("") do |result, group|
result + Base64.strict_decode64(group)
end
end
restored = decode_in_chunks(Base64.strict_encode64("a" * 1_000_000))
puts restored.length
# => 1000000
Questo funziona su input puliti e non avvolto - le stesse regole che strict_decode64 applica - perché un gruppo finale solitario è valido solo con il suo riempimento presente. Per i file davvero enormi, archivi multi-gigabyte e cose del genere, il pattern è leggere il file a fette, decodificare ogni fetta e mandare i byte su disco in streaming, in modo che in memoria ci sia sempre una sola fetta alla volta.
One-liner per il terminale
Non ti serve un file di script per decodificare in shell. Ruby può richiedere il modulo al volo:
ruby -rbase64 -e 'puts Base64.decode64(ARGV[0])' "aGVsbG8gd29ybGQ="
# => hello world
E per i file, passa il percorso di un file invece del payload stesso:
ruby -rbase64 -e 'print Base64.decode64(File.read(ARGV[0]))' payload.b64 > payload.bin
Due insidie vivono qui. Prima, se fai passare il dato attraverso echo o qualsiasi comando di testo, un a capo finale viaggia insieme, e strict_decode64 farà partire un'eccezione su di esso - usa decode64, oppure chomp l'input:
echo "aGVsbG8gd29ybGQ=" | ruby -rbase64 -e 'print Base64.strict_decode64(STDIN.read.chomp)'
Secondo, per l'output binario tieni print invece di puts, perché puts aggiunge un a capo suo e corromperebbe la fine del file ripristinato.
Le insidie che gli sviluppatori Ruby incontrano davvero
- decode64 non solleva mai eccezioni. Spazzatura dentro, spazzatura fuori. Se il tuo input non è fidato e accetti in silenzio byte corrotti, il bug affiorerà settimane dopo in un file corrotto, non sulla riga di decodifica. Di default usa un decoder rigoroso per tutto ciò che non hai codificato tu.
- strict_decode64 e l'a capo finale. I file di testo, i tubi di echo e il copia-incolla amano tutti finire con un a capo, e il decoder rigoroso solleva
ArgumentErrorsu di esso. Fai primachompall'input - oppure leggilo in modalità binaria ed elimina gli a capo. - Dimenticare il passaggio della codifica. Una stringa decodificata è BINARY finché non dici diversamente. Forza UTF-8 (e verifica la validità) prima di trattare il risultato come testo, altrimenti otterrai caratteri corrotti e
Encoding::CompatibilityErrornel momento in cui lo mescoli con stringhe UTF-8. - Confrontare BINARY con UTF-8. Stessi byte, etichette diverse, e
==dice false - a meno che la stringa non capiti di essere ASCII puro. Ri-etichetta prima di confrontare. - Input URL-safe attraverso il decoder sbagliato. Trattini e trattini bassi vengono eliminati in silenzio da
decode64, quindi un payload URL-safe torna con un byte di meno e corrotto, senza alcun errore. Usaurlsafe_decode64. - I dati dopo il riempimento sono invisibili.
decode64si ferma al primo=. Perfetto per il MIME, pessimo per beccare un payload che è stato troncato e poi riempito di nuovo da qualche altro strumento. - Il riempimento non canonico è accettato in silenzio. Una stringa come
QR==porta bit di riempimento che un encoder corretto avrebbe messo a zero;decode64la decodifica volentieri mentrestrict_decode64la rigetta. Niente ti dirà mai che il tuo encoder stava mentendo. - Le letture di file in modalità testo su Windows riscrivono le terminazioni di riga prima che tu le veda. Leggi i file
.b64in modalità binaria quando hai intenzione di validarli.
Buone abitudini per il lato decodifica
- Scegli il decoder in base alla provenienza dei dati:
strict_decode64per tutto ciò che non è fidato (e rescueArgumentErrorcome ramo per l'input non valido),urlsafe_decode64per i payload nati in un URL,decode64solo per formati genuinamente tolleranti, come i corpi MIME. - Nel momento in cui i byte sono decodificati, decidi la loro identità: binario (tieni ASCII-8BIT, scrivi con
File.binwrite) o testo (force_encodinga UTF-8, poivalid_encoding?prima dell'uso). - Non decodificare mai e fidarti. Un payload JWT è leggibile proprio perché è Base64; è la firma a decidere se è autentico. Una stringa Base64 in un file di configurazione è un dato, non una prova.
- Quando scrivi validatori, mettili alla prova con i casi noiosi: la stringa vuota, l'input senza riempimento, l'input avvolto, l'input URL-safe e il riempimento sbagliato. Sono quei casi a separare i tre decoder.
Una breve storia del Base64 in Ruby
Il modulo Base64 fa parte della libreria standard di Ruby da oltre quindici anni, e il modo in cui viene distribuito è cambiato più di quanto potresti immaginare:
- 2008, Ruby 1.8.7: il modulo arriva con
encode64,decode64, più due metodi che non esistono più -b64encode(avvolgimento a una lunghezza di riga scelta) edecode_b(decodifica di header email RFC 2047). I vecchi libri e persino alcune gem vecchie li citano ancora, e chiamare oggi l'uno o l'altro è unNoMethodError. - 2009, la linea 1.9: arrivano
strict_encode64,strict_decode64,urlsafe_encode64eurlsafe_decode64, e i due metodi legacy vanno in pensione (1.9.1 ha già spedito entrambi i cambiamenti a gennaio 2009). - 2015, Ruby 2.3:
urlsafe_encode64ottiene la parola chiavepadding:, che ti permette di emettere output senza riempimento per token e URL. - 2020, Ruby 3.0: base64 viene estratto dalla libreria standard nella sua gem, versione 0.1.0, sotto il repository
ruby/base64. Viene spedito come default gem, quindirequire "base64"continua a funzionare semplicemente. - 2023, Ruby 3.3: la versione 0.2.0 aggiunge
Base64::VERSIONe un set di documentazione molto più ricco. - 2024, Ruby 3.4: la gem viene riclassificata da default gem a bundled gem. La conseguenza pratica: nei progetti basati su Bundler con Ruby 3.4 e successivi, elenca
gem "base64"nel tuo Gemfile (o installala congem install base64). - 2025, Ruby 4.0: arriva la versione 0.3.0, che aggiunge le firme di tipo RBS tra le altre operazioni di manutenzione.
Attraverso tutto questo, un fatto non è mai cambiato: il modulo sono poche decine di righe di Ruby puro che stanno sopra i template pack e unpack del core. Nessuna estensione C, nessuna dipendenza, niente da compilare - e un conteggio di download nell'ordine delle centinaia di milioni su rubygems.org.
Curiosità Ruby per i curiosi
- Il lato decodifica del modulo sono due corpi di metodo di una riga,
str.unpack1("m")estr.unpack1("m0"), più la variante urlsafe, che è uno scambio di lettere e una sistemazione del riempimento sopra quella rigorosa. Puoi cancellare il require e scriverlo tu. - Il
Net::HTTPdi Ruby non usa nemmeno il moduloBase64per la Basic auth - chiama direttamente il templatepack:["user:pass"].pack("m0"). - I cookie firmati e cifrati di Rails sono stringhe Base64 sotto la superficie: il message codec di ActiveSupport sceglie
strict_encode64per i cookie normali eurlsafe_encode64conpadding: falseper gli ID firmati URL-safe. Probabilmente ne hai già decodificato uno senza saperlo. - Ogni classe digest ha un metodo
base64digest-Digest::SHA256.base64digest("hello")- un one-liner per i checksum che devono stare nel testo. - Il tag
!binarydi YAML è Base64. Fai il dump di una stringa BINARY con Psych e il formato fa la codifica in silenzio per te. decode64non si cura se le tue righe sono di 60, 64 o 76 caratteri, o una singola riga enorme. Il templatemsalta gli a capo, quindi input avvolto e non avvolto si decodificano in modo identico.
Continua così
Ora hai la cassetta completa per la decodifica: un lettore tollerante per i blob a forma di MIME, un guardiano rigoroso per tutto ciò che non è fidato, un diplomatico URL-safe per token e link, e il passaggio della codifica che trasforma i byte risultanti in un testo che Ruby ti lascerà usare. La direzione opposta - decidere quale dei tre encoder di Ruby dare in pasto ai tuoi byte, e controllare l'alfabeto, il riempimento e gli a capo - porta con sé il suo set di sorprese, a partire da un a capo finale che nessuno ha chiesto. Quella parte del lavoro è coperta in dettaglio nell'articolo sulla codifica Base64, linkato qui sotto.
Ultimo aggiornamento: 2026-09-08
Articolo correlato: Codifica Base64 in Ruby: una guida completa