Decodifica Base64 in C++ (Cpp): una guida completa
Hai in mano la stringa. Un lungo nastro di lettere e cifre, l'occasionale + o /, magari un = o due alla fine, e da qualche parte nel tuo ticket, contratto o colonna di database, la promessa che si tratta di Base64. Ora ti servono di nuovo i byte originali, in C++, e ti servono giusti. La home page di questo sito passa in rassegna il formato in dettaglio, quindi qui c'è solo la versione breve: quattro caratteri dell'alfabeto portano tre byte, una coda di uno o due = segna dove finiva il dato vero, e la forma codificata corre circa il 33 percento oltre l'originale. La decodifica è la direzione che riduce, quindi un decoder non avrà mai bisogno di più memoria del payload che già tiene in mano. È una proprietà davvero piacevole, e una delle gioie tranquille di lavorare in questa direzione.
La notizia più grossa è che il C++ stesso non decodifica un solo carattere per te. La libreria standard ha avuto trenta anni per crescere una funzione base64 e li ha spesi tutti su altre cose, quindi ogni programma C++ porta con sé un decoder scelto da una panchina di tre personalità molto diverse, più l'opzione di scrivere circa quaranta righe proprie. Una è un cavallo da lavoro che porta in spalla internet dagli anni Novanta, una è un tipo silenzioso che si ferma a metà frase e non ne dice una sola, e una è una pedante che lancia un'eccezione su un singolo spazio fuori posto. Appena sai cosa ciascuna perdona, cosa rifiuta e cosa fa in silenzio alle tue spalle, decodificare smette di essere una fonte di bug misteriosi. Apriamo qualche pacchetto.
La cassetta degli attrezzi: quattro decoder, quattro temperamenti
Ecco il panorama in un colpo d'occhio. Tutti e quattro gestiscono l'alfabeto standard; le differenze stanno nei bordi, ed è proprio ai bordi che vivono i bug.
| Decoder | Da dove viene | Modello di errore | Capriccio da ricordare |
|---|---|---|---|
| OpenSSL EVP | <openssl/evp.h>, link -lcrypto |
Restituisce -1 su input rotto |
La versione a colpo singolo riempie la coda di zeri |
| Boost.Beast | <boost/beast/core/detail/base64.hpp>, solo header |
Nessuno: si ferma e basta | Nessun canale di errore di nessun tipo |
| I iteratori di Boost.Serialization | <boost/archive/iterators/binary_from_base64.hpp>, solo header |
Lancia dataflow_exception |
Tratta = come un valore zero reale |
| Le tue quaranta righe | Non viene da nessuna parte: è tua | Scegli tu, fino alla posizione del byte | Ogni caso limite è tuo per sempre |
L'installazione è un nome di pacchetto per distribuzione. Per OpenSSL: libssl-dev su Debian e Ubuntu, openssl-devel su Fedora e RHEL, openssl su Arch, e brew install openssl su macOS. Per Boost, la cui release corrente è la 1.92.0 di agosto 2026 in un progetto che costruisce librerie dal 1998: libboost-dev o boost-devel. Entrambi i decoder Boost di seguito sono solo header, quindi non c'è proprio nulla da linkare. Se il tuo progetto è basato su CMake, l'intera configurazione fa tre righe:
find_package(OpenSSL REQUIRED)
find_package(Boost REQUIRED)
target_link_libraries(my_app PRIVATE OpenSSL::Crypto)
Una nota sulla versione prima del codice, perché cambia ciò che restituirà il tuo decoder. OpenSSL 3.5, rilasciata in aprile 2025 come linea di supporto a lungo termine, ha corretto un bug vero del decoder in streaming (ne parliamo tra un minuto), e la più recente release di funzionalità 4.0, di aprile 2026, ha ereditato la correzione. Se la tua compilazione fissa un vecchio 3.0 o 3.3, leggi il paragrafo sulle versioni nella sezione OpenSSL qui sotto prima di fidarti di una lunghezza di coda.
OpenSSL: il decoder che probabilmente linki già
Se il tuo programma C++ tocca TLS, hash o certificati, OpenSSL è già nel binario, e le sue routine base64 EVP sono i decoder più collaudati del settore. La funzione a colpo singolo è una chiamata sola:
int EVP_DecodeBlock(unsigned char *t, const unsigned char *f, int n);
Dagli un buffer di caratteri base64 e la sua lunghezza, e scrive i byte decodificati in t. Rimuove gli spazi iniziali, rimuove spazi finali, a capo e ritorni a carrello, e poi applica regole senza compromessi: nessun spazio interno, e la lunghezza dopo il taglio deve essere un multiplo di 4. Ogni quattro caratteri in input producono esattamente tre byte in output, ed ecco la parte che sorprende: i caratteri di padding vengono decodificati in sei bit a zero, e la man page nota con calma che è responsabilità del chiamante tenere conto del padding finale. In altre parole, la funzione fa l'aritmetica per te e poi aggiunge di nascosto fino a due byte bonus a zero alla fine. Il wrapper C++ idiomatico nasconde la matematica dei buffer dietro un std::string:
#include <cstddef>
#include <cstdio>
#include <string>
#include <vector>
#include <openssl/evp.h>
std::string openssl_decode(const std::string &b64) {
std::vector<unsigned char> out(b64.size() * 3 / 4 + 4);
int n = EVP_DecodeBlock(out.data(),
reinterpret_cast<const unsigned char *>(b64.data()),
static_cast<int>(b64.size()));
if (n < 0) return {};
size_t pads = 0;
for (size_t i = b64.size(); i > 0 && b64[i - 1] == '='; --i) ++pads;
return std::string(reinterpret_cast<const char *>(out.data()),
static_cast<size_t>(n) - pads);
}
int main() {
std::string one = openssl_decode("TQ==");
std::printf("%zu bytes: %02x\n", one.size(),
static_cast<unsigned char>(one[0]));
std::string word = openssl_decode("TWFuZQ==");
std::printf("%zu bytes: %s\n", word.size(), word.c_str());
}
Eseguilo e il riempimento a zero compare esattamente dove la documentazione lo aveva promesso: decodificando la stringa di quattro caratteri TQ== si ottengono tre byte, la lettera M più due zeri, prima che il wrapper li tagli via. Dammici TWFuZQ== e ottieni i quattro byte puliti di "Mane". Dammici un carattere fuori dall'alfabeto e ti torna la stringa vuota, perché la funzione ha risposto con -1. Nota cosa sta facendo in silenzio std::string in questo wrapper: tiene traccia della sua lunghezza e contiene volentieri byte a zero, quindi un JPEG decodificato può vivere nel tuo tipo testo e essere confrontato, trasformato in hash e passato in giro. In C avresti contato le benedizioni per una variabile di lunghezza; qui la stringa semplicemente funziona.
Per i dati che arrivano a pezzi, OpenSSL ti consegna un contesto in cui infili i frammenti e da cui tiri fuori i risultati, e le tre funzioni hanno un vocabolario compatto di valori di ritorno:
| Chiamata | Restituisce | Cosa significa |
|---|---|---|
EVP_DecodeUpdate |
-1 |
Carattere non valido, o un carattere di padding in mezzo ai dati |
EVP_DecodeUpdate |
1 |
Si aspetta ancora input |
EVP_DecodeUpdate |
0 |
Fine dati: l'ultimo gruppo portava padding, o è apparso il marcatore morbido di fine input |
EVP_DecodeFinal |
1 / -1 |
Flusso terminato in pulito / i caratteri residui non erano un multiplo di 4 |
Due comportamenti rendono il decoder in streaming il più indulgente della cassetta degli attrezzi. Salta spazi, tab, ritorni a carrello e a capo ovunque nel flusso, quindi un blocco email MIME con righe CRLF di 76 caratteri passa esattamente come una stringa su una riga, e riporta il conteggio vero dei byte: lo stesso TQ== che ha ingannato la funzione a colpo singolo qui ti dà esattamente un byte, senza aritmetica. Mastica l'input in frammenti di fino a 64 caratteri base64, lavorando da un buffer interno di 80 byte, e mette in buffer ciò che non rientra in un gruppo di quattro, ed è per questo che puoi darglielo a pezzi di dimensioni arbitrarie. Il wrapper:
#include <algorithm>
#include <cstdio>
#include <string>
#include <vector>
#include <openssl/evp.h>
std::string openssl_decode_stream(const std::string &b64) {
EVP_ENCODE_CTX *ctx = EVP_ENCODE_CTX_new();
EVP_DecodeInit(ctx);
std::string out;
std::vector<unsigned char> chunk(1024);
int outl = 0;
for (size_t pos = 0; pos < b64.size(); pos += chunk.size()) {
size_t take = std::min(chunk.size(), b64.size() - pos);
int ret = EVP_DecodeUpdate(ctx, chunk.data(), &outl,
reinterpret_cast<const unsigned char *>(b64.data()) + pos,
static_cast<int>(take));
if (ret < 0) {
EVP_ENCODE_CTX_free(ctx);
return {};
}
out.append(reinterpret_cast<const char *>(chunk.data()), outl);
if (ret == 0) break;
}
unsigned char tail[3];
int tail_l = 0;
int fin = EVP_DecodeFinal(ctx, tail, &tail_l);
EVP_ENCODE_CTX_free(ctx);
if (fin != 1) return {};
out.append(reinterpret_cast<const char *>(tail), tail_l);
return out;
}
E adesso la nota a piè di pagina sulle versioni della tabella della cassetta degli attrezzi, perché è esattamente il tipo di cosa che riapre un ticket "risolto": su ogni release OpenSSL prima di 3.5, il percorso in streaming aveva la stessa abitudine di riempire a zero del decoder a blocco. È stato segnalato nel febbraio 2025 come problema 26677; il commit della correzione è atterrato su master il 27 febbraio 2025, e la pull request associata è stata chiusa lo stesso giorno. La man page ufficiale ora lo registra nella sua sezione di storia: da OpenSSL 3.5 in poi, EVP_DecodeUpdate produce il numero di byte che la documentazione dichiarava sempre e non decodifica più il padding in bit a zero. Se la tua base di codice fissa un OpenSSL vecchio e le tue lunghezze di coda sembrano fuori di uno o due byte, questa è la prima cosa da controllare. E c'è un'eccentricità ereditata dall'era PEM: il trattino - non è un carattere dell'alfabeto nemmeno per errore - è un marcatore morbido di fine input. Se il tuo flusso ne contiene uno dopo un multiplo di 4 caratteri validi, il decoder restituisce 0 e ti chiede di fermarti, ed è per questo che una stringa base64url che ha davvero bisogno dei suoi caratteri - non si decodificherà semplicemente: prima trascodifica, nella sezione qui sotto, e il viaggiatore nel tempo sparisce.
Boost.Beast: il decoder veloce che non si lamenta mai
Se Boost è già nel progetto, la sua libreria HTTP spedisce un codec base64 all'improbabile indirizzo boost/beast/core/detail/base64.hpp. Lo spazio dei nomi detail:: è il modo di Boost per dire "questa è roba nostra", e i manutentori si sono rifiutati di promuoverlo a API pubblica. Tutti lo usano lo stesso, perché è piccolo, veloce e solo header: definisci BOOST_BEAST_HEADER_ONLY prima dell'include e non c'è nulla da linkare. È anche, tra l'altro, il codec che la stessa negoziazione WebSocket di Boost.Beast usa per il calcolo di Sec-WebSocket-Accept, quindi mastica traffico vero da anni.
La personalità della funzione di decodifica è la sorpresa. Prende il tuo buffer di output, l'input e la sua lunghezza, e restituisce una coppia: il numero di ottetti scritti e il numero di caratteri letti. Si ferma al primo = e al primo carattere non valido - e in entrambi i casi lo fa senza dirtelo. Nessun codice di errore, nessuna eccezione, nessuna flag di stato. Un payload corrotto, un payload a capo e una coda troncata producono tutti un risultato parziale di successo:
| Input | Byte decodificati | Caratteri letti | Perché si è fermato |
|---|---|---|---|
"TWFuZQ==" |
4 byte "Mane" |
6 | Fermo al primo = |
"TWF!ZQ==" |
2 byte "Ma" |
3 | Fermo su ! |
"TWFu\nZQ==" |
3 byte "Man" |
4 | Fermo su \n |
"TWFuZQ" |
4 byte "Mane" |
6 | La coda era troncata |
"TQ==" |
1 byte "M" |
2 | Padding gestito senza problemi |
Rileggi quella tabella una seconda volta, perché è l'intero modello di minaccia di un decoder che non si lamenta mai: ha fatto il suo meglio, si è fermato dove si è fermato, e spetta a te accorgertene. Il controllo ha una ruga: per un input con padding il conteggio dei "caratteri letti" si ferma al primo =, quindi riassunti i pad prima di confrontare, e richiedi anche che il totale sia un multiplo di quattro, perché è l'unica forma che ha un payload vero:
#define BOOST_BEAST_HEADER_ONLY
#include <boost/beast/core/detail/base64.hpp>
#include <cstddef>
#include <string>
namespace b64 = boost::beast::detail::base64;
std::string beast_decode(const std::string &in) {
std::string out;
out.resize(in.size() / 4 * 3 + 3); /* margine per le lunghezze dispari */
auto result = b64::decode(out.data(), in.data(), in.size());
out.resize(result.first);
size_t pads = 0;
for (size_t i = in.size(); i > 0 && in[i - 1] == '='; --i) ++pads;
if (result.second + pads != in.size() || in.size() % 4 != 0)
return {}; /* si è fermato presto, oppure la coda era impossibile */
return out;
}
Due dettagli da mettere da parte. Primo, l'helper decoded_size(n) a cui l'header ti indirizza è un limite superiore valido solo quando n è un multiplo di 4 - il commento della funzione stessa lo dice - ed è per questo che il wrapper qui sopra aggiunge un paio di byte di margine invece di fidarsene per lunghezze arbitrarie. Secondo, la provenienza: i file sorgente riportano un copyright 2016-2019 di Vinnie Falco, con un piè di pagina che attribuisce alcune parti a uno snippet 2004-2008 di Rene Nyffenegger. Quello snippet è la coppia base64 che viene copiata e incollata su tutto l'internet anglofono da vent'anni, e ora spedisce dentro Boost, nel tuo binario, facendo l'autenticazione HTTP Basic per tutta la rete.
Boost.Serialization: il decoder che fa eccezioni su uno spazio fuori posto
La libreria di serializzazione di Boost porta il base64 più vecchio dell'ecosistema C++: un insieme di adattatori di iteratori componibili del 2002, scritti da Robert Ramey, che trattano "sii generoso su ciò che accetti" come un'offesa personale. La direzione di decodifica vive in binary_from_base64.hpp (sì, il nome è dal punto di vista dell'output) e si accoppia a un trasformatore di larghezza che rimpacchetta i valori a sei bit in byte a otto bit:
#include <boost/archive/iterators/binary_from_base64.hpp>
#include <boost/archive/iterators/transform_width.hpp>
#include <cstddef>
#include <string>
namespace it = boost::archive::iterators;
std::string boost_decode(const std::string &in) {
using dec =
it::transform_width<it::binary_from_base64<const char *>, 8, 6>;
std::string out(dec(in.data()), dec(in.data() + in.size()));
size_t pads = 0;
for (size_t i = in.size(); i > 0 && in[i - 1] == '='; --i) ++pads;
out.resize(out.size() - pads);
return out;
}
L'iteratore interno converte ogni carattere base64 nel suo valore a sei bit, e quello esterno raggruppa di nuovo quei valori in byte. Il suo rigore è totale: qualsiasi carattere fuori dall'alfabeto - incluso un singolo spazio - fa lanciare all'iteratore boost::archive::iterators::dataflow_exception con il messaggio "attempt to decode a value not in base64 char set". Questo è il comportamento "rifiuta salvo diversa istruzione" che gli RFC hanno reso esplicito più tardi - implementato nel 2002, un anno intero prima che RFC 3548 codificasse la stessa regola. La conseguenza pratica è che un input avvolto in MIME deve essere liberato dei suoi a capo prima di toccare questo iteratore. Il secondo capriccio è più sottile: nella tabella di ricerca, il carattere di padding = non viene saltato - viene reso come il valore zero. Decodificare TWFuZQ== produce quindi sei byte - 4d 61 6e 65 00 00 - perché entrambi i caratteri di padding hanno contribuito dati veri (zero), e la riga resize(size - pads) nello snippet è portante, non decorativa. Decodifica TQ== e ottieni tre byte che si riducono alla singola lettera M, esattamente dove vuoi atterrare.
Quaranta righe che sono tue
Il Base64 è abbastanza piccolo da rendere un decoder corretto una cosa rispettabile da possedere, e in C++ il rendimento è migliore che in qualsiasi altro linguaggio: std::string rende piacevole la gestione dei buffer, e un decoder fatto a mano può fare qualcosa che nessuna delle versioni di libreria qui sopra fa, ovvero indicare il byte esatto che ha fatto male. Questa versione segue la lettura rigorosa di RFC 4648 - taglia le estremità, rifiuta gli spazi interni, rifiuta il padding in mezzo, applica le regole sulla lunghezza, e controlla persino i bit di padding che l'RFC dice un encoder conforme deve aver azzerato:
#include <cstddef>
#include <cstring>
#include <string>
std::string strict_decode(const std::string &in, size_t *error_pos = nullptr) {
static const char *table =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
auto fail = [&](size_t pos) {
if (error_pos) *error_pos = pos;
return std::string();
};
size_t start = 0;
size_t end = in.size();
while (start < end && (in[start] == ' ' || in[start] == '\t' ||
in[start] == '\r' || in[start] == '\n'))
++start;
while (end > start && (in[end - 1] == ' ' || in[end - 1] == '\t' ||
in[end - 1] == '\r' || in[end - 1] == '\n'))
--end;
size_t pads = 0;
while (end > start && in[end - 1] == '=') {
--end;
++pads;
}
size_t body = end - start;
if (pads > 2 || (pads == 1 && body % 4 != 3) ||
(pads == 2 && body % 4 != 2) || (pads == 0 && body % 4 == 1))
return fail(in.size());
if (body >= 2 && body % 4 != 0) {
int leftover = static_cast<int>((body % 4) * 6 % 8);
int last = static_cast<int>(std::strchr(table, in[end - 1]) - table);
if ((last & ((1 << leftover) - 1)) != 0)
return fail(end - 1); /* bit di padding non canonici */
}
int value = 0;
int bits = -8;
std::string out;
out.reserve(body / 4 * 3);
for (size_t i = start; i < end; ++i) {
const char *p = std::strchr(table, in[i]);
if (!p)
return fail(i);
value = (value << 6) + static_cast<int>(p - table);
bits += 6;
if (bits >= 0) {
out.push_back(static_cast<char>((value >> bits) & 0xFF));
bits -= 8;
}
}
return out;
}
Percorri ciò che applica. Gli spazi iniziali e finali vengono tagliati, perché un payload copiato da un'intestazione email ha grandi probabilità di arrivare vestito di spazi. Gli spazi interni vengono rifiutati, perché RFC 4648 dice che un decoder DEVE rifiutare i caratteri non alfabetici a meno che la specifica circostante non dica altrimenti, e a un confine di sicurezza vuoi la lettura rigorosa. Un carattere di padding in mezzo ai dati viene rifiutato, così come qualsiasi lunghezza che non possa corrispondere a un payload vero: un carattere in meno rispetto a un gruppo è impossibile, e un singolo pad è legale solo dietro tre caratteri di corpo. Il controllo canonico alla fine è quello che la maggior parte delle implementazioni salta: se l'ultimo gruppo aveva uno o due caratteri di padding, i bit bassi non usati dell'ultimo carattere dell'alfabeto devono essere zero, altrimenti gli stessi byte potrebbero essere scritti come due stringhe visibilmente diverse. Quella malleabilità è la ragione per cui il controllo esiste, e costa quattro righe. Infine, il decoder accetta input senza padding, che è esattamente ciò che sono i segmenti dei JWT. E in caso di fallimento ti consegna la posizione: dargli TWF!ZQ== e l'errore sta all'indice 3, sul punto esclamativo, che è la differenza tra un report di bug e una correzione.
Base64url: l'alfabeto che i token parlano
L'alfabeto standard ha due caratteri che non sopravvivono a un URL: + significa spazio in una stringa di query, e / significa directory in un percorso. RFC 4648 sezione 5 risolve il problema con due scambi di caratteri - + diventa - e / diventa _ - ed è schietto sul risultato: questa codifica "non dovrebbe essere considerata la stessa della codifica base64". È l'alfabeto dei JWT, delle code challenge OAuth PKCE, degli identificatori video di YouTube e della maggior parte dei token API, e di solito toglie anche il padding =, perché in un token la lunghezza è nota implicitamente e il padding sarebbe solo una percent-escape in attesa di succedere. Nessuno dei decoder C++ qui sopra lo parla nativamente - OpenSSL tratta persino - come quel marcatore morbido di fine input - quindi la soluzione è una piccola trascodifica prima di decodificare. È così corto da poterlo tenere in testa:
#include <string>
std::string url_to_standard(std::string in) {
for (char &c : in) {
if (c == '-') c = '+';
else if (c == '_') c = '/';
}
switch (in.size() % 4) {
case 2: in += "=="; break; /* ripristina il padding rimosso */
case 3: in += '='; break;
default: break;
}
return in;
}
Applicalo a un JWT vero e i primi due segmenti sono JSON puro in attesa di accadere. Un token di esempio classico si decodifica in un header di {"alg":"HS256","typ":"JWT"} e in un insieme di claims contenente un soggetto, un nome e un timestamp di emissione. Il terzo segmento si decodifica allo stesso modo e ti dà i byte grezzi della firma - non testo, e non prova di niente. Decodificare un token ti dice cosa afferma; verificare la firma ti dice se crederci, e quello è un lavoro di crittografia che nessuna libreria base64 farà per te. Su Windows la situazione è a senso unico nella stessa direzione: CryptBinaryToStringA ha una flag CRYPT_STRING_BASE64URI per il lato codifica, ma la direzione di decodifica non ha una flag URL-safe nemmeno per sbaglio, quindi la trascodifica qui sopra si guadagna il suo posto nella tua memoria muscolare su ogni piattaforma.
Dai byte di nuovo al testo
Chiedi a un decoder C++ "quale codifica caratteri ho appena decodificato?" e ottieni la risposta più onesta che il linguaggio abbia: nessuna. I decoder sono orientati ai byte da capo a fondo. Non vedono testo; vedono byte, e ti restituiscono esattamente i byte che erano stati impacchettati. Se l'originale era UTF-8, ora tieni UTF-8, e non serve nient'altro. La bella svolta C++ è che std::string stesso è un contenitore di byte con un membro di lunghezza, quindi la modalità di fallimento classica di C - una funzione di stringa che si ferma al primo NUL - si evapora quasi del tutto. Un file decodificato, un certificato decodificato, un'immagine decodificata: tutti possono vivere in una string, essere confrontati con ==, trasformati in hash e passati per valore, e i byte a zero dentro sono semplicemente byte. Solo non convertire in una stringa C e poi misurarla con strlen; usa size().
Per le codifiche legacy che ancora si nascondono in vecchi database, esportazioni e strumenti scritti a mano - ISO-8859-1, Windows-1252 e loro parenti - lo strumento standard è POSIX iconv, che spedisce con glibc. Prima decodifica in byte, poi converti quei byte in UTF-8 con il codec che corrisponde alla sorgente:
#include <iconv.h>
#include <cstddef>
#include <string>
#include <vector>
std::string to_utf8(const std::vector<unsigned char> &raw,
const char *source_charset) {
iconv_t cd = iconv_open("UTF-8", source_charset);
if (cd == (iconv_t)-1) return {};
char *inptr = reinterpret_cast<char *>(const_cast<unsigned char *>(raw.data()));
size_t inleft = raw.size();
std::vector<char> utf8buf(raw.size() * 4 + 8);
char *outptr = utf8buf.data();
size_t outleft = utf8buf.size();
if (iconv(cd, &inptr, &inleft, &outptr, &outleft) == (size_t)-1) {
iconv_close(cd);
return {};
}
iconv_close(cd);
return std::string(utf8buf.data(),
static_cast<size_t>(outptr - utf8buf.data()));
}
Il giro completo è senza perdita in entrambe le direzioni: impacchetta una stringa come ISO-8859-1, mettila in base64, spediscila, decodificala, convertila, e ottieni esattamente da dove avevi iniziato, con i caratteri accentati intatti. E i dati binari non hanno alcuna codifica caratteri - un PNG è un PNG che ci stai o no, ed è la risposta più liberante di tutto l'articolo.
Decodificare file
I file piccoli sono una danza di quattro passi: apri in binario, leggi in un vector, decodifica, riscrivi il risultato in binario. Modalità binaria, ogni volta, su ogni piattaforma - su Windows una lettura in modalità testo tradurrebbe le coppie CRLF in singoli a capo e cambierebbe silenziosamente i tuoi dati prima ancora che il decoder li veda:
#include <fstream>
#include <iterator>
#include <string>
#include <vector>
std::vector<unsigned char> read_file(const std::string &path) {
std::ifstream in(path, std::ios::binary);
return {std::istreambuf_iterator<char>(in),
std::istreambuf_iterator<char>()};
}
Nota le parentesi graffe nel codice qui sopra. Con una sola parentesi tonda, una riga della forma std::vector<unsigned char> bytes(istreambuf_iterator<char>(file), istreambuf_iterator<char>()) è l'infame "most vexing parse": il compilatore la legge come una dichiarazione di una funzione che restituisce un vector, ed è interamente corretto nel farlo. La forma con inizializzazione tra graffe qui sopra aggira la grammatica per intero. Una volta che i byte sono in mano, passali attraverso qualsiasi decoder di questo articolo e scrivi il risultato con std::ofstream in std::ios::binary, usando write(data.data(), data.size()) invece dell'operatore di stream, in modo che eventuali byte a zero incorporati sopravvivano al viaggio sul disco. Un file .b64 e il suo gemello decodificato differiscono allora di esattamente la tassa del 33 percento che hai pagato in entrata, ed è un momento di checksum soddisfacente.
File grandi: streaming in entrambe le direzioni
Per i file troppo grandi da tenere in memoria, il decoder in streaming della sezione OpenSSL fa tutto il lavoro: leggi un frammento, passalo attraverso il contesto, scrivi ciò che esce, ripeti. In un dato momento in RAM c'è solo un piccolo buffer, quindi un file base64 da 10 GB si decodifica con lo stesso codice di uno da 10 KB, e l'a capo in stile MIME non richiede alcuna pre-elaborazione lungo il percorso, perché il decoder in streaming si scrolla gli a capo di dosso:
#include <fstream>
#include <string>
#include <vector>
#include <openssl/evp.h>
bool decode_stream_to_file(const std::string &in_path,
const std::string &out_path) {
std::ifstream in(in_path, std::ios::binary);
std::ofstream out(out_path, std::ios::binary);
if (!in || !out) return false;
EVP_ENCODE_CTX *ctx = EVP_ENCODE_CTX_new();
EVP_DecodeInit(ctx);
std::string chunk(65536, '\0');
std::vector<unsigned char> decoded(49152);
bool ok = true;
for (;;) {
std::streamsize got = in.read(chunk.data(), chunk.size()).gcount();
if (got < 0) { ok = false; break; }
if (got == 0) break;
int outl = 0;
int ret = EVP_DecodeUpdate(ctx, decoded.data(), &outl,
reinterpret_cast<const unsigned char *>(chunk.data()),
static_cast<int>(got));
if (ret < 0) { ok = false; break; }
out.write(reinterpret_cast<const char *>(decoded.data()), outl);
if (ret == 0) break;
}
unsigned char tail[3];
int tail_l = 0;
if (ok && EVP_DecodeFinal(ctx, tail, &tail_l) != 1)
ok = false;
if (ok)
out.write(reinterpret_cast<const char *>(tail), tail_l);
EVP_ENCODE_CTX_free(ctx);
return ok;
}
Le dimensioni dei buffer non sono arbitrarie: i parametri di lunghezza delle funzioni EVP sono int, quindi una singola chiamata è sicura fino a 2 GB, e i numeri qui sopra tengono ogni frammento a 64 KB di input con un buffer di output da 48 KB, che è esattamente 3 su 4. Quel soffitto di int è l'intera ragione per cui il percorso in streaming esiste, e vale la pena conoscerlo come fatto duro invece di scoprirlo come bug di piattaforma. Se l'input dovesse risultare corrotto, la funzione restituisce false al primo frammento che non si riesce a decodificare, e il file di output tiene ciò che era valido prima - il che, a seconda della tua pipeline, potrebbe essere esattamente il risultato parziale che volevi.
HTTP, API e i campi JSON che nascondono byte
Il Base64 compare in HTTP in due forme. La prima sono i dati: una risposta JSON con un campo "certificate" o "avatar" pieno di base64, un endpoint di upload che accetta i byte in una colonna sicura per il testo, un endpoint di download che ti consegna un file .b64. Lo schema è sempre lo stesso - parsa il JSON, tira fuori la stringa, decodificala, tratta il risultato come byte - e il lato decodifica di questo articolo è l'intera implementazione. La seconda forma sono le credenziali: l'header Authorization: Basic è il base64 di user:password, ed è l'unico caso d'uso base64 dello standard da trenta anni. Parserlo è in due passi, e il primo è quello in cui la gente arriva a prendere una stringa C terminata da NUL in mezzo a dati vicini al binario e si chiede perché:
#include <optional>
#include <string>
/* strict_decode dalla sezione "Quaranta righe che sono tue" */
std::optional<std::pair<std::string, std::string>> parse_basic_auth(
const std::string &b64) {
std::string raw = strict_decode(b64);
size_t colon = raw.find(':');
if (colon == std::string::npos)
return std::nullopt;
return std::make_pair(raw.substr(0, colon), raw.substr(colon + 1));
}
Passagli il valore dell'header dopo il prefisso Basic e ti consegna utente e password come stringhe vere e proprie con lunghezza tracciata, oppure niente del tutto se il payload non è una coppia user:pass. La nota di sicurezza sta qui anche se non è un argomento C++: l'autenticazione Basic è offuscamento, non protezione. L'header viaggia in chiaro per chiunque possa leggere la rete, quindi è accettabile solo dietro TLS, e anche così è la scelta per chiamate macchina-a-macchina, non per le persone.
JWT: leggere ciò che un token afferma
Un JSON Web Token è tre segmenti base64url incollati con dei punti: header, claims, firma. I primi due sono oggetti JSON; il terzo è una firma crittografica sulla stringa header.claims, calcolata con l'algoritmo indicato nell'header. Il C++ non ha un tipo JWT integrato, ma leggere un token non richiede più della trascodifica della sezione base64url e di un decoder, perché la parte interessante è la lettura:
#include <cstddef>
#include <cstdio>
#include <string>
/* url_to_standard e strict_decode dalle sezioni precedenti */
int main() {
const std::string token =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
"eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ."
"SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
size_t dot1 = token.find('.');
size_t dot2 = token.find('.', dot1 + 1);
std::string header = strict_decode(
url_to_standard(token.substr(0, dot1)));
std::string claims = strict_decode(
url_to_standard(token.substr(dot1 + 1, dot2 - dot1 - 1)));
std::printf("header: %s\n", header.c_str());
std::printf("claims: %s\n", claims.c_str());
}
L'header torna come {"alg":"HS256","typ":"JWT"} e i claims come {"sub":"1234567890","name":"John Doe","iat":1516239022} - il soggetto, il nome e un timestamp di emissione. Questo è l'intero lato lettura, ed è genuinamente utile: registrare ciò che un token afferma, debuggare un 401 guardando il campo di scadenza, o decidere a quali claims credere sono tutti a una decodifica di distanza. Ciò che non è, è verifica. Il segmento di firma è anch'esso base64url, e decodificarlo ti dà 32 o 64 byte grezzi che da soli non provano niente; la firma ha senso solo quando ricalcoli l'hash di header.claims con il segreto condiviso o la chiave pubblica e confronti. Tratta un JWT decodificato come tratti una lettera: dice quello che dice, e verificare il sigillo è un lavoro a parte, crittografico.
Data URI: file che si sono incollati da soli in una pagina
Una data URI è un URL il cui payload è proprio lì nell'indirizzo: data: seguito da un tipo media facoltativo, un marcatore ;base64 facoltativo, una virgola, e poi i dati stessi - l'intero schema di RFC 2397. I browser li usano per incorporare immagini, font e piccoli script direttamente in HTML e CSS senza richieste extra, e se vedi una pagina che continua a funzionare con la rete disattivata, una data URI è una forte sospettata. Dal lato C++, il lavoro di decodifica è dividere la URI e poi far passare il payload attraverso il tuo decoder abituale, perché quando il marcatore ;base64 è presente il payload è base64 standard puro - di solito con padding, di solito su una riga:
#include <cstddef>
#include <string>
std::string data_uri_payload(const std::string &uri, bool *is_base64) {
const std::string prefix = "data:";
if (uri.rfind(prefix, 0) != 0)
return {};
size_t comma = uri.find(',');
if (comma == std::string::npos)
return {};
std::string meta = uri.substr(prefix.size(), comma - prefix.size());
*is_base64 = meta.size() >= 7 &&
meta.compare(meta.size() - 7, 7, ";base64") == 0;
return uri.substr(comma + 1);
}
Chiamala con data:image/png;base64,iVBORw0KGgo=... e ti consegna il payload più la flag che ti dice quale percorso di decodifica prendere. Due insidie. Prima, quando il marcatore è assente il payload è testo URL-encodificato, non base64, quindi la flag non è una formalità - una URI che sembra base64 ma è stata generata come testo percent-encodificato si decodificherà in spazzatura. Secondo, alcuni generatori avvolgono le data URI lunghe con a capo come fa il MIME; il tuo decoder rigoroso li rifiuterà, quindi toglie gli a capo prima di decodificare se la sorgente non è sotto il tuo controllo. Decodificare il payload di una URI data:image/png ti dà i byte esatti del PNG, header compreso, ed è la soddisfazione silenziosa dell'esercizio intero.
Email, MIME e l'abitudine dei 76 caratteri
L'email è la ragione per cui il base64 ha imparato a avvolgere le righe. SMTP, nella sua forma originale, è stato costruito per trasportare ASCII a sette bit, quindi qualunque cosa binaria doveva essere riscritta come testo stampabile prima di poter viaggiare. Privacy-Enhanced Mail l'ha fatto nel 1987 con righe da 64 caratteri, e MIME, quando ha standardizzato la codifica per l'email nel 1993, ha rilassato il limite a 76 caratteri e ha aggiunto la regola per cui un decoder conforme deve semplicemente ignorare i a capo. L'abitudine è sopravvissuta: un allegato email è ancora base64 oggi, avvolto a 76, e l'aritmetica esatta arriva a 4/3 per 78/76 - circa il 137 percento della dimensione originale, più qualche centinaio di byte di header. Il tuo decoder C++ la riduce di nuovo fino al 100 percento, ed è questo il punto dell'intero formato.
La ruga in C++ è che i decoder di questo articolo non sono d'accordo sui a capo, e ognuno ha una ragione. Il decoder in streaming di OpenSSL li salta ovunque nel flusso, che è esattamente la regola di MIME. La funzione a colpo singolo di OpenSSL rifiuta qualunque spazio dentro il payload. Boost.Beast si ferma al primo a capo senza dirlo. Gli iteratori di Boost fanno eccezione su un singolo spazio. Quindi quando un payload viene dall'email, la tua prima decisione è quale decoder usare, oppure togli tu i a capo - un passaggio erase-remove di una riga su \r e \n - e lasci che qualsiasi decoder tu preferisca faccia il lavoro vero. Togliere tutto in anticipo è la scelta noiosa e affidabile, ed è quella che mantiene la scelta del tuo decoder indipendente dalla storia del tuo payload.
Database, file di configurazione e variabili d'ambiente
La terza casa del base64 è il livello di archiviazione: una colonna in un database legacy la cui documentazione dice "base64" e nient'altro, un blob di configurazione in un file JSON, un payload in una variabile d'ambiente che un servizio ha messo in base64 perché potesse sopravvivere a una shell. Lo schema di decodifica è lo stesso di ovunque - leggi la stringa, decodifica, tratta come byte - ma l'input senza etichetta merita un paragrafo speciale, perché a volte non sai davvero quale alfabeto sia stato usato. Non puoi saperlo, ma puoi testare, perché quattro caratteri fanno gran parte del lavoro:
- Contiene
+o/- solo l'alfabeto standard può essere giusto. - Contiene
-o_- solo l'alfabeto URL-safe può essere giusto. - Nessuno dei due, ma finisce con
=- standard con padding, o una stringa URL-safe con padding il cui payload non è mai finito per avere bisogno dei due caratteri scambiati. - Nessuno dei due, senza padding - può essere l'uno o l'altro; la forma URL-safe grezza è la comune sul web, quindi per dati nati in un URL o in un token, prova prima quello.
Le stringhe che non usano nessuno dei quattro caratteri distintivi si decodificano identicamente sotto entrambi gli alfabeti, quindi per loro l'ordine in cui le provi dipende da dove vengono i dati: le cose nate nell'email vogliono l'alfabeto standard, le cose nate in un URL vogliono quello URL. E ricordati di provare la lettura con padding e quella senza padding della stessa stringa - un = mancante è la differenza tra "rifiutato" e "risolto", ed è per questo che il decoder rigoroso qui sopra accetta entrambe.
La riga di comando ha due base64
Per lavori una tantum, una macchina Linux di solito ha due decoder base64, e si comportano diversamente proprio nel modo che morde la gente. Il primo è base64 di GNU coreutils (alcune distribuzioni più nuove spediscono la reimplementazione uutils al posto suo, e entrambi parlano le stesse flag - controlla con base64 --version). È conforme a RFC 4648, avvolge a 76 caratteri in codifica (con -w 0 lo spegni), e in decodifica accetta volentieri a capo ovunque; la sua flag -i rende esplicita la tolleranza verso la spazzatura invece che accidentale. Il secondo è quello di OpenSSL, ed ecco la svolta: openssl base64 non è affatto un'app sua. Dalla serie 1.1.0 (2016), il programma enc controlla il proprio nome di invocazione, e se è stato chiamato "base64" si mette da solo in modalità base64 - un confronto di stringhe su argv[0], che è il modo C di spedire un alias. Senza -A, si aspetta un a capo da qualche parte nei primi 1024 byte di input, quindi una stringa lunga su una riga torna vuota, con codice di uscita 0. Con -A legge una riga, e l'elenco di bug documentati per il comando enc è un museo di due voci: l'opzione -A non funziona correttamente con i file grandi, e senza -A, se i primi 1024 byte non contengono un a capo, le prime due righe di input vengono ignorate. In una pipeline, un file vuoto silenzioso sembra esattamente una decodifica riuscita di un payload vuoto.
# i one-liner onesti
base64 -d < payload.b64 > payload.bin
openssl base64 -d -A < payload.b64 > payload.bin
Nessuno dei due parla base64url nativamente, che è un'altra ragione per cui lo snippet di trascodifica deve stare nella tua memoria muscolare. Per qualsiasi cosa conti, decodifica nel tuo programma, dove gli errori tornano come numeri che puoi testare e il codice di uscita di uno strumento silenzioso non è il tuo unico segnale.
Insidie che mordono specificamente il C++
- Il riempimento a zero.
EVP_DecodeBlockrestituisce tre byte perTQ==: la lettera M più due zeri. Ripristina la lunghezza vera dal padding, oppure usa l'API in streaming, che è onesta sul conteggio. - Il capriccio in streaming pre-3.5. Sulle release OpenSSL precedenti a 3.5.0 (aprile 2025),
EVP_DecodeUpdateaveva la stessa abitudine di riempire a zero. Codice scritto contro un pin 3.0 o 3.3 potrebbe mentirti sulle lunghezze di coda; la correzione è registrata nella sezione di storia della man page. - La fermata silenziosa. La
decodedi Boost.Beast non ha un canale di errore: si ferma a qualsiasi carattere non valido, a qualsiasi a capo e a qualsiasi lunghezza di coda impossibile, e restituisce un risultato parziale senza battere ciglio. Controlla checonsumed + pads == input.size()e che il totale sia un multiplo di 4, altrimenti stai decodificando ciò che ha deciso di decodificare. - La trappola decoded_size.
b64::decoded_size(n)dà per scontato chensia divisibile per 4. Due caratteri di input possono dare un byte, mentredecoded_size(2)dice zero - aggiungi un margine per le lunghezze dispari. - I pad a byte zero. Gli iteratori di Boost decodificano
=come il valore zero, quindiTWFuZQ==diventa sei byte inclusi due zeri finali. Sottrai il numero di pad, oppure goditi i tuoi fantasmi. - Spazi bianchi, in quattro modi. Lo streaming di OpenSSL li salta, il colpo singolo di OpenSSL li rifiuta internamente, gli iteratori di archive fanno eccezione su di essi, e Beast si ferma su di essi. Le stringhe incollate amano portare spazi bianchi, e ogni decoder ha la sua opinione in proposito.
- Indicizzazione con char firmato. Se costruisci la tua tabella di decodifica indicizzata per carattere, indicizza con
unsigned char. Su piattaforme dovecharè firmato, un byte sopra 127 diventa un indice negativo, che è comportamento indefinito con il camice da laboratorio. - Il segno meno è un viaggiatore nel tempo. In OpenSSL,
-è un marcatore morbido di fine input dell'era PEM, non un carattere dell'alfabeto. Trascodifica base64url prima di decodificare. - int, non size_t. I parametri di lunghezza EVP sono
int. Sopra i 2 GB, solo il percorso in streaming a frammenti è sicuro, ed è per questo che esiste. - Modalità testo su Windows. Aprire un file per testo traduce CRLF in LF e corrompe il tuo input prima della decodifica.
std::ios::binary, ogni volta, su ogni piattaforma. - Il most vexing parse.
std::vector<char> v(istreambuf_iterator<char>(f), istreambuf_iterator<char>())è una dichiarazione di funzione. Usa l'inizializzazione con graffe o una coppia di puntatori. - Bit di padding non canonici. Un decoder tollerante può accettare stringhe i cui bit di padding non usati sono non nulli, quindi due stringhe visibilmente diverse si decodificano negli stessi byte (malleabilità del base64). Ai confini di sicurezza, rifiuta ciò di cui non hai bisogno - RFC 4648 dice che i decoder possono fare esattamente quello.
- La riga di comando fallisce in silenzio.
openssl base64 -dsenza-Ainghiotte l'input su una riga (output vuoto, uscita 0); i bug documentati coprono file grandi e input senza a capo in entrambe le direzioni. Controlla il tuo output nelle pipeline. - strlen sul binario.
std::stringtiene felici i byte a zero, ma nel momento in cui consegni una stringa C a un'API legacy,strlensi ferma al primo NUL. Passa lunghezza e puntatore, mai un puntatore nudo.
Una breve storia del Base64 in C++
Il formato è più vecchio dell'era moderna del linguaggio. Il primo uso standardizzato della codifica oggi chiamata base64 MIME è stato il protocollo Privacy-Enhanced Mail, proposto nel 1987 con righe da 64 caratteri e un controllo di integrità del messaggio RSA-MD2/MD5 incollato alla fine; il nome "base64" in sé è arrivato solo nel 1993, quando gli standard MIME gliel'hanno dato. Il C++ è entrato in scena come C++98 nel 1998 - cinque anni dopo il MIME - e il primo codice base64 che gli sviluppatori del linguaggio hanno preso in mano è stata la coppia C di Rene Nyffenegger (2004-2008), che una domanda su Stack Overflow del 4 dicembre 2008 ha diffuso per tutto il web. La parte più bella di quella storia è chi non è comparso: l'autore non ha mai postato lui stesso una risposta, ma il suo snippet è diventato la canzone popolare che tutti hanno copiato. Una risposta nel filo di discussione ha ristampato la sua implementazione completa dal suo stesso sito web, per il caso in cui il sito fosse andato giù.
Poi l'ecosistema ha fatto ciò che gli ecosistemi fanno. Nel 2002, il Boost.Serialization di Robert Ramey ha spedito gli adattatori di iteratori - il base64 più vecchio della cassetta degli attrezzi C++, rigoroso al punto di lanciare un'eccezione su un singolo spazio, un anno prima che RFC 3548 codificasse la regola che già applicava. Nel 2017, Boost 1.66 ha portato Beast, e con esso il codec solo header che spedisce ancora oggi con l'attribuzione a Nyffenegger nel piè di pagina. Intanto lo standard stesso è andato C++11, C++14, C++17, C++20 e C++23 (pubblicato nel 2024), e ognuno di essi, senza eccezioni, ha guardato l'alfabeto a 64 caratteri e ha proseguito. Il C++26 aggiunge un nuovo header <text_encoding> per il lavoro sui codec di testo, approvato già nel 2022; il suo contenuto tecnico è stato completato al meeting ISO di marzo 2026 a Londra, dove il comitato ha votato 114-12-3 per mandarlo alla pubblicazione, e i meeting successivi del comitato nel 2026 - incluso quello di Búzios, Brasile, dal 16 al 21 novembre - sono dedicati ad aprire la prossima bozza di lavoro, il C++29, anziché votare questo. Il Base64 non c'è mai stato nella bozza. Sette standard, tre decenni, un header per la codifica di testo - e il comitato ha ormai avuto ogni possibile scusa per aggiungere il base64 e ha rinunciato a tutte. La storia pratica del base64 in C++ è, e resta, la storia delle sue librerie: le routine EVP di OpenSSL, due varianti Boost, una chiamata API di Windows, e uno snippet di quaranta righe che è tuo.
Fatti divertenti, edizione C++
- La stessa coppia di funzioni compare nelle risposte a una domanda su Stack Overflow del 2008, nel sorgente di Boost.Beast con un piè di pagina di attribuzione, e nei file di intestazione di innumerevoli basi di codice private. Chiedi a uno sviluppatore C++ da dove viene il suo base64 e la risposta più onesta è "non lo so, e nemmeno l'internet".
- Gli iteratori di archive di Boost sono il base64 più vecchio di questo articolo, copyright 2002 - lo stesso anno in cui è uscito l'SDK di .NET Framework 1.0. Lanciano un'eccezione su un singolo spazio, il che significa che applicavano la regola "rifiuta i caratteri non alfabetici" prima che gli RFC ci arrivassero: RFC 3548 l'ha codificata nel 2003, e RFC 4648 l'ha ripetuta nel 2006.
- Il decoder in streaming di OpenSSL lavora da un buffer interno di 80 byte, ma svuota ogni 64 caratteri base64, la stessa larghezza di riga che l'armatura PEM usa dal 1987. Quel silenzioso 64 è uno degli ultimi posti dove il vecchio formato sta ancora facendo lavoro portante nel 2026.
- Il base64 con padding più piccolo possibile fa quattro caratteri,
TQ==: un byte vestito da due caratteri. Quello senza padding più piccolo fa due caratteri,TQ. Quale dei due ti capita di decodificare dipende interamente da chi l'ha codificato, e quella persona non stava pensando a te. - La matematica di MIME è esatta: 4/3 per 78/76, ed è per questo che un allegato email arriva a circa il 137 percento della sua dimensione originale (più qualche centinaio di byte di header sopra). Il tuo decoder C++ lo riduce di nuovo al 100 percento, ed è la gioia tranquilla dell'intero esercizio.
- Su un libstdc++ o MSVC tipico,
std::stringporta i payload piccoli in un buffer di stack tramite l'ottimizzazione delle stringhe corte invece di allocare. Un input di 9 byte si decodifica in 6 byte e non tocca mai l'heap. La forma base64 del tuo minuscolo blob di configurazione può letteralmente vivere in uno stack frame, ed è il genere di pranzo gratis che la libreria standard non pubblicizza. - Il comando
openssl base64a cui potresti arrivare in una shell non è affatto un comando. È il programmaencche controlla il proprio nome inargv[0]e cambia personalità. Un alias per confronto di stringhe, che è il modo C++ di fare le cose, in C. - Gli identificatori video di YouTube sono base64url: undici caratteri, senza padding, nessun
+o/da nessuna parte vicino a un URL. Il formato di codifica più guardato al pianeta gira sulla variante "sicura per URL e nomi di file" che RFC 4648 ha aggiunto in una sezione che sta in una pagina.
Quando invece devi impacchettare
Tutto ciò che hai appena decodificato è stato impacchettato dalla stessa cassetta degli attrezzi dall'altra parte: EVP_EncodeBlock per i colpi singoli, EVP_EncodeUpdate più EVP_EncodeFinal per i flussi (ed è da lì che vengono quelle righe da 64 caratteri), la stessa aritmetica di buffer al contrario, e la stessa tassa del 33 percento che la decodifica restituisce in silenzio. La storia completa dell'impacchettamento - la matematica delle dimensioni voce per voce, gli encoder che terminano in NUL il loro output, l'iteratore di Boost che non ha mai incontrato un carattere di padding, base64url, a capo in stile MIME, file, e l'API di Windows con la sua abitudine CRLF - vive nella guida C++ alla codifica sul sito gemello. Vai a leggerla, poi torna e apri qualcosa di grosso. Questa è l'intera partita: nessuna libreria standard, tre fornitori fidati con tre temperamenti diversi, un decoder che indica il byte esatto che ha fatto male, una correzione di bug del 2025 che ha cambiato la coda in streaming, e una triade riempita di zeri da ricordare per sempre. Buon disimballaggio.
Ultimo aggiornamento: 2026-09-08
Articolo correlato: Codifica Base64 in C++ (Cpp): una guida completa