RFC 7230 HTTP/1.1 の Content-Length

Content-Length ヘッダは、 10進数の数字列 1 個だけを値に持つべきなのですが、 RFC 7230 では特殊な場合として、 コンマで区切った同じ数値を表す複数の数字列が並んでいても良いということにしてあります。 これは Content-Length ヘッダが複数ある場合を一つにまとめたときに相当します。 このような場合、 まともなサーバなら、 数値を 1 つにまとめて、 ヘッダも 1 つにまとめて、 プロキシなら次の中継先へ、 CGI 等ならゲートウェイへと送り込むように作るべきです。 ただ、 すべてがまともなサーバであるとは限らないので、 念のために、 RFC 7230 に適合しているかどうかを調べてみることにします。

同時に大きすぎる値をはじくために、 リミットチェックもおこなうことにしましょう。 戻り値が真のときは、 first と second メンバに最初の数字列の先頭と末尾の添字をセットし、 戻り値が偽のときは、 status メンバへエラーに対応する HTTP/1.1 ステータス・コードを入れます。

struct content_length_validator_type {
    enum { OK = 200, BAD_REQUEST = 400, LENGTH_REQUIRED = 411, PAYLOAD_TOO_LARGE = 413 };
    int status;
    std::size_t first, second;
    bool check (std::string const& str, std::size_t limit);
};

まず、 構文として正しいかどうかをチェックします。 CGI で受け取った CONTENT_LENGTH のチェックに使うことも想定して、 空のときは 411 を返します。 空でないときは、 最低でも 1 個の 10 進数の数字列になっていることを調べます。 数値 2 個以上をコンマで区切って含んでいるときは、 数値としてすべて同じであることを調べます。 構文検査は、 ダブル配列な決定性状態遷移機械でおこないます。

//    content-type = [ *( "," OWS ) +DIGIT *( OWS "," [ OWS +DIGIT ] ) ]
//    DIGIT        = "0" / "1" / "2" / "3" / "4" / "5" / "6" / "7" / "8" / "9"
//    OWS          = *( SP / HTAB )
bool
content_length_validator_type::check (std::string const& str, std::size_t limit)
{
    static const char CODE[] =
        "@@@@@@@@@B@@@@@@@@@@@@@@@@@@@@@@B@@@@@@@@@@@C@@@DDDDDDDDDD@@@@@@";
    static const char BASE[] = {0-1, 4-2, 7-1, 11-1, 14-1};
    static const unsigned RULE[] = {
        0x12, 0x32, 0x32, 0x42, 0x33, 0x33, 0x43, 0x14, 0x54, 0x64, 0x44,
        0x15, 0x55, 0x65, 0x16, 0x66, 0x66, 0x46,
    };
    static const std::size_t NRULE = sizeof (RULE) / sizeof (RULE[0]);
    first = second = 0;
    std::size_t c1 = 0, c2 = 0;
    int next_state = 2;
    for (std::size_t sp = 0; next_state > 1 && sp <= str.size (); ++sp) {
        int const ch = sp == str.size () ? 0 : static_cast<unsigned char> (str[sp]);
        int const code = sp == str.size () ? 1 : ch < 64 ? CODE[ch] - '@' : 0;
        int const state = next_state;
        int const i = BASE[state - 2] + code;
        unsigned int rule = 0 <= i && i < NRULE ? RULE[i] : 0;
        next_state = (rule & 0x0fU) == state ? ((rule & 0xf0U) >> 4) : 0;
        if (2 == state && 1 == next_state) {
            status = LENGTH_REQUIRED;
            return false;
        }
        else if (4 != state && 4 == next_state) {
            c1 = sp;
        }
        else if (4 == state && 4 != next_state) {
            c2 = sp;
            if (0 == second)
                first = c1, second = c2;
            else if (digits_compare (str, first, second - first, str, c1, c2 - c1) != 0)
                next_state = 0;
        }
    }
    if (1 != next_state) {
        status = BAD_REQUEST;
        return false;
    }
    std::string strlimit = std::to_string (limit);
    if (digits_compare (str, first, second - first, strlimit, 0, strlimit.size ()) > 0) {
        status = PAYLOAD_TOO_LARGE;
        return false;
    }
    status = OK;
    return true;
}

数値のチェックでは、 途方もなく大きな数千桁の数字列を送られてきても確実に大小比較をおこなうために、 10 進文字列同士のままで比較をおこなうことにします。

int
digits_compare (std::string const& str1, std::size_t pos1, std::size_t len1,
                std::string const& str2, std::size_t pos2, std::size_t len2)
{
    while (len1 > 1 && '0' == str1[pos1]) {
        ++pos1;
        --len1;
    }
    while (len2 > 1 && '0' == str2[pos2]) {
        ++pos2;
        --len2;
    }
    if (len1 < len2)
        return -1;
    if (len1 > len2)
        return +1;
    return str1.compare (pos1, len1, str2, pos2, len2);   
}