Subir un archivo
  1. base64hub
  2. Base64 en código
  3. C++
Código

Base64 en C++

La biblioteca estándar de C++ no tiene Base64, ni siquiera en C++23. Esta página ofrece una implementación compacta header-only que sometimos a fuzzing contra Python y repasa las API de bibliotecas que quizá ya tengas.

Una implementación header-only (C++17)

encode recibe el alfabeto y un indicador de relleno, así que la misma función genera Base64 estándar y URL-safe. decode acepta entradas con y sin relleno, pero rechaza caracteres incorrectos, = fuera de lugar, longitudes imposibles y bits de relleno distintos de cero.

// base64.hpp
#pragma once
#include <array>
#include <cstdint>
#include <optional>
#include <string>
#include <string_view>
#include <vector>

namespace b64 {

inline constexpr std::string_view kStd =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
inline constexpr std::string_view kUrl =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";

inline std::string encode(const void* data, std::size_t n,
                          std::string_view abc = kStd, bool pad = true) {
    auto p = static_cast<const std::uint8_t*>(data);
    std::string out;
    out.reserve((n + 2) / 3 * 4);
    std::size_t i = 0;
    for (; i + 2 < n; i += 3) {
        std::uint32_t v = p[i] << 16 | p[i + 1] << 8 | p[i + 2];
        out += abc[v >> 18 & 63];
        out += abc[v >> 12 & 63];
        out += abc[v >> 6 & 63];
        out += abc[v & 63];
    }
    if (i < n) {
        std::uint32_t v = p[i] << 16 | (i + 1 < n ? p[i + 1] << 8 : 0);
        out += abc[v >> 18 & 63];
        out += abc[v >> 12 & 63];
        if (i + 1 < n) out += abc[v >> 6 & 63];
        if (pad) out.append(i + 1 < n ? 1 : 2, '=');
    }
    return out;
}

inline std::string encode(std::string_view s, std::string_view abc = kStd,
                          bool pad = true) {
    return encode(s.data(), s.size(), abc, pad);
}

inline std::optional<std::vector<std::uint8_t>> decode(
        std::string_view s, std::string_view abc = kStd) {
    std::array<int, 256> val;
    val.fill(-1);
    for (int i = 0; i < 64; ++i) val[static_cast<unsigned char>(abc[i])] = i;

    std::size_t len = s.size();
    if (len % 4 == 0 && len > 0 && s[len - 1] == '=')
        len -= (s[len - 2] == '=') ? 2 : 1;
    if (len % 4 == 1) return std::nullopt;
    if (s.size() != len && s.size() % 4 != 0) return std::nullopt;

    std::vector<std::uint8_t> out;
    out.reserve(len * 3 / 4);
    std::uint32_t acc = 0;
    int bits = 0;
    for (std::size_t i = 0; i < len; ++i) {
        int v = val[static_cast<unsigned char>(s[i])];
        if (v < 0) return std::nullopt;
        acc = (acc << 6) | static_cast<std::uint32_t>(v);
        bits += 6;
        if (bits >= 8) {
            bits -= 8;
            out.push_back(static_cast<std::uint8_t>(acc >> bits));
            acc &= (1u << bits) - 1;
        }
    }
    if (acc != 0) return std::nullopt;  // leftover bits must be zero
    return out;
}

}  // namespace b64

El decodificador funciona como un acumulador de bits: cada carácter añade 6 bits y cada 8 bits completos forman un byte. Lo que queda al final son los bits de relleno, y deben ser cero. La página cómo funciona Base64 explica por qué.

Codificar y decodificar cadenas

#include "base64.hpp"
#include <iostream>

int main() {
    std::string enc = b64::encode("héllo wörld");
    std::cout << enc << '\n';                         // aMOpbGxvIHfDtnJsZA==

    if (auto bytes = b64::decode(enc))
        std::cout << std::string(bytes->begin(), bytes->end()) << '\n';  // héllo wörld
}

Un std::string contiene bytes. Con un archivo fuente en UTF-8 (lo predeterminado en GCC y Clang; MSVC necesita /utf-8), los literales son UTF-8 y la salida coincide con la de cualquier otro lenguaje. Para std::u8string en C++20, pasa s.data() y s.size() a la sobrecarga que recibe un puntero.

URL-safe y sin relleno

