インクリメンタル HTTP チャンク・デコーダ

チャンク・デコーダに、 リクエスト・ライン & リクエスト・ヘッダ解析器を応用しました。 使い方は同じで 1 オクテットごと put していき、 put の結果が偽でなおかつデコーダの状態が good になったときに、 全チャンクのデコードが正常に終わったことになります。 デコード結果は引数 body に追加していきます。 body に貯めたままでなくても、 例えば、長くなりすぎたらデコード途中で適宜 body を一時ファイルに出力して body をクリアしてもかまいません。 チャンクから読み込んだ本文のオクテット長は、 content_length に数えあげていきます。

ところで chunk の最大サイズは RFC 7230 では値が定めてありません。 実際の運用上は制限しないとまずいため、このデコーダは、デフォルトで 1 メガバイトにサイズを制限しています。 デコーダは body のオクテット長への制限チェックをおこないません。 content_length をチェックしながら、 put を呼ぶ側で長さチェックをして、 必要に応じて 400 番台のエラー・レスポンスを返す使い方をします。

#include <string>

class http_chunk_decoder_type {
public:
    enum {CHUNK_SIZE_LIMIT = 1024*1024};
    int chunk_size_limit;
    std::size_t content_length;
    int chunk_size;
    http_chunk_decoder_type ();
    bool put (int c, std::string& body);
    void clear ();
    bool good () const { return 28 == next_state; }
    bool bad () const { return 0 == next_state; }
    bool partial () const { return 1 <= next_state && next_state <= 27; }

private:
    int next_state;
};

http_chunk_decoder_type::http_chunk_decoder_type ()
: chunk_size_limit (CHUNK_SIZE_LIMIT), content_length (0),
  chunk_size (0), next_state (1) {}

void
http_chunk_decoder_type::clear ()
{
    content_length = 0;
    chunk_size = 0;
    next_state = 1;
}

chunk のデコードも決定性有限オートマトンの状態遷移表を使って進めていきます。

2015 年 7 月 10 日改訂: RFC 7230 の ABNF に近づけました。 ただし、 RFC 7230 とは異なり、 token に 0x80 以上の符号の文字が含まれていると、無視せずにエラーにします。 遷移表の最初のカラムはアクションを表すビットマップで、1 で受理、 2 で chunk_size の計算、 4 で chunk_size のクリア、 8 は chunk_body のために文字クラスの番号を 12 へ上書きします。 このコードは、 オリジン・サーバで使うことを前提にしており、 リクエストの chunk には chunk-ext と trailer-part がつくことはないはずなので、 それらがあっても読み飛ばして無視しています。

chunk の状態遷移は、 状態 S1 から S3 へ移り、 S3 で 16 進数表記のサイズを求めます。 chunk-ext があるときは S4 へ遷移します。 S4 と S5 で token を読み、 S6 から S9 で省略可能な値欄を読みます。chunk-size の行末は S11 へと遷移して CRLF を読み、 S12 で chunk-body を buf へ読み取ります。 chunk-body の最後の CRLF を S13 で読んで、 S1 に戻ります。

last-chunk の状態遷移は、 状態 S1 から S2 を経由して chunk-ext があるときは S14 へ移ります。ないときは S21 へ飛びます。 S14 から S20 で chunk-ext を読み飛ばし、行末を S21 で読んだ後に、 trailer-part があるときは S22 から S26 で読み飛ばします。 chunked ペイロード・ボディの最後の CRLF を状態 S27 で読んで遷移が完了します。

static int
tocclass (int c)
{
    // 1:CR 2:LF 3:TAB 4:SP 5:':' 6:'=' 7:'"' 8:'\\' 9:';' 10:','
    // 11:'0' 12:HEXDIGIT 13:tchar 14:vchar
    static int CCLASS[128] = {
        // 略 (リクエスト・ヘッダ解析器と同じ)
    };
    return (0 <= c && c <= 127) ? CCLASS[c] : c > 127 ? 14 : 0; 
}

