空想犬猫記

※当日記では、犬も猫も空想も扱っておりません。(旧・エト記)

続・Base64 encoding/decoding (in C)

id:xoinu:20090727 では,C++の標準ストリームのstreambufを拡張してストリームフィルタを作成してみた。これをCで書くとどうなるだろう。残念ながら,ANSI Cには無いけれど,BSD系の標準I/Oライブラリに funopen というのがあって,これを使うと,FILE*を拡張できるようだ(追記:fwopen,fropenというのもあるが,fcloseに対するフックがあるのはfunopenだけっぽい)。

C++のstreambufに相当するのがcookieというオブジェクト。このオブジェクトが,各callbackに渡されるというわけ。非常に分かり易い。

以下がサンプルコード

int main(void)
{
    /* stringstreamに相当するのが無いので,hoge.txtに出力。*/
    {
        FILE* fout;

        fout = fopen("hoge.txt", "wb");
        fprintf(fout, "Xoinu de gonsu.\n");
        fclose(fout);
    }


    /* foutにbase64フィルタを装着し,sample.txtに出力。*/
    {
        FILE *fout, *fin, *base64;

        fin = fopen("hoge.txt", "rb");
        fout = fopen("sample.txt", "wb");
        base64 = base64_fopen(fout, "wb");
        pipe_stream(base64, fin);
        fclose(base64);
        fclose(fin);
    }

    /* finにbase64フィルタを装着し,読み込んで,stderrに垂れ流す。*/
    {
        FILE *fin, *base64;

        fin = fopen("sample.txt", "rb");
        base64 = base64_fopen(fin, "rb");
        pipe_stream(stderr, base64);
        fprintf(stderr, "\n");
        fclose(base64);
    }

    return 0;
}

出力結果:

Xoinu de gonsu.

sample.txt:

WG9pbnUgZGUgZ29uc3UuCg==

C++に比べると,I/Oの拡張に専念できるので分かり易い。C++のstreambufは高機能だけれども,I/Oの抽象化のインターフェースとバッファリングのインターフェースと実装がごにょごにょ混ざっていているのが残念なところ。うんことは言わないが,人気がないのもうなずける。

以下が,C版base64フィルタのソース。BSDライセンスでお楽しみ下さい。

struct base64_buffer {
    FILE* fp;
    char  buff[4];
    int   cols;
    int   len;
    char  open_mode; /* 0:read, 1:write */
};

/*---------------------------------------------------------------------------*/

static void base64_encode
(
    const char* in,
    int len,
    char out[4]
)
{
    static const char base64_table[65] =
        "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

    switch (len)
    {
    case 1:
        out[0] = base64_table[in[0]>>2];
        out[1] = base64_table[(in[0]<<4)&63];
        out[2] = '=';
        out[3] = '=';
        break;
    case 2:
        out[0] = base64_table[in[0]>>2];
        out[1] = base64_table[((in[0]<<4)|(in[1]>>4))&63];
        out[2] = base64_table[(in[1]<<2)&63];
        out[3] = '=';
        break;
    case 3:
        out[0] = base64_table[(in[0]>>2)&63];
        out[1] = base64_table[((in[0]<<4)|(in[1]>>4))&63];
        out[2] = base64_table[((in[1]<<2)|(in[2]>>6))&63];
        out[3] = base64_table[in[2]&63];
        break;
    default:
        out[0] = '=';
        out[1] = '=';
        out[2] = '=';
        out[3] = '=';
        break;
    }
}

/*---------------------------------------------------------------------------*/

static int base64_decode
(
    const char in[4],
    char* out,
    int* length
)
{
    *length = 0;
    int i;
    char n;

    for (i = 0; i < 4; ++i)
    {
        n = 0;

        if ('A' <= in[i] && in[i] <= 'Z') n = (in[i]-'A') ;
        else if ('a' <= in[i] && in[i] <= 'z') n = (in[i]-'a'+26);
        else if ('0' <= in[i] && in[i] <= '9') n = (in[i]-'0'+52);
        else if ('+' == in[i]) n = 62;
        else if ('/' == in[i]) n = 63;
        else if ('=' == in[i]) break;
        else break;

        switch (i)
        {
        case 0:
            out[0]  = n << 2;
            *length = 1;
            break;
        case 1:
            out[0] |= (n >> 4);
            out[1]  = (n << 4);
            if (out[1])
                *length = 2;
            break;
        case 2:
            out[1] |= (n >> 2);
            out[2]  = (n << 6);
            if (out[2])
                *length = 3;
            break;
        case 3:
            out[2] |= n;
            *length = 3;
            break;
        default:
            break;
        }
    }

    return 0;
}

