ファイルを選択
  1. base64hub
  2. 各言語での Base64
  3. C
コード

C での Base64

C の標準ライブラリには Base64 の関数がないため、多くのプロジェクトが独自の実装を抱えています。ここでは、Python の base64 モジュールと突き合わせてファジングした短い実装と、よく間違えるポイントを紹介します。

エンコーダー

入力 3 バイトごとに、6 ビットずつの 4 文字になります。最後のグループが 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 < n - 2 ではなく i + 2 < n にしているのには理由があります。size_t では、n が 0 か 1 のとき n - 2 がラップアラウンドして巨大な数になるからです。uint32_t へのキャストは、in[i] << 16 が符号付き int で計算されるのを防ぎます。

厳格なデコーダー

このデコーダーは、正規形でパディング付きの Base64 以外をすべて拒否します。長さの誤り、アルファベット外の文字、途中のパディング、0 でないパディングビットが対象です。

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パディングビットが 0 でない
PDw_Pz8-Pg==-1URLセーフの文字
a===-1パディングが多すぎる

gcc -std=c99 -Wall -Wextra -fsanitize=address,undefined でコンパイルし、2,000 個のランダムなバッファで往復変換を行いました。さらに 20,000 個のランダムな文字列について、受理・拒否の判定を Python の b64decode(validate=True) と再エンコードによるチェックの組み合わせと比較しました。差異はなく、サニタイザーの報告もありませんでした。手元の文字列を試すなら、Base64 バリデーター が同じルールを適用します。

ファイルや標準入力をチャンク単位でエンコードする

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セーフな出力

エンコード後に変換します。2 文字を入れ替え、パディングを取り除くだけです。

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 によくあるバグ

  • バッファの 1 バイト不足。 NUL 終端を忘れる、あるいは出力サイズを n * 4 / 3 で見積もるミスです。後者は n が 3 の倍数でないと必ず足りません。正確な数値は サイズ計算ツール と サイズ増加の解説 で確認できます。
  • 符号付き char でのテーブル参照。 c が負だと、table[c] は配列の外を読みます。
  • バイナリデータに strlen。 デコード結果には 0x00 が含まれることがあります。strlen ではなく、戻り値の長さを使ってください。
  • トライグラフ。 厳格な C99/C11 モードでは、"<<???>>" のようなテスト文字列にトライグラフ ??> が含まれ、コンパイラーがこれを } に変換します。GCC は -Wtrigraphs で警告します。"<<?\?\?>>" のようにエスケープするか、バイト配列を使ってください。C23 でトライグラフは廃止されました。

std::string と std::vector を使った C++ 版のコードは C++ での Base64 にあります。

よくある質問

出力バッファはどれくらいの大きさが必要ですか?

エンコードでは 4 * ((n + 2) / 3) 文字に、終端の NUL 用に 1 バイトを足した分です。デコードでは 3 * (len / 4) バイトあれば必ず足ります。実際の長さはパディングの分だけ最大 2 バイト少なくなります。

代わりに使える Base64 関数は OpenSSL や glibc にありますか?

OpenSSL には EVP_EncodeBlock と EVP_DecodeBlock があり、BIO レイヤーで Base64 をストリーム処理することもできます。glibc には公開された Base64 API はありません。すでにこれらのライブラリをリンクしているプロジェクトなら使うのが妥当ですが、そうでなければ新しい依存関係を増やすより下のコードのほうが手軽です。

自作のデコーダーが aGl= のような文字列を受け付けてしまうのはなぜですか?

aGl= と aGk= はどちらも hi にデコードされますが、前者はパディングビットが 0 ではありません。寛容なデコーダーはこれらのビットを無視します。このページのデコーダーは、最後のグループで (c & 3) と (b & 15) を検査して拒否します。

続けて読む