簡易 multipart/form-data デコーダ

suzume.cgi のフォームからマルチパートで POST できるようにしました。そのために書いたデコーダは簡易版で、ファイル・アップロードに対応していません。 Perl で記述しても長くなるデコーダだけあって、C++11 では 200 行ぐらいに膨らんでいます。

https://github.com/tociyuki/suzume-cgi-cxx11 の src/multipartformdata.cpp

デコーダは、一文字単位でストリームから読み取るループを回して、DFA 状態遷移を繰り返します。先頭のバウンダリ行では固定文字列との比較をするための状態遷移をします。ヘッダではヘッダ一つ分ずつキャプチャするために一文字先読みしながら状態遷移します。本文はキャプチャした末尾が固定文字列と一致するまで状態遷移します。

// マルチパートをデコードします。
bool decode_multipart (
    std::istream& input, std::size_t const content_length,
    std::string const& boundary,
    std::vector<std::wstring>& param)
{
    // ヘッダ一つ分のための DFA 遷移表です。state = 1 から始まります。
    static const int pattern_header[5][5] = {
        {0, 0, 0, 0, 0},
        {0, 0, 0, 0, 2},    /* S1: graph S2                     */
        {0, 3, 0, 2, 2},    /* S2: graph S2 | [ \t] S2 | CR S3  */
        {0, 0, 4, 0, 0},    /* S3: LF S4                        */
        {0,-1,-1, 2,-1}};   /* S4: [ \t] S2 |                   */
    // 先頭のバウンダリ行のパターンです
    std::string pattern0 = "--" + boundary + "\x0d\x0a";
    // マルチパートの途中のバウンダリ行のパターンです
    std::string pattern1 = "\x0d\x0a--" + boundary + "\x0d\x0a";
    // マルチパートの最後のバウンダリ行のパターンです
    std::string pattern2 = "\x0d\x0a--" + boundary + "--\x0d\x0a";
    // major_state は、先頭バンダリ行: 0、ヘッダ: 1、本文: 2、完了: 3
    int major_state = 0;
    // state は個々の major_state ごとに使い方が変化します
    int state = 0;
    // ストリームから読み取った文字数です
    std::size_t count = 0;
    // キャプチャしたオクテットが入ります
    std::string header;
    std::string name;
    std::string body;
    while (major_state < 3 && count < content_length) {
        if (0 == major_state) {
            // 先頭のバウンダリ行のパターンに一致している間、文字を読みつづけます
            int ch = input.get ();
            ++count;
            // major_state の state は pattern0 のインデックスです
            if (! (state < pattern0.size () && pattern0[state] == ch))
                return false;
            if (++state == pattern0.size ()) {
                // 最後の LF を読んだので、ヘッダの読み取りを開始します。
                major_state = 1;
                state = 1;
            }
        }
        else if (1 == major_state) {
            // ヘッダを一文字先読みしながら読み進めます
            int ch = input.peek ();
            // 文字クラス・コードを求めて、state を状態遷移します
            //   CR: 0、LF: 1、[\t ] : 2、[[:graph:]]: 3、その他: 0
            int ccls = (' ' < ch && '\x7f' != ch) ? 4
                     : '\x09' == ch ? 3 : ' ' == ch ? 3
                     : '\x0a' == ch ? 2 : '\x0d' == ch ? 1
                     : 0;
            state = pattern_header[state][ccls];
            if (1 <= state && state <= 4) {
                // ヘッダ 1 つの途中の文字を読み進めてキャプチャします
                header.push_back (ch);
                input.get ();
                ++count;
            }
            else if (state < 0) {
                // ヘッダ 1 つ分のキャプチャが終わりました
                std::string::const_iterator s = header.cbegin ();
                std::string::const_iterator const eos = header.cend ();
                // Content-Disposition ヘッダなら
                if (scan_header_word (s, eos, "content-disposition:")) {
                    // ヘッダ・パラメータに分解します
                    std::vector<std::string> attr;
                    if (! scan_header_word (s, eos, "form-data"))
                        return false;
                    if (! scan_header_parameters (s, eos, attr))
                        return false;
                    for (std::size_t i = 0; i < attr.size (); i += 2)
                        if (attr[i] == "filename")
                            // ファイル・アップロードは禁止します
                            return false;
                        else if (attr[i] == "name") {
                            // name パラメータの値をキャプチャします
                            name = attr[i + 1];
                            break;
                        }
                }
                // 次のヘッダの読み進めを再開します
                header.clear ();
                state = 1;
            }
            else if (0 == state) {
                if (name.empty ())
                    return false;
                // 本文の読み取りを開始します
                major_state = 2;
            }
        }
        else if (2 == major_state) {
            int ch = input.get ();
            ++count;
            // ヘッダと本文の間の CR LF を読み飛ばします
            if (0 == state && '\x0d' != ch)
                return false;
            if (1 == state && '\x0a' != ch)
                return false;
            // 本文とそれに続くバウンダリ行をキャプチャします
            if (++state > 2)
                body.push_back (ch);
            if (matchtail (body, pattern1)) {
                // 本文の末尾が途中のバウンダリ行に一致するときは
                // バウンダリ行を削ります
                body.erase (body.size () - pattern1.size ());
                state = 1;
                major_state = 1;
            }
            else if (matchtail (body, pattern2)) {
                // 本文の末尾が最後のバウンダリ行に一致するときは
                // バウンダリ行を削ります
                body.erase (body.size () - pattern2.size ());
                major_state = 3;
            }
            if (2 != major_state) {
                // 本文が終わったので、名前とボディを param へ追加します。
                param.push_back (decode_utf8 (name));
                param.push_back (decode_utf8 (body));
                body.clear ();
            }
        }
    }
    return 3 == major_state && count == content_length;
}