/*---------------------------------------------------------------------------*/

static int base64_writefn
(
    void* cookie,
    const char* buff,
    int len
)
{
    struct base64_buffer* b;
    const char* end;
    const char* it;
    char out[4];

    b   = cookie;
    it  = buff;
    end = it + len;

    while (it != end) {
        while (it != end && b->len < 3)
            b->buff[b->len++] = *it++;

        if (b->len < 3)
            break;

        assert(3 == b->len);
        base64_encode(b->buff, b->len, out);
        fwrite(out, 1, 4, b->fp);
        b->len = 0;

        if (19 == ++b->cols) {
            fwrite("\n", 1, 1, b->fp);
            b->cols = 0;
        }
    }

    return it - buff;
}

/*---------------------------------------------------------------------------*/

static char* base64_decode_chunk
(
    struct base64_buffer* b,
    char* buff,
    int*  len
)
{
    char ch, *it, *end, out[3], chunk[4];
    int i, ndecoded;

    it = &chunk[0];
    end = &chunk[4];

    while (it != end && 1 == fread(&ch, 1, 1, b->fp)) {
        if ('\n' == ch || ' ' == ch || '\r' == ch)
            continue;
        *it++ = ch;
    }

    while (it != end)
        *it++ = '=';

    base64_decode(chunk, out, &ndecoded);

    if (*len < ndecoded) {
        memcpy(buff, out, *len);
        buff += *len;
        ndecoded -= *len;
        memcpy(b->buff, &out[0] + *len, ndecoded);
        b->len = ndecoded;
        *len = 0;
    } else if (ndecoded > 0) {
        memcpy(buff, out, ndecoded);
        buff += ndecoded;
        *len -= ndecoded;
    }

    return buff;
}

/*---------------------------------------------------------------------------*/

static int base64_readfn
(
    void* cookie,
    char* buff,
    int len
)
{
    struct base64_buffer* b;
    int n, nread;

    b = cookie;
    nread = 0;

    if (len < b->len) {
        memcpy(buff, b->buff, len);
        b->len -= len;
        memmove(b->buff, b->buff + len, b->len);
        nread = len;
    } else {
        memcpy(buff, b->buff, b->len);
        nread = b->len;
        len -= b->len;
        buff += b->len;
        b->len = 0;

        while (len > 0) {
            n = len;
            buff = base64_decode_chunk(b, buff, &len);
            n = n - len;
            if (0 == n)
                break;
            nread += n;
        }
    }
    return nread;
}

/*---------------------------------------------------------------------------*/

static int base64_closefn(void* cookie)
{
    struct base64_buffer* b;

    b = cookie;

    if (1 == b->open_mode && b->len) {
        char out[4];
        base64_encode(b->buff, b->len, out);
        fwrite(out, 1, 4, b->fp);
    }

    fclose(b->fp);
    free(b);
    return 0;
}

/*---------------------------------------------------------------------------*/

FILE* base64_fopen(FILE* fp, const char* mode)
{
    struct base64_buffer* cookie;

    cookie = calloc(1, sizeof(*cookie));
    cookie->fp = fp;

    if ('w' == mode[0]) {
        cookie->open_mode = 1;
        return funopen(cookie, NULL, base64_writefn, NULL, base64_closefn);
    }
    else {
        cookie->open_mode = 0;
        return funopen(cookie, base64_readfn, NULL, NULL, base64_closefn);
    }
}

/*---------------------------------------------------------------------------*/

void pipe_stream
(
    FILE* fout,
    FILE* fin
)
{
    char buff[1024];
    size_t nmemb;

    for (;;) {
        nmemb = fread(buff, 1, 1024, fin);
        fwrite(buff, 1, nmemb, fout);
        if (nmemb != 1024)
            break;
    }
}