Base 64 エンコーダとデコーダ

PerlMIME::Base64 のような感じで利用できる Base 64 エンコーダとデコーダが欲しくなったので書いてみました。 RFC 4648 の Base 64 の文字集合を使うものと、 URL 用に 62 と 63 を差し替えた文字集合を使うものと 2 種類ずつ関数を定義してあります。 MIME::Base64 のエンコーダは 76 桁固定ですが、 桁を指定できるようにしています。デコーダMIME::Base64 と同じで、符号化のための文字集合以外の文字は読み飛ばします。 デコーダはパディングが削られていても、複合後のオクテット数が予想できるとき複合をおこないます。パディングが存在するときは、正しいパディングかどうかをチェックしますが、パディングを省略してあるときはチェックを飛ばします。

mime-base64 改訂

2015 年9 月 2 日修正 '\x80' を越えるコードを含むオクテットを正しく encode_base64 していなかったバグを修正しました。
2015 年9 月 3 日修正 encode_base64crypt 関数と decode_base64crypt 関数を追加しました。

BASE64エンコードの本体は encode_base64basic 関数です。 デコーダの本体は decode_base64basic です。 これらは BASE64 の行折り返しあり・なし、 BASE64url、 BASE64crypt に対応できるように汎用に作ってあります。 そのままでは使い勝手が悪いので、 利便性のため、 良く使うラッパ関数を利用できるようにしてあります。

//@<mime-base64.hpp@>=
#ifndef MIME_BASE64_HPP
#define MIME_BASE64_HPP

#include <string>

namespace mime {

std::string encode_base64 (std::string const& in);
std::string encode_base64 (std::string const& in, std::string const& endline);
std::string encode_base64url (std::string const& in);
std::string encode_base64crypt (std::string const& in);
bool decode_base64 (std::string const& str64, std::string& octets);
bool decode_base64url (std::string const& str64, std::string& octets);
bool decode_base64crypt (std::string const& str64, std::string& octets);

std::string encode_base64basic (std::string const& in, std::string const& b64,
    int const padding, std::string const& endline, int const width);
bool decode_base64basic (std::string const& str64, std::string& octets,
    int const *c64);

}// namespace mime
#endif

関数定義は STL 以外には依存していません。

//@<mime-base64.cpp@>=
#include <string>
#include <algorithm>
#include "mime-base64.hpp"

namespace mime {
//@<encode_base64 と encode_base64url と encode_base64crypt を定義します@>
//@<decode_base64 と decode_base64url と decode_base64crypt を定義します@>

//@<encode_base64basic を定義します@>

//@<decode_base64basic を定義します@>

}// namespace mime

オクテットから BASE64エンコードする encode_base64 関数で endline 文字列を省略すると 76 桁目で LF で改行します。endline 文字列で改行コードを別のもの、 例えば CRLF、 に変更することもできますし、空文字列を指定することで行分割なしにすることもできます。 一方、encode_base64url は行分割しません。

//@<encode_base64 と encode_base64url と encode_base64crypt を定義します@>=
std::string
encode_base64 (std::string const& in)
{
    static const std::string B64
        = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    return encode_base64basic (in, B64, '=', "\n", 76);
}

std::string
encode_base64 (std::string const& in, std::string const& endline)
{
    static const std::string B64
        = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    return encode_base64basic (in, B64, '=', endline, 76);
}

std::string
encode_base64url (std::string const& in)
{
    static const std::string B64
        = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
    return encode_base64basic (in, B64, '=', "", -1);
}

std::string
encode_base64crypt (std::string const& in)
{
    static const std::string B64
        = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789./";
    return encode_base64basic (in, B64, '\0', "", -1);
}

decode_base64 関数は BASE64 をオクテットへデコードします。 BASE64 にエラーがあるときは戻り値が偽になり、真のとき、参照引数 octets にデコード結果を格納します。decode_base64url はそれの BASE64url 版です。変換テーブルで ASCII7 のそれぞれの文字を 64 進数にマッピングします。-1 の文字は無視します。変換テーブル中でパディング文字も -1 にします。パディング文字は '=' に固定しています。

//@<decode_base64 と decode_base64url と decode_base64crypt を定義します@>=
bool
decode_base64 (std::string const& str64, std::string& octets)
{
    static const int C64[128] = {
       -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
       -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
       -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
       52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1,
       -1,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14,
       15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
       -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
       41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
    };
    return decode_base64basic (str64, octets, C64);
}

bool
decode_base64url (std::string const& str64, std::string& octets)
{
    static const int C64[128] = {
       -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
       -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
       -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1,
       52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1,
       -1,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14,
       15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63,
       -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
       41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
    };
    return decode_base64basic (str64, octets, C64);
}

