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

Base64 в C

В стандартной библиотеке C нет функций для Base64, поэтому большинство проектов носят с собой свою реализацию. Ниже короткая версия, которую мы сверили фаззингом с модулем base64 из Python, и детали, в которых обычно ошибаются.

Кодировщик

Каждые 3 входных байта превращаются в 4 символа по 6 бит. Последняя группа из 1 или 2 байт дополняется знаками =. Раскладка битов подробно разобрана в статье как работает Base64.

#include <stddef.h>
#include <stdint.h>

static const char B64[] =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

/* Output size for n input bytes, without the terminating NUL. */
size_t b64_encoded_len(size_t n) { return 4 * ((n + 2) / 3); }

/* Writes b64_encoded_len(n) chars plus a NUL to out. */
void b64_encode(const uint8_t *in, size_t n, char *out) {
    size_t i = 0;
    for (; i + 2 < n; i += 3) {
        uint32_t v = (uint32_t)in[i] << 16 | (uint32_t)in[i + 1] << 8 | in[i + 2];
        *out++ = B64[v >> 18 & 63];
        *out++ = B64[v >> 12 & 63];
        *out++ = B64[v >> 6 & 63];
        *out++ = B64[v & 63];
    }
    if (i < n) {                         /* 1 or 2 bytes left */
        uint32_t v = (uint32_t)in[i] << 16;
        if (i + 1 < n) v |= (uint32_t)in[i + 1] << 8;
        *out++ = B64[v >> 18 & 63];
        *out++ = B64[v >> 12 & 63];
        *out++ = (i + 1 < n) ? B64[v >> 6 & 63] : '=';
        *out++ = '=';
    }
    *out = '\0';
}

Условие цикла i + 2 < n вместо i < n - 2 важно: у size_t выражение n - 2 при n, равном 0 или 1, переполняется в огромное число. Приведения к uint32_t не дают выполнять in[i] << 16 над знаковым int.

Строгий декодер

Декодер принимает только канонический Base64 с дополнением и отвергает неверную длину, символы вне алфавита, дополнение в середине и ненулевые биты дополнения.

static int b64_val(unsigned char c) {
    if (c >= 'A' && c <= 'Z') return c - 'A';
    if (c >= 'a' && c <= 'z') return c - 'a' + 26;
    if (c >= '0' && c <= '9') return c - '0' + 52;
    if (c == '+') return 62;
    if (c == '/') return 63;
    return -1;
}

/* Returns the number of bytes written, or -1 on invalid input.
   out needs room for 3 * (len / 4) bytes. */
long b64_decode(const char *in, size_t len, uint8_t *out) {
    if (len % 4 != 0) return -1;
    size_t o = 0;
    for (size_t i = 0; i < len; i += 4) {
        int a = b64_val(in[i]), b = b64_val(in[i + 1]);
        int c = b64_val(in[i + 2]), d = b64_val(in[i + 3]);
        int last = (i + 4 == len);
        if (a < 0 || b < 0) return -1;
        if (c < 0) {                     /* must be "xx==" at the end */
            if (!last || in[i + 2] != '=' || in[i + 3] != '=' || (b & 15)) return -1;
            out[o++] = (uint8_t)(a << 2 | b >> 4);
        } else if (d < 0) {              /* must be "xxx=" at the end */
            if (!last || in[i + 3] != '=' || (c & 3)) return -1;
            out[o++] = (uint8_t)(a << 2 | b >> 4);
            out[o++] = (uint8_t)(b << 4 | c >> 2);
        } else {
            out[o++] = (uint8_t)(a << 2 | b >> 4);
            out[o++] = (uint8_t)(b << 4 | c >> 2);
            out[o++] = (uint8_t)(c << 6 | d);
        }
    }
    return (long)o;
}

b64_val принимает unsigned char, чтобы на платформах со знаковым char байты больше 127 не превращались в отрицательные индексы. Таблица поиска на 256 элементов быстрее, но цепочку if проще проверить глазами.

Использование и результаты

#include <stdio.h>
#include <string.h>

int main(void) {
    const char *s = "héllo wörld";          /* source file saved as UTF-8 */
    char enc[64];
    b64_encode((const uint8_t *)s, strlen(s), enc);
    printf("%s\n", enc);                     /* aMOpbGxvIHfDtnJsZA== */

    uint8_t dec[64];
    long n = b64_decode(enc, strlen(enc), dec);
    printf("%ld %.*s\n", n, (int)n, dec);    /* 13 héllo wörld */
    return 0;
}