bool
http_chunk_decoder_type::put (int const c, std::string& body)
{
    static const int SHIFT[29][15] = {
        // CR  LF TAB  SP   :   =   "   \   ;   ,   0 HEX tch VCH
        {0, 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0},
        {4, 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  2,  3,  0,  0}, // S1: '0' S2 | HEX S3
        {0,21,  0,  0,  0,  0,  0,  0,  0, 14,  0,  2,  3,  0,  0}, // S2: CR S21 | ';' S14 | '0' S2 | HEX S3
        {2,11,  0,  0,  0,  0,  0,  0,  0,  4,  0,  3,  3,  0,  0}, // S3: CR S11 | ';' S4 | '0' S3 | HEX S3
        {0, 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  5,  5,  5,  0}, // S4: tchar S5
        {0,11,  0,  0,  0,  0,  6,  0,  0,  4,  0,  5,  5,  5,  0}, // S5: CR S11 | '=' S6 | ';' S4 | tchar S5
        {0, 0,  0,  0,  0,  0,  0,  8,  0,  0,  0,  7,  7,  7,  0}, // S6: '"' S8 | tchar S7
        {0,11,  0,  0,  0,  0,  0,  0,  0,  4,  0,  7,  7,  7,  0}, // S7: CR S11 | ';' S4 | tchar S7
        {0, 0,  0,  8,  8,  8,  8, 10,  9,  8,  8,  8,  8,  8,  8}, // S8: TAB S8 | SP S8 | '"' S10 | '\\' S9 | VCHAR S8
        {0, 0,  0,  8,  8,  8,  8,  8,  8,  8,  8,  8,  8,  8,  8}, // S9: TAB S8 | SP S8 | VCHAR S8
        {0,11,  0,  0,  0,  0,  0,  0,  0,  4,  0,  0,  0,  0,  0}, // S10: CR S11 | ';' S4
        {0, 0, 12,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0}, // S11: LF S12
        {8,13, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12}, // S12: OCTET{chunk-size} CR S13
        {0, 0,  1,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0}, // S13: LF S1
        {0, 0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 15, 15, 15,  0}, // S14: tchar S15
        {0,21,  0,  0,  0,  0, 16,  0,  0, 14,  0, 15, 15, 15,  0}, // S15: CR S21 | '=' S16 | ';' S14 | tchar S15
        {0, 0,  0,  0,  0,  0,  0, 18,  0,  0,  0, 17, 17, 17,  0}, // S16: '"' S18 | tchar S17
        {0,21,  0,  0,  0,  0,  0,  0,  0, 14,  0, 17, 17, 17,  0}, // S17: CR S21 | ';' S14 | tchar S17
        {0, 0,  0, 18, 18, 18, 18, 20, 19, 18, 18, 18, 18, 18, 18}, // S18: TAB S18 | SP S18 | '"' S20 | '\\' S19 | VCHAR S18
        {0, 0,  0, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18}, // S19: TAB S18 | SP S18 | VCHAR S18
        {0,21,  0,  0,  0,  0,  0,  0,  0, 14,  0,  0,  0,  0,  0}, // S20: CR S21 | ';' S14
        {0, 0, 22,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0}, // S21: LF S22
        {0,27,  0,  0,  0,  0,  0,  0,  0,  0,  0, 23, 23, 23,  0}, // S22: CR S27 | tchar S23
        {0,27,  0,  0,  0, 24,  0,  0,  0,  0,  0, 23, 23, 23,  0}, // S23: ':' S24 | tchar S23
        {0,25,  0, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24}, // S24: CR S25 | TAB S24 | SP S24 | VCHAR S24
        {0, 0, 26,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0}, // S25: LF S26
        {0,27,  0, 24, 24,  0,  0,  0,  0,  0,  0, 23, 23, 23,  0}, // S26: CR S27 | TAB S24 | SP S24 | tchar S23
        {0, 0, 28,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0}, // S27: LF S28
        {1, 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0}, // S28: MATCH
    };
    if (! partial ())
        return false;
    int prev_state = next_state;
    if (4 & SHIFT[prev_state][0])
        chunk_size = 0;
    int cls = tocclass (c);
    if ((8 & SHIFT[prev_state][0]) && --chunk_size >= 0)
        cls = 14;
    next_state = 0 == cls ? 0 : SHIFT[prev_state][cls];
    if (0 == next_state)
        return false;
    if (2 & SHIFT[next_state][0]) {
        if (chunk_size_limit <= chunk_size) {
            next_state = 0;
            return false;
        }
        chunk_size = chunk_size * 16
            + (c >= 'a' ? c - 'a' + 10 : c >= 'A' ? c - 'A' + 10 : c - '0');
    }
    if ((8 & SHIFT[prev_state][0]) && (8 & SHIFT[next_state][0])) {
        body.push_back (c);
        ++content_length;
    }
    return partial ();
}

// License: The BSD 3-Clause
//
// Copyright (c) 2015, MIZUTANI Tociyuki
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
//  1. Redistributions of source code must retain the above copyright notice,
//     this list of conditions and the following disclaimer.
//  2. Redistributions in binary form must reproduce the above copyright
//     notice, this list of conditions and the following disclaimer in the
//     documentation and/or other materials provided with the distribution.
//  3. Neither the name of the copyright holder nor the names of its
//     contributors may be used to endorse or promote products derived from
//     this software without specific prior written permission.