マルチパートでは、ヘッダを解釈しないといけないので必要な処理が増えます。 なお、このデコーダでは multipart/form-data のヘッダの書式が HTTP/1.1 の RFC 7230、 RFC 7231 にしたがっているものと勝手に決めつけています。

scan_header_word は、大文字・小文字の区別をつけずに文字列を比較して一致しているときは、イテレータを一致した次の文字の位置へ進めます。その上で、さらに空白を読み飛ばします。一致するかどうかを調べるパターン文字列は小文字で書くようにしています。

static inline int lowercase (int const c)
{
    return 'A' <= c && c <= 'Z' ? c + ('a' - 'A') : c;
}

static bool scan_header_word (
    std::string::const_iterator& s0, std::string::const_iterator const eos,
    std::string const& str)
{
    std::string::const_iterator s = s0;
    for (char c : str) {
        if (s >= eos || lowercase(*s) != c)
            return false;
        ++s;
    }
    while (s < eos && *s <= ' ')
        ++s;
    s0 = s;
    return true;
}

scan_header_parameters は、 RFC 7231 の Content-Type 形式の最初のセミコロン以降のパラメータを読み取ります。

static bool scan_header_parameters (
    std::string::const_iterator s,
    std::string::const_iterator const eos,
    std::vector<std::string>& param)
{
    for (;;) {
        std::string name;
        std::string value;
        // 空白* ';' 空白* を読みます
        while (s < eos && *s <= ' ')
            ++s;
        if (! (s < eos && ';' == *s))
            break;
        ++s;
        while (s < eos && *s <= ' ')
            ++s;
        // token を読んで、小文字にしてパラメータの名前欄にキャプチャします
        while (s < eos && istchar (static_cast<unsigned char> (*s)))
            name.push_back (lowercase (*s++));
        // '=' を読みます
        if (name.empty () || ! (s < eos && '=' == *s))
            return false;
        ++s;
        if (s < eos && '"' == *s) {
            // '"' qchar* '"' を読んで値欄にキャプチャします
            ++s;
            for (;;) {
                if (! (s < eos && ('\t' == *s || ' ' <= *s)))
                    return false;
                int ch = *s++;
                if ('"' == ch)
                    break;
                if ('\\' == ch) {
                    // エスケープされた文字を読み取ります
                    if (! (s < eos && ' ' <= *s))
                        return false;
                    ch = *s++;
                }
                value.push_back (ch);
            }
        }
        else if (s < eos && istchar (static_cast<unsigned char> (*s))) {
            // token を読んで、パラメータの値欄にキャプチャします
            while (s < eos && istchar (static_cast<unsigned char> (*s)))
                value.push_back (*s++);
        }
        else
            return false;
        // 名前と値をセットします。
        param.push_back (name);
        param.push_back (value);
    }
    return s == eos;
}

istchar は RFC 7230 の token を構成する文字クラスに含まれて入れば真を返します。一致する文字が多いので、判定にビットマップ集合を使っています。

static inline bool istchar (int const c)
{
    // [!#$%&'*+\-.^_`|~0-9A-Za-z]
    static const unsigned long bitmap[8] = {
        0, 0x5f36ffc0UL, 0x7fffffe3UL, 0xffffffeaUL, 0, 0, 0, 0};
    if (c < 0 || c > 255)
        return false;
    // ビットマップには MSB から LSB の順にビットパックしてあります
    int const sft = 31 - (c & 31);
    return ((bitmap[c >> 5] >> sft) & 1) != 0;
}

lowercase は 7 ビット ASCII の大文字を小文字へ、 matchtail は、文字列 s の末尾が文字列 t に一致するかどうかを調べます。

static inline int lowercase (int const c)
{
    return 'A' <= c && c <= 'Z' ? c + ('a' - 'A') : c;
}

static inline bool matchtail (std::string const &s, std::string const &t)
{
    return s.size () >= t.size ()
            && s.compare (s.size () - t.size (), t.size (), t) == 0;
}