Загрузить файл
  1. base64hub
  2. Base64 в коде
  3. C++
Код

Base64 в C++

В стандартной библиотеке C++ нет Base64, даже в C++23. Здесь есть компактная header-only реализация, проверенная фаззингом по эталону на Python, и API библиотек, которые у вас, возможно, уже подключены.

Header-only реализация (C++17)

encode принимает алфавит и флаг дополнения, поэтому одна функция выдаёт и стандартный, и URL-safe Base64. decode принимает входные данные с дополнением и без него, но отвергает недопустимые символы, = не на своём месте, невозможные длины и ненулевые биты дополнения.

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

Декодер работает как накопитель битов: каждый символ добавляет 6 бит, и каждые полные 8 бит становятся байтом. Всё, что осталось в конце, – биты дополнения, и они должны быть нулевыми. Почему так, показано в статье как работает Base64.

Кодирование и декодирование строк

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

std::string хранит байты. Если исходный файл в UTF-8 (по умолчанию так у GCC и Clang, MSVC нужен /utf-8), литералы будут в UTF-8 и результат совпадёт с любым другим языком. Для std::u8string из C++20 передайте s.data() и s.size() в перегрузку с указателем.

URL-safe и без дополнения

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

Экранирование нужно потому, что до C++17 ??> – это триграф; GCC заменяет его в строгих режимах -std=c++11 / c++14. JWT и большинство токенов используют Base64URL без дополнения; см. Base64 и Base64URL.

Результаты проверки

Что возвращает decode (всё проверено):

ВходРезультат
aGk=2 байта
aGk2 байта (без дополнения принимается)
aA== / aA1 байт
aGl=nullopt (ненулевые биты дополнения)
aGk==nullopt (неверная длина)
a===nullopt (слишком много дополнения)
aGk=!!nullopt
PDw_Pz8-Pg==nullopt с kStd

Код собран с g++ -std=c++17 -Wall -Wextra -Wpedantic -fsanitize=address,undefined. Тест: прогон 5000 случайных буферов туда и обратно в обоих алфавитах, сверка кодировщика с b64encode из Python и сверка решения «принять или отвергнуть» с эталоном на Python на 30 000 случайных строк. Расхождений не было. Чтобы проверить одну строку вручную, откройте валидатор Base64: он объясняет причину каждого отказа.

Файлы

#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 важен в Windows, где текстовый режим преобразует концы строк и портит двоичные данные. Большие файлы читайте блоками, размер которых кратен 3, и вызывайте encode для каждого: результаты без проблем склеиваются. Такой цикл показан на странице Base64 в C. Чтобы превратить результат в массив C для встраивания, воспользуйтесь инструментом Base64 в массив байтов.

Библиотеки, которые у вас, возможно, уже есть

Если одна из них уже в зависимостях, её API для Base64 избавит вас от лишнего файла:

БиблиотекаКодированиеДекодирование
OpenSSLEVP_EncodeBlockEVP_DecodeBlock (возвращает длину вместе с байтами дополнения)
Abseilabsl::Base64Escape, absl::WebSafeBase64Escapeabsl::Base64Unescape, absl::WebSafeBase64Unescape
QtQByteArray::toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals)QByteArray::fromBase64
PocoPoco::Base64Encoder (ostream)Poco::Base64Decoder (istream)
Boostboost::beast::detail::base64 (внутренний, не стабильный API)то же

Прежде чем полагаться на библиотеку для проверки входных данных, посмотрите, как она обращается с дополнением и пробелами: поведение у всех разное. Например, EVP_DecodeBlock не вычитает дополнение из возвращаемого значения, поэтому для aGk= сообщает 3 байта, а не 2.

Подводные камни

  • Знаковый char в качестве индекса. table[s[i]] – неопределённое поведение для байтов больше 127, если char знаковый. Приводите к unsigned char, как в коде выше.
  • Декодированные байты как строка C. В декодированных байтах может встретиться \0; никогда не пропускайте их через strlen или любой API, который принимает голый const char* без длины.
  • Буферы размером n * 4 / 3. Их не хватает на величину до 3 символов. Точная формула – в статье о росте размера.

Вопросы и ответы

Есть ли в C++ стандартная функция для Base64?

Нет. Её нет ни в стандартной библиотеке, ни в одном предложении, принятом вплоть до C++23. Возьмите небольшую реализацию вроде той, что на этой странице, или API библиотеки, от которой проект уже зависит.

Что должен возвращать decode: std::string или std::vector<uint8_t>?

Декодированный Base64 – это двоичные данные, и std::vector<std::uint8_t> честно об этом говорит. Если вы знаете, что внутри текст, соберите std::string из итераторов вектора.

Почему std::optional, а не исключения?

Некорректный Base64 во входных данных от пользователя – ожидаемая ситуация, а не исключительная. std::optional заставляет вызывающий код её обработать и ничего не стоит на основном пути. Если в вашем проекте приняты исключения, бросайте их из обёртки.

Читайте также