Base64-decodering in C++ (Cpp): een complete gids
Je hebt de string. Een lang lint van letters en cijfers, af en toe een + of /, misschien een of twee = aan het einde, en ergens in je ticket, contract of databasekolom de belofte dat het Base64 is. Nu heb je de oorspronkelijke bytes terug nodig, in C++, en ze moeten gewoon kloppen. De startpagina van deze site behandelt het formaat uitgebreid, dus hier is alleen de korte versie: vier alfabettekens dragen drie bytes, een staart van één of twee =-tekens markeert waar de echte data ophield, en de geëncodeerde vorm loopt ongeveer 33 procent groter dan het origineel. Decoderen is de verkleinende richting, dus een decoder heeft nooit meer geheugen nodig dan de payload die hij al vasthoudt. Dat is een oprecht prettige eigenschap, en een van de stille pretjes van het werk in deze richting.
Het grotere kopje is dat C++ zelf geen enkel karakter voor je decodeert. De standaardlibrary had dertig jaar de tijd om een base64-functie te kweken en heeft ze allemaal aan andere dingen besteed, dus elk C++-programma pikt zijn decoder uit een rij van drie heel verschillende persoonlijkheden, plus de optie om zelf zo'n veertig regels te schrijven. De ene is een werkpaard dat het internet sinds de jaren 90 draagt, de andere is een stil type dat halverwege stopt en er nooit een woord over zegt, en de derde is een perfectionist die een uitzondering gooit om een enkele verdwaalde spatie. Zodra je weet wat elke decoder vergeeft, wat hij weigert en wat hij in stilte achter je rug om doet, stopt decoderen met een bron van mysterieuze bugs zijn. Laten we maar een paar pakketten openen.
De gereedschapskist: vier decoders, vier temperamenten
Hier is het landschap in één oogopslag. Alle vier dekken het standaardalfabet; de verschillen zitten in de randen, en de randen zijn waar bugs vandaan komen.
| Decoder | Waar het vandaan komt | Foutmodel | De quirk om te onthouden |
|---|---|---|---|
| OpenSSL EVP | <openssl/evp.h>, link -lcrypto |
Geeft -1 terug bij kapotte input |
De one-shot versie vult de staart op met nullen |
| Boost.Beast | <boost/beast/core/detail/base64.hpp>, header-only |
Geen: stopt gewoon | Geen foutkanaal van enige soort |
| Boost.Serialization-iterators | <boost/archive/iterators/binary_from_base64.hpp>, header-only |
Gooit dataflow_exception |
Behandelt = als een echte nulwaarde |
| Je eigen veertig regels | Nergens: die zijn van jou | Jouw keuze, tot op de bytepositie | Jij bezit elke edge case voor altijd |
Installatie is één pakketnaam per distro. Voor OpenSSL: libssl-dev op Debian en Ubuntu, openssl-devel op Fedora en RHEL, openssl op Arch, en brew install openssl op macOS. Voor Boost, waarvan de huidige release 1.92.0 uit augustus 2026 is in een project dat sinds 1998 libraries bouwt: libboost-dev of boost-devel. Beide Boost-decoders hieronder zijn header-only, dus er is helemaal niets te linken. Als je project CMake-gebaseerd is, is de hele setup drie regels:
find_package(OpenSSL REQUIRED)
find_package(Boost REQUIRED)
target_link_libraries(my_app PRIVATE OpenSSL::Crypto)
Eén versienoot vóór de code, want het verandert wat je decoder teruggeeft. OpenSSL 3.5, released in april 2025 als long-term-support-lijn, verholp een echte bug in de streamende decoder (daarover straks meer), en de nieuwere 4.0-functierelease van april 2026 erfde het herstel. Als je build een oude 3.0 of 3.3 vastzet, lees dan de versieparagraaf in het OpenSSL-hoofdstuk hieronder voordat je een staartlengte vertrouwt.
OpenSSL: de decoder die je waarschijnlijk al linkt
Als je C++-programma met TLS, hashing of certificaten te maken heeft, zit OpenSSL al in de binary, en de EVP-base64-routines ervan zijn de meest beproefde decoders van het vak. De one-shot functie is één enkele aanroep:
int EVP_DecodeBlock(unsigned char *t, const unsigned char *f, int n);
Geef hem een buffer met base64-karakters en de lengte, en hij schrijft de gedecodeerde bytes naar t. Hij knipt witruimte aan het begin weg, knipt witruimte, nieuwe regels en carriage returns aan het einde weg, en daarna hanteert hij regels zonder compromissen: geen whitespace in het midden, en de geknipte lengte moet een veelvoud van 4 zijn. Elke vier inputkarakters produceren precies drie outputbytes, en dit is het deel dat mensen verast: pad-karakters worden gedecodeerd naar zes nulbits, en de man page merkt rustig op dat de aanroeper zelf verantwoordelijk is voor het rekening houden met de afsluitende padding. Met andere woorden: de functie doet de rekensom voor je en voegt dan in stilte tot twee bonusnulbytes toe aan het einde. De idiomatische C++-wrapper verstopt de bufferrekening achter een 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());
}
Voer het uit en de nulopvulling verschijnt precies waar de documentatie belooft: als je de string van vier karakters TQ== decodeert, krijg je drie bytes, de letter M plus twee nullen, voordat de wrapper ze wegknipt. Geef hem TWFuZQ== en je krijgt de nette vier bytes van "Mane". Geef hem een karakter buiten het alfabet en je krijgt een lege string terug, omdat de functie antwoordde met -1. Merk op wat std::string in deze wrapper in stilte doet: hij bijhoudt zijn eigen lengte en bevat met plezier nulbytes, dus een gedecodeerde JPEG kan in je teksttype wonen en vergeleken, gehasht en rondgereikt worden. In C zou je dankbaar zijn voor een lengtevariabele; hier werkt de string gewoon.
Voor data die in stukken aankomt, geeft OpenSSL je een context waarin je chunks voedt en resultaten uithaalt, en hebben de drie functies een compacte retourtaal:
| Aanroep | Geeft terug | Wat het betekent |
|---|---|---|
EVP_DecodeUpdate |
-1 |
Ongeldig karakter, of een pad-karakter in het midden van de data |
EVP_DecodeUpdate |
1 |
Er wordt meer input verwacht |
EVP_DecodeUpdate |
0 |
Einde van de data: de laatste groep droeg padding, of de zachte einde-van-input-marker verscheen |
EVP_DecodeFinal |
1 / -1 |
De stream eindigde schoon / de resterende karakters waren geen veelvoud van 4 |
Twee gedragingen maken de streamende decoder de meest vergevingsgezinde van de gereedschapskist. Hij overslaat spaties, tabs, carriage returns en nieuwe regels overal in de stream, dus een MIME-e-mailblok met CRLF-regels van 76 tekens stroomt door alsof het een eenregelige string is, en hij rapporteert de echte bytelengte: hetzelfde TQ== dat de one-shot functie in de war bracht, levert hier precies één byte op, zonder rekenwerk. Hij kauwt input in chunks van maximaal 64 base64-karakters, werkt met een interne buffer van 80 bytes, en buffert wat niet in een groep van vier past, waardoor je hem in willekeurige stukgroottes kunt voeren. De 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;
}
En nu de versievoetnoot uit de gereedschapskist-tabel, want dit is precies het soort ding dat een "opgelost" ticket weer openzet: in elke OpenSSL-release vóór 3.5 had het streamende pad dezelfde nulopvulgewoonte als de blokdecoder. Het werd in februari 2025 gemeld als issue 26677; de herstelcommit landde op master op 27 februari 2025, en de bijbehorende pull request werd dezelfde dag gesloten. De officiële man page vermeldt het nu in zijn geschiedenissectie: vanaf OpenSSL 3.5 produceert EVP_DecodeUpdate het aantal bytes dat de documentatie altijd beweerde, en decodeert padding niet meer naar nulbits. Als je codebase een oude OpenSSL vastzet en je staartlengtes een of twee bytes af lijken, is dit het eerste om te checken. En er is één excentriciteit die is overgenomen van het PEM-tijdperk: het koppelteken - is helemaal geen alfabetkarakter - het is een zachte einde-van-input-marker. Als je stream er een bevat na een veelvoud van 4 geldige karakters, geeft de decoder 0 terug en vraagt hij dat je stopt, waardoor een base64url-string die écht zijn --karakters nodig heeft niet zomaar decodeert - transcodeer eerst, in het hoofdstuk hieronder, en de tijdsreiziger verdwijnt.
Boost.Beast: de snelle decoder die nooit klacht
Als Boost al in het project zit, levert de HTTP-library er een base64-codec mee op het onwaarschijnlijke adres boost/beast/core/detail/base64.hpp. De detail::-namespace is de manier van Boost om te zeggen "dit is onze interne aangelegenheid", en de maintainers hebben geweigerd het te promoten naar een publieke API. Iedereen gebruikt het toch, want het is klein, snel en header-only: definieer BOOST_BEAST_HEADER_ONLY vóór de include en er is niets te linken. Het is overigens ook de codec die Boost.Beasts eigen WebSocket-handshake gebruikt voor de Sec-WebSocket-Accept-berekening, dus het kauwt al jaren echt verkeer.
De persoonlijkheid van de decoderingsfunctie is de verrassing. Die neemt je outputbuffer, de input en de lengte, en geeft een paar terug: het aantal geschreven octetten en het aantal gelezen karakters. Die stopt bij de eerste = en bij het eerste ongeldige karakter - en in beide gevallen doet hij dat zonder het je te vertellen. Geen foutcode, geen uitzondering, geen statusvlag. Een gecorrumpeerde payload, een payload met regelafbreking en een afgekapte staart leveren allemaal een geslaagd gedeeltelijk resultaat op:
| Input | Gedecodeerde bytes | Gelezen karakters | Waarom het stopte |
|---|---|---|---|
"TWFuZQ==" |
4 bytes "Mane" |
6 | Gestopt bij de eerste = |
"TWF!ZQ==" |
2 bytes "Ma" |
3 | Gestopt bij ! |
"TWFu\nZQ==" |
3 bytes "Man" |
4 | Gestopt bij \n |
"TWFuZQ" |
4 bytes "Mane" |
6 | De staart was afgekapt |
"TQ==" |
1 byte "M" |
2 | Padding goed afgehandeld |
Lees die tabel nog een keer, want het is het hele dreigingsmodel van een decoder die nooit klacht: hij deed zijn best, hij stopte waar hij stopte, en het is aan jou om het op te merken. De check heeft één vinkje erbij: bij gepadde input stopt de teller voor "gelezen karakters" bij de eerste =, dus je telt de padtekens er weer bij op vóórdat je vergelijkt, en je eist ook dat het totaal een veelvoud van vier is, want dat is de enige vorm die een echte payload heeft:
#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); /* speelruimte voor oneven lengtes */
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 {}; /* het stopte te vroeg, of de staart was onmogelijk */
return out;
}
Twee details om op te slaan. Eerst: de decoded_size(n)-helper waar de header je naartoe stuurt, is alleen een geldige bovengrens als n een veelvoud van 4 is - het eigen commentaar van de functie zegt dat ook - en daarom voegt de wrapper hierboven een paar bytes reserve toe in plaats van hem te vertrouwen voor willekeurige lengtes. Ten tweede, de oorsprong: de bronbestanden hebben een copyright 2016-2019 van Vinnie Falco, met een voetnoot die delen toekent aan een snippet van Rene Nyffenegger uit 2004-2008. Die snippet is het base64-paar dat twee decennia lang over het Engelstalige internet is gekopieerd en geplakt, en hij wordt nu meegeleverd in Boost, in je binary, en doet HTTP Basic auth voor het hele web.
Boost.Serialization: de decoder die gooit bij een verdwaalde spatie
De serialisatiebibliotheek van Boost draagt de oudste base64 in het C++-ecosysteem: een reeks samenstelbare iterator-adapters uit 2002, geschreven door Robert Ramey, die "wees gul in wat je accepteert" als een persoonlijke belediging beschouwen. De decoderingsrichting woont in binary_from_base64.hpp (ja, de naam is vanuit het standpunt van de output) en maakt een paar met een breedtetransformer die waarden van zes bits herpakt naar bytes van acht bits:
#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;
}
De innerlijke iterator zet elk base64-karakter om naar de zesbitswaarde ervan, en de buitenste groupt die waarden weer in bytes. Zijn striktheid is totaal: elk karakter buiten het alfabet - inclusief een enkele spatie - zorgt ervoor dat de iterator boost::archive::iterators::dataflow_exception gooit met de melding "poging om een waarde te decoderen die niet in de base64-karakterset staat". Dat is het "weigeren tenzij anders aangegeven"-gedrag dat de RFC's later expliciet maakten - geïmplementeerd in 2002, een volledig jaar vóórdat RFC 3548 dezelfde regel codificeerde. Het praktische gevolg is dat MIME-omwikkelde input ontdaan moet worden van zijn regelafbrekingen vóórdat die iterator hem raakt. De tweede quirk is subtieler: in de zoektabel wordt het padkarakter = niet overgeslagen - het wordt gerenderd als de waarde nul. Als je TWFuZQ== decodeert, produceert dat zes bytes - 4d 61 6e 65 00 00 - omdat beide padtekens echte (nul) data bijdroegen, en de resize(size - pads)-regel in de snippet is onmisbaar, niet decoratief. Decodeer TQ== en je krijgt drie bytes die terugknipt tot de ene letter M, precies waar je wilt landen.
Veertig regels die van jou zijn
Base64 is klein genoeg dat een correcte decoder een respectabel ding is om zelf te bezitten, en in C++ is de opbrengst beter dan in enige andere taal: std::string maakt het bufferbeheer prettig, en een handgemaakte decoder kan iets wat geen van de libraryversies hierboven kunnen, namelijk wijzen naar de exacte byte die pijn deed. Deze versie volgt de strikte lectuur van RFC 4648 - de uiteinden knippen, interne whitespace weigeren, padding in het midden weigeren, de lengteregels afdwingen, en zelfs de padbits controleren die volgens de RFC nul moeten zijn bij een conformerende encoder:
#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); /* niet-kanonieke padding-bits */
}
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;
}
Loop mee wat het afdwingt. Witruimte aan het begin en het einde wordt weggeknipt, omdat een payload die uit een e-mailheader is gekopieerd heel goed met een laagje witruimte aankomt. Interne whitespace wordt geweigerd, omdat RFC 4648 zegt dat een decoder MUST niet-alfabetkarakters moet weigeren tenzij de omringende specificatie anders zegt, en op een beveiligingsgrens wil je de strikte lectuur. Een padkarakter in het midden van de data wordt geweigerd, net als elke lengte die niet aan een echte payload kan corresponderen: één karakter kort van een groep is onmogelijk, en één enkel padteken is alleen legaal achter drie body-karakters. De canonieke check aan het einde is de die de meeste implementaties overslaan: als de laatste groep één of twee padtekens had, moeten de ongebruikte lage bits van het laatste alfabetkarakter nul zijn, anders zouden dezelfde bytes geschreven kunnen worden als twee zichtbaar verschillende strings. Die vormbaarheid is de reden dat de check bestaat, en het kost vier regels. Tot slot accepteert de decoder input zonder padding, wat precies is wat JWT-segmenten zijn. En bij falen geeft hij je de positie: geef hem TWF!ZQ== en de fout zit op index 3, op het uitroepteken, en dat is het verschil tussen een bugrapport en een oplossing.
Base64url: het alfabet waar tokens mee spreken
Het standaardalfabet heeft twee karakters die een URL niet overleven: + betekent spatie in een query string, en / betekent map in een path. Sectie 5 van RFC 4648 lost dit op met twee karakterwissels - + wordt - en / wordt _ - en zegt er geen woorden omheen: deze codering is "niet als hetzelfde te beschouwen als de base64-encodering". Het is het alfabet van JWTs, OAuth PKCE code challenges, YouTube-video-identifiers en de meeste API-tokens, en het laat ook routinematig de =-padding vallen, omdat de lengte in een token impliciet bekend is en de padding slechts een percent-escape zou zijn die zich nog mocht aandienen. Geen van de C++-decoders hierboven spreekt het natief - OpenSSL behandelt een - zelfs als die zachte einde-van-input-marker - dus de oplossing is een kleine transcode vóórdat je decodeert. Die is zo kort dat je haar makkelijk in je hoofd kunt houden:
#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; /* herstel de weggelaten padding */
case 3: in += '='; break;
default: break;
}
return in;
}
Pas het toe op een echte JWT en de eerste twee segmenten zijn ronduit JSON in spe. Een klassiek voorbeeldtoken decodeert naar een header van {"alg":"HS256","typ":"JWT"} en een set claims met een onderwerp, een naam en een issued-at-tijdstempel. Het derde segment decodeert op dezelfde manier en geeft je de rauwe signatuurebytes - geen tekst, en geen bewijs van iets. Een token decoderen vertelt je wat het beweert; de signatuur verifiëren vertelt je of je het moet geloven, en dat is een cryptografieklus die geen base64-library voor je zal doen. Op Windows is de situatie in dezelfde richting eenzijdig: CryptBinaryToStringA heeft een CRYPT_STRING_BASE64URI-flag voor de encoderingskant, maar de decoderingsrichting heeft helemaal geen URL-safe flag, dus verdient de transcode hierboven zijn plaats in je spiergeheugen op elk platform.
Van bytes terug naar tekst
Stel een C++-decoder de vraag "welke charset heb ik zojuist gedecodeerd?" en je krijgt het eerlijkste antwoord dat de taal heeft: geen. Decoders zijn van begin tot eind byte-georiënteerd. Ze zien geen tekst; ze zien bytes, en ze geven je precies de bytes terug die waren ingepakt. Als het origineel UTF-8 was, houd je nu UTF-8 vast, en je hebt niets meer nodig. De prettige C++-twist is dat std::string zelf een bytecontainer is met een lengtelid, dus het klassieke C-foutpatroon - een stringfunctie die stopt bij het eerste NUL - verdampt grotendeels. Een gedecodeerd bestand, een gedecodeerd certificaat, een gedecodeerde afbeelding: ze kunnen allemaal in een string wonen, vergeleken worden met ==, gehasht en by value doorgegeven worden, en de nulbytes erin zijn gewoon bytes. Alleen maar niet omzetten naar een C-string en het dan meten met strlen; gebruik size().
Voor de legacy-encoderingen die nog sluipen in oude databases, exports en handgeschreven tools - ISO-8859-1, Windows-1252 en hun neven - is het standaardgereedschap POSIX iconv, dat met glibc wordt meegeleverd. Decodeer eerst naar bytes, en zet daarna die bytes om naar UTF-8 met de codec die bij de bron past:
#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()));
}
De rondreis is verliesvrij in beide richtingen: pak een string in als ISO-8859-1, zet hem in base64, verzend hem, decodeer hem, zet hem om, en je krijgt exact terug waar je mee begon, met de accentletters intact. En binaire data heeft helemaal geen charset - een PNG is een PNG of je vindt het leuk of niet, en dat is het bevrijdendste antwoord van het hele artikel.
Bestanden decoderen
Kleine bestanden zijn een dans in vier stappen: openen in binair, lezen in een vector, decoderen, en het resultaat binair terugschrijven. Binair, elke keer, op elk platform - op Windows zou lezen in tekstmodus CRLF-paren vertalen naar enkele nieuwe regels en je data in stilte veranderen vóórdat de decoder hem ziet:
#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>()};
}
Merk de haakjes in de code hierboven op. Met een enkele reeks haakjes is een regel van de vorm std::vector<unsigned char> bytes(istreambuf_iterator<char>(file), istreambuf_iterator<char>()) de beruchte "most vexing parse": de compiler leest hem als een declaratie van een functie die een vector teruggeeft, en hij heeft alle reden om dat te doen. De initialisatievorm met haakjes hierboven omzeilt de grammatica helemaal. Zodra de bytes in handen zijn, stuur ze door een willekeurige decoder uit dit artikel en schrijf het resultaat weg met std::ofstream in std::ios::binary, gebruikmakend van write(data.data(), data.size()) in plaats van de streamoperator, zodat ingebedde nulbytes de reis naar schijf overleven. Een .b64-bestand en zijn gedecodeerde tweeling verschillen dan precies om de 33-procent-belasting die je bij de ingang betaalde, en dat levert een bevredigend checksummoment op.
Grote bestanden: streamen in beide richtingen
Voor bestanden die te groot zijn om in het geheugen te houden, doet de streamende decoder uit het OpenSSL-hoofdstuk het hele werk: een chunk lezen, hem door de context sturen, wat eruit komt wegschrijven, en herhalen. Op elk moment woont er alleen een kleine buffer in het RAM, dus een base64-bestand van 10 GB wordt gedecodeerd met dezelfde code als een van 10 KB, en MIME-stijl regelafbrekingen hebben geen voorverwerking nodig onderweg, omdat de streamende decoder bij nieuwe regels alleen maar schoudert:
#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;
}
De buffergroottes zijn niet willekeurig: de lengteparameters van de EVP-functies zijn int, dus een enkele aanroep is veilig tot 2 GB, en de aantallen hierboven houden elke chunk op 64 KB input met een outputbuffer van 48 KB, wat precies driekwart van de input is. Dat int-plafond is de hele reden waarom het streamende pad bestaat, en het loont om het te kennen als een hard feit, in plaats van het te ontdekken als een platformbug. Als de input zich als gecorrumpeerde onthult, geeft de functie bij de eerste chunk die niet gedecodeerd kan worden false terug, en houdt het outputbestand vast wat daarvoor geldig was - wat, afhankelijk van je pipeline, precies het gedeeltelijke resultaat kan zijn dat je wilde.
HTTP, APIs en de JSON-velden die bytes verbergen
Base64 komt in HTTP in twee vormen voor. De eerste is data: een JSON-antwoord met een "certificate"- of "avatar"-veld vol base64, een upload-endpoint dat de bytes accepteert in een tekstveilige kolom, een download-endpoint dat je een .b64-bestand overhandigt. Het patroon is altijd hetzelfde - de JSON parsen, de string eruit halen, hem decoderen, en het resultaat als bytes behandelen - en de decoderingskant van dit artikel is de hele implementatie. De tweede vorm is inloggegevens: de Authorization: Basic-header is de base64 van user:password, en al dertig jaar is die het enige base64-gebruiksgeval van de standaard. Parsen is twee stappen, en de eerste is waar mensen naar een NUL-afgesloten C-string grijpen midden in binair nabije data en zich afvragen waarom:
#include <optional>
#include <string>
/* strict_decode uit de sectie "Veertig regels die van jou zijn" */
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));
}
Geef hem de headerwaarde na het Basic -prefix, en hij geeft je de gebruiker en het wachtwoord terug als nette strings met lengtebijhouding, of helemaal niets als de payload geen user:pass-paar is. De beveiligingsnoot hoort hier, al is het geen C++-onderwerp: Basic auth is obfuscatie, geen bescherming. De header rijdt in het open voor iedereen die het netwerk kan lezen, dus is het alleen aanvaardbaar achter TLS, en zelfs dan is het de keuze voor machine-op-machine-aanroepen, niet voor mensen.
JWTs: lezen wat een token beweert
Een JSON Web Token is drie base64url-segmenten die met punten zijn gelijmd: header, claims, signatuur. De eerste twee zijn JSON-objecten; de derde is een cryptografische signatuur over de string header.claims, berekend met het algoritme dat in de header staat. C++ heeft geen ingebouwd JWT-type, maar om een token te lezen heb je niets meer nodig dan de transcode uit de base64url-sectie en een decoder, want het interessante deel is het lezen:
#include <cstddef>
#include <cstdio>
#include <string>
/* url_to_standard en strict_decode uit eerdere secties */
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());
}
De header komt terug als {"alg":"HS256","typ":"JWT"} en de claims als {"sub":"1234567890","name":"John Doe","iat":1516239022} - het onderwerp, de naam en een issued-at-tijdstempel. Dat is de hele leeskant, en het is écht handig: loggen wat een token beweert, het debuggen van een 401 door naar het veld voor de vervaldatum te kijken, of beslissen welke claims je vertrouwt, is allemaal maar één decodage verwijderd. Wat het niet is, is verificatie. Het signatuursegment is ook base64url, en het decoderen ervan geeft je 32 of 64 rauwe bytes die op zichzelf niets bewijzen; de signatuur is pas betekenisvol als je de hash van header.claims herberekent met het gedeelde geheim of de publieke sleutel en vergelijkt. Behandel een gedecodeerde JWT zoals je een brief behandelt: hij zegt wat hij zegt, en de zegel verifiëren is een apart, cryptografisch klusje.
Data-URIs: bestanden die zichzelf in een pagina plakten
Een data-URI is een URL waarvan de payload rechtstreeks in het adres zit: data:, gevolgd door een optionele mediatype, een optionele ;base64-marker, een komma, en daarna de data zelf - het hele schema van RFC 2397. Browsers gebruiken ze om afbeeldingen, lettertypen en kleine scripts direct in HTML en CSS in te bedden zonder extra request, en als je ooit een pagina ziet die doorgaat met het netwerk uitgeschakeld, dan is een data-URI een sterke verdachte. Aan de C++-kant is de decoderingsklus de URI opsplitsen en daarna de payload door je gewone decoder sturen, want wanneer de ;base64-marker aanwezig is, is de payload gewoon standaard base64 - meestal gepad, meestal op één regel:
#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);
}
Roep hem aan met data:image/png;base64,iVBORw0KGgo=... en hij geeft je de payload plus de vlag die vertelt welke decoderingsroute je moet kiezen. Twee valkuilen. Eerst: wanneer de marker ontbreekt, is de payload URL-geëncodeerde tekst, geen base64, dus de vlag is geen formaliteit - een URI die er als base64 uitziet maar als percent-geëncodeerde tekst is gegenereerd, decodeert naar rommel. Ten tweede: sommige generators wikkelen lange data-URIs in met nieuwe regels zoals MIME dat doet; je strikte decoder weigert die, dus strip de regeleinden weg vóórdat je decodeert als de bron niet onder je controle is. Als je de payload van een data:image/png-URI decodeert, krijg je de exacte PNG-bytes, header en al, en dat is de stille tevredenheid van de hele onderneming.
E-mail, MIME en de 76-karakters-gewoonte
E-mail is de reden waarom base64 leerde om zijn regels te wikkelen. SMTP was in zijn oorspronkelijke vorm gebouwd om zevenbits-ASCII te vervoeren, dus alles binair moest worden geschreven als afdrukbare tekst voordat het kon reizen. Privacy-Enhanced Mail deed het in 1987 met regels van 64 tekens, en MIME, dat de codering voor e-mail in 1993 standaardiseerde, versoepelde de limiet naar 76 tekens en voegde de regel toe dat een conforme decoder regeleinden gewoon moet negeren. De gewoonte overleefde: een e-mailbijlage is vandaag nog steeds base64, gewikkeld op 76, en de exacte rekensom komt uit op 4/3 keer 78/76 - ongeveer 137 procent van de oorspronkelijke grootte, plus een paar honderd bytes aan headers. Je C++-decoder krimpt het allemaal terug naar 100 procent, en dat is het punt van het hele formaat.
Het vinkje in C++ is dat de decoders in dit artikel het niet eens zijn over regeleinden, en dat elk een reden heeft. De OpenSSL streamende decoder slaat ze overal in de stream over, wat precies de regel van MIME is. De OpenSSL one-shot functie weigert elke whitespace binnen de payload. Boost.Beast stopt bij de eerste nieuwe regel zonder het te zeggen. De Boost-iterators gooien bij een enkele spatie. Dus als een payload uit e-mail komt, is je eerste beslissing welke decoder je gebruikt, of je stript de regeleinden zelf - met één regel aan erase-remove over \r en \n - en laat je elke decoder die je wilt het echte werk doen. Vooraf strippen is de saaie, betrouwbare keuze, en het is de keuze die je decoderkeuze onafhankelijk houdt van de geschiedenis van je payload.
Databases, configbestanden en omgevingsvariabelen
De derde thuisbasis van base64 is de opslaglaag: een kolom in een legacy-database waarvan de documentatie "base64" zegt en niets anders, een config-blob in een JSON-bestand, een payload in een omgevingsvariabele die door één dienst was gebase64d, zodat hij een shell overleefde. Het decoderingspatroon is hetzelfde als overal - de string lezen, decoderen, als bytes behandelen - maar de niet-benoemde input verdient een speciale paragraaf, want soms weet je écht niet welk alfabet werd gebruikt. Je kunt het niet weten, maar je kunt het testen, want vier karakters doen het grootste deel van het werk:
- Bevat
+of/- dan kan alleen het standaardalfabet kloppen. - Bevat
-of_- dan kan alleen het URL-safe alfabet kloppen. - Geen van beide, maar eindigt op
=- gepadde standaard, of een gepadde URL-safe string waarvan de payload toevallig nooit de twee gewisselde karakters nodig had. - Geen van beide, geen padding - kan allebei zijn; de rauwe URL-safe vorm is de gangbare op het web, dus probeer die eerst voor data die in een URL of een token geboren is.
Strings die geen enkel van de vier onderscheidende karakters gebruiken, decoderen identiek onder beide alfabetten, dus voor die is de volgorde waarin je ze probeert een kwestie van waar de data vandaan komt: dingen die in e-mail geboren zijn willen het standaardalfabet, dingen die in een URL geboren zijn willen het URL-alfabet. En onthoud om ook de versie met en zonder padding van dezelfde string te proberen - één ontbrekend = is het verschil tussen "geweigerd" en "opgelost", en daarom accepteert de strikte decoder hierboven beide.
De command line heeft twee base64's
Voor eenmalige klusjes heeft een Linux-doos meestal twee base64-decoders, en ze gedragen zich anders op precies de manier die mensen beetpakt. De eerste is base64 van GNU coreutils (sommige nieuwere distributies leveren in plaats daarvan de uutils-herimplementatie, en beide spreken dezelfde flags - check met base64 --version). Het is conform RFC 4648, wikkelt bij encoderen op 76 tekens (met -w 0 schakel je dat uit), en bij decoderen accepteert het met plezier nieuwe regels overal; de -i-flag maakt de rommelverdraagzaamheid expliciet in plaats van toevallig. De tweede is die van OpenSSL, en hier is de wending: openssl base64 is helemaal geen eigen app. Sinds de 1.1.0-reeks (2016) checkt het enc-programma zijn eigen aanroepnaam, en als het "base64" werd genoemd, schakelt het zichzelf om naar base64-modus - een stringvergelijking op argv[0], wat de C-wijze is om een alias mee te leveren. Zonder -A verwacht het ergens in de eerste 1024 bytes van de input een nieuwe regel, dus een lange éénregelige string komt terug leeg, met exit code 0. Met -A leest het één regel, en de gedocumenteerde buglijst voor het enc-commando is een museum met twee items: de -A-optie werkt niet goed met grote bestanden, en zonder -A, als de eerste 1024 bytes geen nieuwe regel bevatten, worden de eerste twee regels van de input genegeerd. In een pipeline ziet een stil leeg bestand er precies zo uit als een geslaagde decodage van een lege payload.
# de eerlijke commando's op één regel
base64 -d < payload.b64 > payload.bin
openssl base64 -d -A < payload.b64 > payload.bin
Geen van beide spreekt base64url natief, wat nog een reden is waarom de transcode-snippet in je spiergeheugen hoort. Voor alles wat ertoe doet, decodeer in je programma, waar fouten terugkomen als getallen die je kunt testen, en de exit code van een stille tool niet je enige signaal is.
Vallen die specifiek C++ beetpakken
- De nulopvulling.
EVP_DecodeBlockgeeft drie bytes terug voorTQ==: de letter M plus twee nullen. Herstel de echte lengte uit de padding, of gebruik de streamende API, die eerlijk is over de telling. - De streamende quirk van vóór 3.5. In OpenSSL-releases vóór 3.5.0 (april 2025) had
EVP_DecodeUpdatedezelfde nulopvulgewoonte. Code die tegen een 3.0- of 3.3-pinning is geschreven, kan je liegen over staartlengtes; het herstel staat vermeld in de geschiedenissectie van de man page. - De stille stop.
decodevan Boost.Beast heeft geen foutkanaal: hij stopt bij elk ongeldig karakter, elke nieuwe regel en elke onmogelijke staartlengte, en geeft een gedeeltelijk resultaat terug met een serieuze blik. Check datconsumed + pads == input.size()en dat het totaal een veelvoud van 4 is, anders decodeer je wat hij besloot te decoderen. - De decoded_size-val.
b64::decoded_size(n)gaat ervan uit datndeelbaar is door 4. Twee inputkarakters kunnen één byte opleveren, terwijldecoded_size(2)nul zegt - voeg reserve toe voor oneven lengtes. - De nulbyte-padtekens. De Boost-iterators decoderen
=als de waarde nul, dusTWFuZQ==wordt zes bytes inclusief twee afsluitende nullen. Trek het aantal padtekens af, of geniet van je spoken. - Whitespace, op vier manieren. OpenSSL streaming slaat hem over, OpenSSL one-shot weigert hem intern, de archive-iterators gooien erop, en Beast stopt erbij. Gekopieëerde strings houden ervan om whitespace mee te dragen, en elke decoder heeft er een eigen mening over.
- Gesigneerde char-indexering. Als je je eigen decodetafel bouwt die per karakter wordt geïndexeerd, indexeer dan met
unsigned char. Op platforms waarchargesigneerd is, wordt een byte boven 127 een negatieve index, en dat is ongedefinieerd gedrag in een laboratoriumjas. - Het minteken is een tijdsreiziger. In OpenSSL is
-een zachte einde-van-input-marker uit het PEM-tijdperk, geen alfabetkarakter. Transcodeer base64url voordat je decodeert. - int, geen size_t. De EVP-lengteparameters zijn
int. Boven 2 GB is alleen het in chunks verwerkende streamende pad veilig, en dat is waarom het bestaat. - Tekstmodus op Windows. Een bestand openen voor tekst vertaalt CRLF naar LF en corrumpt je input vóór het decoderen.
std::ios::binary, elke keer, op elk platform. - De most vexing parse.
std::vector<char> v(istreambuf_iterator<char>(f), istreambuf_iterator<char>())is een functiedeclaratie. Gebruik initialisatie met haakjes of een pointerpair. - Niet-kanonieke padbits. Een soepele decoder kan strings accepteren waarvan de ongebruikte padbits niet nul zijn, zodat twee zichtbaar verschillende strings naar dezelfde bytes decoderen (base64-vormbaarheid). Op beveiligingsgrenzen weiger wat je niet nodig hebt - RFC 4648 zegt dat decoders dat precies mag doen.
- De command line faalt in stilte.
openssl base64 -dzonder-Aslaakt éénregelige input (lege output, exit 0); de gedocumenteerde bugs dekken grote bestanden en input zonder nieuwe regel in beide richtingen. Check je output in pipelines. - strlen op binair.
std::stringhoudt nulbytes gelukkig, maar het moment dat je een C-string overhandigt aan een legacy API, stoptstrlenbij het eerste NUL. Geef lengte en pointer door, nooit een kale pointer.
Een korte geschiedenis van Base64 in C++
Het formaat is ouder dan het moderne tijdperk van de taal. Het eerste gestandaardiseerde gebruik van de codering die nu MIME base64 heet, was het Privacy-Enhanced Mail-protocol, voorgesteld in 1987 met regels van 64 tekens en een RSA-MD2/MD5-berichtintegriteitscheck die eraan was gelijmd; de naam "base64" zelf kwam pas in 1993, toen de MIME-standaarden hem die naam gaven. C++ verscheen op het toneel als C++98 in 1998 - vijf jaar na MIME - en de eerste base64-code waarnaar de ontwikkelaars van de taal grepen, was het C-paar van Rene Nyffenegger (2004-2008), dat een Stack Overflow-vraag van 4 december 2008 over het web verspreidde. Het mooiste deel van dat verhaal is wie er niet opdook: de auteur postte zelf nooit een antwoord, maar zijn snippet werd het volkslied dat iedereen kopieerde. Een antwoord in de thread herdrukte zijn volledige implementatie van zijn eigen website, voor het geval de site zou verdwijnen.
Toen deed het ecosysteem wat ecosystemen doen. In 2002 bracht de Boost.Serialization van Robert Ramey de iterator-adapters uit - de oudste base64 in de C++-gereedschapskist, zo strikt dat hij een uitzondering gooit bij een enkele spatie, een jaar vóórdat RFC 3548 de regel codificeerde die hij al afdwong. In 2017 bracht Boost 1.66 Beast, en samen met haar de header-only-codec die vandaag nog wordt meegeleverd, met de Nyffenegger-toeschrijving in de voetnoot. Ondertussen ging de standaard zelf C++11, C++14, C++17, C++20 en C++23 (gepubliceerd in 2024), en ze keken allemaal even naar het alfabet van 64 tekens, en gingen daarna gewoon verder. C++26 voegt een nieuwe <text_encoding>-header toe voor werk aan tekstcoders, goedkeurd al in 2022; de technische inhoud was afgerond op de ISO-bijeenkomst van maart 2026 in Londen, waar het comité stemde met 114-12-3 om hem naar publicatie te sturen, en de latere bijeenkomsten van het comité in 2026 - waaronder die in Búzios, Brazilië op 16-21 november - besteden tijd aan het openen van de volgende working draft, C++29, in plaats van over deze te stemmen. Base64 zat nooit in de draft. Zeven standaarden, drie decennia, één header voor tekstcodering - en het comité heeft nu elke denkbare reden gehad om base64 toe te voegen, en heeft ze allemaal laten liggen. De praktische geschiedenis van base64 in C++ is, en blijft, de geschiedenis van zijn libraries: de EVP-routines van OpenSSL, twee Boost-varianten, een Windows API-aanroep, en een snippet van veertig regels die van jou is.
Leuke weetjes, C++-editie
- Hetzelfde paar functies verschijnt in de antwoorden op een Stack Overflow-vraag uit 2008, in de bron van Boost.Beast met een toeschrijvingsvoetnoot, en in de headerbestanden van oneindig veel privé-codebases. Vraag een C++-ontwikkelaar waar hun base64 vandaan komt, en het eerlijkste antwoord is "ik weet het niet, en het internet ook niet".
- De archive-iterators van Boost zijn de oudste base64 in dit artikel, copyright 2002 - hetzelfde jaar dat de .NET Framework 1.0 SDK werd uitgebracht. Zij gooien een uitzondering bij een enkele spatie, wat betekent dat zij de regel "niet-alfabetkarakters weigeren" al afdwongen vóórdat de RFC's bijhaalden: RFC 3548 codificeerde hem in 2003, en RFC 4648 herhaalde hem in 2006.
- De streamende decoder van OpenSSL werkt met een interne buffer van 80 bytes, maar hij spoelt elke 64 base64-karakters, dezelfde regelbreedte die PEM-harnas sinds 1987 gebruikt. Dat stille 64 is een van de laatste plekken waar het oude formaat in 2026 nog onmisbaar werk doet.
- De kleinste mogelijke gepadde base64 is vier karakters,
TQ==: één byte die een kostuum van twee karakters draagt. De kleinste zonder padding is twee karakters,TQ. Welke van de twee je mag decoderen, hangt volledig af van wie hem heeft geëncodeerd, en die persoon dacht niet aan jou. - De rekensom van MIME is exact: 4/3 keer 78/76, daarom komt een e-mailbijlage aan op ongeveer 137 procent van de oorspronkelijke grootte (plus een paar honderd bytes aan headers erbovenop). Je C++-decoder krimpt het terug naar 100 procent, en dat is het stille genot van de hele onderneming.
- Op een gangbare libstdc++ of MSVC vervoert
std::stringkleine payloads in een stack-buffer via small-string optimization in plaats van te alloceren. Een input van 9 bytes decodeert naar 6 bytes en raakt de heap nooit aan. De base64-vorm van je kleine config-blob kan letterlijk in een stackframe wonen, en dat is het soort gratis lunch dat de standaardlibrary niet reclameert. - Het
openssl base64-commando waarnaar je in een shell mag grijpen, is helemaal geen commando. Het is hetenc-programma dat zijn eigen naam checkt inargv[0]en van persoonlijkheid wisselt. Een alias via stringvergelijking, wat de C++-wijze is om dingen te doen, in C. - YouTube-video-identifiers zijn base64url: elf karakters, geen padding, geen
+of/in de buurt van een URL. Het meest bekeken coderingsformaat ter planeet draait op de "URL- en bestandsnaam-veilige"-variant die RFC 4648 toevoegde in een sectie die op één pagina past.
Wanneer je juist moet inpakken
Alles wat je zojuist gedecodeerd hebt, is aan de andere kant ingepakt door dezelfde gereedschapskist: EVP_EncodeBlock voor one-shots, EVP_EncodeUpdate plus EVP_EncodeFinal voor streams (en daar komen die regels van 64 tekens vandaan), dezelfde bufferrekening in omgekeerde richting, en dezelfde 33-procent-belasting die decodering in stilte terugbetaalt. Het complete inpakverhaal - de groottemathematica opgesomd, de encoders die hun output NUL-afsluiten, de Boost-iterator die een padkarakter nooit heeft ontmoet, base64url, MIME-wikkelen, bestanden, en de Windows API met zijn CRLF-gewoonte - staat in de C++-encoderingshandleiding op de zustersite. Ga die lezen, en kom dan terug om iets groots te openen. Dat is het hele spel: geen standaardlibrary, drie betrouwbare aanbieders met drie verschillende temperamenten, een decoder die wijst naar de exacte byte die pijn deed, een bugfix uit 2025 die de streamende staart veranderde, en één nulgevulde drietal om voor altijd te onthouden. Fijn uitpakwerk.
Laatst bijgewerkt: 2026-10-06
Gerelateerd artikel: Base64-codering in C++ (Cpp): een complete gids