bool
decode_base64crypt (std::string const& str64, std::string& octets)
{
    static const int C64[128] = {
       -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
       -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
       -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, 63,
       52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1,
       -1,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14,
       15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
       -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
       41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
    };
    return decode_base64basic (str64, octets, C64);
}

encode_base64basic は、オクテットをいろんな書式の BASE64エンコードします。BASE64 の表記に用いる文字セットに b64 文字列を使い、パディング文字に padding 文字を使います。b64 は 64 文字以上のサイズがないといけませんが、エラー・チェックを省いています。padding はヌル文字のときはパディングなし、それ以外のときはパディングを補います。endline と width で行分割のやりかたを制御します。width が 1 以上のときは、それ以上の 4 カラムごとのタブ位置で行を分割します。そのとき、endline を改行コードに使います。

encode_base64basic は 3 オクテット 24 ビットごとに処理を進めて 4 つの 6 ビットに切り分けて、それぞれ b64 の文字へ置き換え、 4 文字の BASE64 に変換していきます。

//@<encode_base64basic を定義します@>=
std::string
encode_base64basic (std::string const& in, std::string const& b64,
    int const padding, std::string const& endline, int const width)
{
    std::string out;
    std::string::const_iterator s = in.cbegin ();
    int cols = 0;
    while (s < in.cend ()) {
        // octets: 00000000 11111111 22222222
        // base64: aaaaaabb bbbbcccc ccdddddd
        std::size_t const u0 = static_cast<uint8_t> (s[0]);
        std::size_t const u1 = s + 1 < in.cend () ? static_cast<uint8_t> (s[1]) : 0;
        std::size_t const u2 = s + 2 < in.cend () ? static_cast<uint8_t> (s[2]) : 0;
        out.push_back (b64[(u0 >> 2) & 0x3f]);
        out.push_back (b64[((u0 << 4) | (u1 >> 4)) & 0x3f]);
        if (s + 1 < in.cend ())
            out.push_back (b64[((u1 << 2) | (u2 >> 6)) & 0x3f]);
        else if (padding)
            out.push_back (padding);
        if (s + 2 < in.cend ())
            out.push_back (b64[u2 & 0x3f]);
        else if (padding)
            out.push_back (padding);
        s = s + 2 < in.cend () ? s + 3 : in.cend ();
        if (width > 0) {
            cols += 4;
            if (cols >= width) {
                out.append (endline);
                cols = 0;
            }
        }
    }
    if (width > 0 && cols > 0 && cols < width)
        out.append (endline);
    return out;
}

decode_base64basic は BASE64 をオクテットへ変換します。 ASCII7 の 128 文字を 64 進数にマッピングするテーブル c64 配列で、 0 から 63 へ変換可能な文字をバッファに入れていき、 4 文字分溜まったところで、 3 オクテットへデコードします。パディング文字は '=' 固定で、パディングが存在するときは、正しいパディング数かどうかをチェックして、正しくないときは偽を返します。パディングが存在しないときは、エラーとせずにパディング文字数を予測して、最後のオクテット数を求めます。

//@<decode_base64basic を定義します@>=
bool
decode_base64basic (std::string const& str64, std::string& octets, int const *c64)
{
    std::string out;
    unsigned int u[4] = {0, 0, 0, 0};
    std::size_t k = 0;
    std::string::const_iterator s = str64.cbegin ();
    while (s < str64.cend () && '=' != *s) {
        unsigned int ch = *s++;
        if (ch > 127 || c64[ch] < 0)
            continue;
        u[k++] = c64[ch];
        if (k >= 4) {
            out.push_back ((u[0] << 2) | (u[1] >> 4));
            out.push_back (((u[1] & 0x0f) << 4) | (u[2] >> 2));
            out.push_back (((u[2] & 0x03) << 6) | u[3]);
            k = 0;
            u[0] = u[1] = u[2] = u[3] = 0;
        }
    }
    std::size_t npadding = 0;
    while (s < str64.cend ()) {
        unsigned int ch = *s++;
        if ('=' == ch)
            ++npadding;
    }
    k = 0 == k ? 4 : k;  // k の補正。ただし、以下のコードでは補正結果を利用していない。
    if (k == 1 || npadding > 2 || (npadding > 0 && k + npadding != 4))
        return false;
    npadding = 4 - k;
    if (1 == npadding || 2 == npadding)
        out.push_back ((u[0] << 2) | (u[1] >> 4));
    if (1 == npadding)
        out.push_back (((u[1] & 0x0f) << 4) | (u[2] >> 2));
    std::swap (octets, out);
    return true;
}