std::string q = "<<?\?\?>>";                          // "<<???>>", escaped (see below)
b64::encode(q, b64::kUrl);                            // PDw_Pz8-Pg==
b64::encode(q, b64::kUrl, false);                     // PDw_Pz8-Pg
b64::decode("PDw_Pz8-Pg", b64::kUrl);                 // bytes of "<<???>>"

Los escapes están ahí porque ??> es un trígrafo en C++ anterior a C++17; GCC lo sustituye en los modos estrictos -std=c++11 / c++14. Los JWT y la mayoría de los tokens usan Base64URL sin relleno; consulta Base64 frente a Base64URL.

Resultados de validación

Lo que devuelve decode, todo verificado:

EntradaResultado
aGk=2 bytes
aGk2 bytes (se acepta sin relleno)
aA== / aA1 byte
aGl=nullopt (bits de relleno distintos de cero)
aGk==nullopt (longitud incorrecta)
a===nullopt (demasiado relleno)
aGk=!!nullopt
PDw_Pz8-Pg==nullopt con kStd

Compilamos con g++ -std=c++17 -Wall -Wextra -Wpedantic -fsanitize=address,undefined, hicimos ida y vuelta con 5000 búferes aleatorios en los dos alfabetos, comparamos el codificador con b64encode de Python y comprobamos la decisión de aceptar o rechazar en 30 000 cadenas aleatorias frente a una referencia en Python. Ninguna discrepancia. Para revisar una sola cadena a mano, el validador Base64 explica cada rechazo.

Archivos

#include <fstream>
#include <iterator>

std::ifstream f("hello.txt", std::ios::binary);
std::string data((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
std::cout << b64::encode(data) << '\n';               // aGVsbG8K

auto raw = b64::decode("aGVsbG8K");
std::ofstream("copy.txt", std::ios::binary)
    .write(reinterpret_cast<const char*>(raw->data()),
           static_cast<std::streamsize>(raw->size()));

std::ios::binary es importante en Windows, donde el modo texto convierte los finales de línea y corrompe los datos binarios. Con archivos grandes, lee bloques cuyo tamaño sea múltiplo de 3 y llama a encode con cada uno; las salidas se concatenan sin problemas. La página Base64 en C muestra ese bucle. Para convertir el resultado en un array de C que incrustar, lo genera la herramienta Base64 a array de bytes.

Bibliotecas que quizá ya tengas

Si alguna de estas ya es una dependencia, su API de Base64 te ahorra un archivo:

BibliotecaCodificarDecodificar
OpenSSLEVP_EncodeBlockEVP_DecodeBlock (devuelve una longitud que incluye los bytes de relleno)
Abseilabsl::Base64Escape, absl::WebSafeBase64Escapeabsl::Base64Unescape, absl::WebSafeBase64Unescape
QtQByteArray::toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals)QByteArray::fromBase64
PocoPoco::Base64Encoder (un ostream)Poco::Base64Decoder (un istream)
Boostboost::beast::detail::base64 (interno, no es una API estable)igual

Antes de confiar en una de ellas para validar, comprueba cómo trata el relleno y los espacios; no todas lo hacen igual. Por ejemplo, EVP_DecodeBlock no resta el relleno de su valor de retorno, así que aGk= da 3 bytes, no 2.

Trampas

  • Un char con signo como índice. table[s[i]] es comportamiento indefinido con bytes por encima de 127 cuando char tiene signo. Convierte a unsigned char como arriba.
  • Tratar los bytes decodificados como una cadena C. Los bytes decodificados pueden contener \0; nunca los pases a strlen ni a ninguna API que reciba un const char* a secas, sin longitud.
  • Dimensionar búferes como n * 4 / 3. Se queda corto en hasta 3 caracteres. La fórmula exacta está en la guía del aumento de tamaño.

Preguntas frecuentes

¿C++ tiene una función estándar para Base64?

No. Ni la biblioteca estándar ni ninguna propuesta incorporada hasta C++23 incluyen una. Usa una implementación pequeña como la de esta página o la API de una biblioteca de la que tu proyecto ya dependa.

¿decode debe devolver std::string o std::vector<uint8_t>?

El Base64 decodificado es binario, y std::vector<std::uint8_t> lo dice con honestidad. Si sabes que el contenido es texto, construye un std::string a partir de los iteradores del vector.

¿Por qué usar std::optional en lugar de excepciones?

Un Base64 no válido que llega de la entrada del usuario es un caso esperado, no excepcional. std::optional obliga a quien llama a tratarlo y no cuesta nada en el camino feliz. Lanza una excepción desde un wrapper si tu código base las prefiere.

Sigue leyendo