В C нет понятия кодировки текста: в строковом литерале лежат те байты, что записаны в исходном файле. Если исходник сохранён в UTF-8 и у GCC кодировка исполнения по умолчанию, вы получите UTF-8.

Что b64_decode возвращает для типичных входных данных:

ВходВозвратПочему
""0пустая строка допустима
aGk=2hi
aGk-1длина не кратна 4
aGk=!!-1длина и символы
aGl=-1ненулевые биты дополнения
PDw_Pz8-Pg==-1URL-safe символы
a===-1слишком много дополнения

Мы собрали код с gcc -std=c99 -Wall -Wextra -fsanitize=address,undefined, прогнали туда и обратно 2000 случайных буферов и сравнили решение «принять или отвергнуть» на 20 000 случайных строк с b64decode(validate=True) из Python и повторным кодированием. Расхождений и сообщений санитайзеров не было. Проверить свои строки можно валидатором Base64: он применяет те же правила.

Кодирование файла или stdin по частям

Читайте блоки, размер которых кратен 3, и склеенный результат совпадёт с кодированием всего файла за раз:

int main(void) {
    uint8_t in[3 * 1024];                /* multiple of 3: no padding mid-stream */
    char out[4 * 1024 + 1];
    size_t n;
    while ((n = fread(in, 1, sizeof in, stdin)) > 0) {
        b64_encode(in, n, out);
        fputs(out, stdout);
    }
    return ferror(stdin) ? 1 : 0;
}

На случайном файле размером 1 000 003 байта ./b64 < big.bin выдал то же, что base64 -w 0 big.bin. fread читает, пока буфер не заполнится или не кончатся данные, поэтому короткие чтения из канала не вставляют дополнение в середину. В Windows сначала вызовите _setmode(_fileno(stdin), _O_BINARY), иначе преобразование CRLF испортит двоичные данные.

URL-safe вывод

Преобразуйте строку после кодирования: замените два символа и отбросьте дополнение.

void b64_to_url(char *s) {
    char *w = s;
    for (char *r = s; *r; r++) {
        if (*r == '=') break;
        *w++ = (*r == '+') ? '-' : (*r == '/') ? '_' : *r;
    }
    *w = '\0';
}
/* bytes {0xfb, 0xff}: "+/8=" -> "-_8" */

Для декодирования сопоставьте в b64_val символ - с 62, а _ с 63 и принимайте последнюю группу из 2 или 3 символов без =. См. Base64 и Base64URL и статью про дополнение.

Частые ошибки в самописном Base64

  • Буферы с ошибкой на единицу. Забытый завершающий NUL или выходной буфер размером n * 4 / 3, которого не хватает, когда n не кратно 3. Точные числа дают калькулятор размера и статья о росте размера.
  • Поиск по таблице со знаковым char. table[c] с отрицательным c читает за пределами массива.
  • strlen для двоичных данных. В декодированном результате может встретиться 0x00; используйте возвращённую длину, а не strlen.
  • Триграфы. В строгих режимах C99/C11 тестовая строка вроде "<<???>>" содержит триграф ??>, который компилятор превращает в }. GCC предупреждает об этом с -Wtrigraphs. Экранируйте строку как "<<?\?\?>>" или используйте массив байтов. В C23 триграфы убрали.

Версия этого кода на C++ с std::string и std::vector – на странице Base64 в C++.

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

Какого размера должен быть выходной буфер?

Для кодирования – 4 * ((n + 2) / 3) символов плюс один байт на завершающий NUL. Для декодирования всегда хватает 3 * (len / 4) байт; реальная длина из-за дополнения может быть на один-два байта меньше.

Есть ли в OpenSSL или glibc готовая функция Base64?

В OpenSSL есть EVP_EncodeBlock и EVP_DecodeBlock, а её слой BIO умеет кодировать Base64 потоком. Публичного API для Base64 в glibc нет. Если проект уже линкуется с одной из этих библиотек, разумно взять её; иначе код ниже проще, чем новая зависимость.

Почему мой декодер принимает строки вроде aGl=?

aGl= и aGk= обе декодируются в hi, но у первой ненулевые биты дополнения. Нестрогие декодеры эти биты игнорируют. Декодер на этой странице отвергает такие строки, проверяя (c & 3) и (b & 15) в последней группе.

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