Enviar um arquivo
  1. base64hub
  2. Base64 no código
  3. C++
Código

Base64 em C++

A biblioteca padrão do C++ não tem Base64, nem mesmo no C++23. Esta página traz uma implementação header-only compacta, testada com fuzzing contra o Python, e lista as APIs das bibliotecas que você talvez já tenha.

Uma implementação header-only (C++17)

O encode recebe o alfabeto e uma flag de preenchimento, então a mesma função gera Base64 padrão e URL-safe. O decode aceita entrada com e sem preenchimento, mas rejeita caracteres inválidos, = fora do lugar, comprimentos impossíveis e bits de preenchimento diferentes de zero.

// 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

O decodificador funciona como um acumulador de bits: cada caractere acrescenta 6 bits, e cada 8 bits completos viram um byte. O que sobra no final são os bits de preenchimento, e eles precisam ser zero. A página como o Base64 funciona mostra por quê.

Codificar e decodificar strings

#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
}

Uma std::string guarda bytes. Com um arquivo-fonte em UTF-8 (o padrão do GCC e do Clang; o MSVC precisa de /utf-8), os literais são UTF-8 e a saída bate com a de qualquer outra linguagem. Para std::u8string no C++20, passe s.data() e s.size() para a sobrecarga com ponteiro.

URL-safe e sem preenchimento

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 "<<???>>"

Os escapes estão ali porque ??> é um trígrafo no C++ anterior ao C++17; o GCC o substitui nos modos estritos -std=c++11 / c++14. JWTs e a maioria dos tokens usam Base64URL sem preenchimento; veja Base64 vs Base64URL.

Resultados da validação

O que o decode retorna, tudo conferido:

EntradaResultado
aGk=2 bytes
aGk2 bytes (sem preenchimento é aceito)
aA== / aA1 byte
aGl=nullopt (bits de preenchimento diferentes de zero)
aGk==nullopt (comprimento inválido)
a===nullopt (preenchimento demais)
aGk=!!nullopt
PDw_Pz8-Pg==nullopt com kStd

Compilamos com g++ -std=c++17 -Wall -Wextra -Wpedantic -fsanitize=address,undefined, fizemos ida e volta com 5.000 buffers aleatórios nos dois alfabetos, comparamos o codificador com o b64encode do Python e conferimos a decisão de aceitar ou rejeitar em 30.000 strings aleatórias contra uma referência em Python. Nenhuma divergência. Para conferir uma string à mão, o validador de Base64 explica cada rejeição.

Arquivos

#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()));

O std::ios::binary faz diferença no Windows, onde o modo texto converte os finais de linha e corrompe dados binários. Para arquivos grandes, leia blocos com tamanho múltiplo de 3 e chame encode em cada um; as saídas se concatenam sem problema. A página Base64 em C mostra esse laço. Para transformar o resultado num array C para embutir no código, a ferramenta Base64 para array de bytes gera o array.

Bibliotecas que você talvez já tenha

Se uma destas já é dependência do projeto, a API de Base64 dela poupa um arquivo:

BibliotecaCodificarDecodificar
OpenSSLEVP_EncodeBlockEVP_DecodeBlock (retorna o tamanho incluindo os bytes de preenchimento)
Abseilabsl::Base64Escape, absl::WebSafeBase64Escapeabsl::Base64Unescape, absl::WebSafeBase64Unescape
QtQByteArray::toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals)QByteArray::fromBase64
PocoPoco::Base64Encoder (um ostream)Poco::Base64Decoder (um istream)
Boostboost::beast::detail::base64 (interno, não é uma API estável)idem

Confira como cada biblioteca trata o preenchimento e os espaços antes de confiar nela para validar; elas diferem. Por exemplo, o EVP_DecodeBlock não desconta o preenchimento do valor de retorno, então aGk= informa 3 bytes, não 2.

Armadilhas

  • char com sinal como índice. table[s[i]] é comportamento indefinido para bytes acima de 127 quando char tem sinal. Faça o cast para unsigned char, como acima.
  • Tratar os bytes decodificados como string C. Os bytes decodificados podem conter \0; nunca os passe para strlen nem para qualquer API que receba só um const char*, sem o tamanho.
  • Dimensionar buffers como n * 4 / 3. Faltam até 3 caracteres. A fórmula exata está no guia de aumento de tamanho.

Perguntas frequentes

O C++ tem uma função padrão de Base64?

Não. Nem a biblioteca padrão nem nenhuma proposta aprovada até o C++23 inclui uma. Use uma implementação pequena como a desta página ou a API de uma biblioteca da qual o seu projeto já depende.

O decode deve retornar std::string ou std::vector<uint8_t>?

Base64 decodificado é binário, e std::vector<std::uint8_t> deixa isso claro. Se você sabe que o payload é texto, construa uma std::string a partir dos iteradores do vetor.

Por que usar std::optional em vez de exceções?

Base64 inválido vindo de entrada do usuário é um caso esperado, não excepcional. O std::optional obriga quem chama a tratá-lo e não custa nada no caminho feliz. Lance uma exceção num wrapper se a sua base de código preferir exceções.

Continue lendo