用增强精神完全解码HTTP标头值

decode http header value fully with boost spirit

本文关键字:HTTP 解码 增强      更新时间:2023-10-16

再次,我发现自己正在寻求Boost Spirit。我再次发现自己被它击败了。

a http标头 value 采用一般形式:

text/html; q=1.0, text/*; q=0.8, image/gif; q=0.6, image/jpeg; q=0.6, image/*; q=0.5, */*; q=0.1

即。value *OWS [; *OWS name *OWS [= *OWS possibly_quoted_value] *OWS [...]] *OWS [ , <another value> ...]

因此,在我看来,此标头解码为:

value[0]: 
  text/html
  params:
    name : q
    value : 1.0
value[1]:
  text/*
  params:
    name : q
    value : 0.8
...

等等。

我可以肯定,对于任何知道如何的人来说

我谦虚地寻求您的帮助。

例如,以下是解码Content-Type标头的代码的概述,该标题仅限于表单type/subtype的一个值,其中任何数量的参数 <sp> ; <sp> token=token|quoted_string

template<class Iter>
void parse(ContentType& ct, Iter first, Iter last)
{
    ct.mutable_type()->append(to_lower(consume_token(first, last)));
    consume_lit(first, last, '/');
    ct.mutable_subtype()->append(to_lower(consume_token(first, last)));
    while (first != last) {
        skipwhite(first, last);
        if (consume_char_if(first, last, ';'))
        {
            auto p = ct.add_parameters();
            skipwhite(first, last);
            p->set_name(to_lower(consume_token(first, last)));
            skipwhite(first, last);
            if (consume_char_if(first, last, '='))
            {
                skipwhite(first, last);
                p->set_value(consume_token_or_quoted(first, last));
            }
            else {
                // no value on this parameter
            }
        }
        else if (consume_char_if(first, last, ','))
        {
            // normally we should get the next value-token here but in the case of Content-Type
            // we must barf
            throw std::runtime_error("invalid use of ; in Content-Type");
        }
    }
}
ContentType& populate(ContentType& ct, const std::string& header_value)
{
    parse(ct, header_value.begin(), header_value.end());
    return ct;
}

好吧,经过24小时英雄的挣扎(嗯,不是真的 - 更像是一遍又一遍地阅读手册...),我发现 a 效的方式。

我绝不是boost::spirit胜任。如果外面的人可以改善此答案,请发布。

这款精神状态机采用标头的值(带有一个,可选的参数化,值),并将其变成content_type结构。

我对HTTP标准的业余读数表明某些标题具有该表格(这里的空间表示任何数量的空白,值可能是否引用:

Header-Name: tokena/tokenb [; param1 = "value" [; param2 = value]...]

其他人的形式更为一般:

Header-Name: token [; param1 = "value"[; param2 = value]...] [ , token ...]

此代码涵盖了第一种情况 - 即HTTP Content-Type标头值。我需要将其扩展到适合Accept标头(可以用参数宣传多个值) -

将会出现。

所以这是代码。请一定要告诉我如何改进它!

#define BOOST_SPIRIT_DEBUG
#include <gtest/gtest.h>
#include <boost/spirit/include/qi.hpp>
#include <boost/config/warning_disable.hpp>
#include <boost/spirit/include/qi.hpp>
#include <boost/spirit/include/qi_char.hpp>
#include <boost/spirit/include/phoenix_core.hpp>
#include <boost/spirit/include/phoenix_operator.hpp>
#include <boost/spirit/include/phoenix_fusion.hpp>
#include <boost/spirit/include/phoenix_stl.hpp>
#include <boost/fusion/include/adapt_struct.hpp>
#include <boost/fusion/include/std_pair.hpp>
#include <utility>
#include <vector>
#include <string>
#include <boost/variant.hpp>
namespace qi = boost::spirit::qi;
namespace ascii = boost::spirit::ascii;
using unary_parameter = std::string;
struct binary_parameter
{
    std::string name;
    std::string value;
};
BOOST_FUSION_ADAPT_STRUCT(binary_parameter,
                          (std::string, name)
                          (std::string, value))
using parameter = boost::variant<unary_parameter, binary_parameter>;
struct type_subtype
{
    std::string type;
    std::string subtype;
};
BOOST_FUSION_ADAPT_STRUCT(type_subtype,
                          (std::string, type)
                          (std::string, subtype))
using content_type_pair = std::pair<std::string, std::string>;
struct content_type
{
    type_subtype type;
    std::vector<parameter> params;
};
BOOST_FUSION_ADAPT_STRUCT(content_type,
                          (type_subtype, type)
                          (std::vector<parameter>, params))
template<class Iterator>
struct token_grammar : qi::grammar<Iterator, content_type()>
{
    token_grammar() : token_grammar::base_type(content_type_rule)
    {
        using ascii::char_;
        using qi::omit;
        using qi::eoi;
        CR = char_('r');
        LF = char_('n');
        CRLF = CR >> LF;
        SP = char_(' ');
        HT = char_('t');
        LWS = -CRLF >> +(SP | HT);
        UPALPHA = char_('A', 'Z');
        LOALPHA = char_('a', 'z');
        ALPHA = UPALPHA | LOALPHA;
        DIGIT = char_('0', '9');
        CTL = char_(0, 31) | char_(127);
        QUOT = char_('"');
        TEXT = (char_ - CTL) | HT;
        separator = char_('(') | ')' | '<' | '>' | '@'
        | ',' | ';' | ':' | '' | '"'
        | '/' | '[' | ']' | '?' | '='
        | '{' | '}' | SP | HT;
        end_sequence = separator | space;
        token = +(char_ - separator);
        qdtext = char_ - char_('"') - '';
        quoted_pair = omit[char_('')] >> char_;
        quoted_string = omit[char_('"')] >> *(qdtext | quoted_pair) >> omit[char_('"')];
        value = quoted_string | token ;
        type_subtype_rule = token >> '/' >> token;
        name_only = token;
        nvp = token >> omit[*SP] >> omit['='] >> omit[*SP] >> value;
        any_parameter = omit[*SP] >> omit[char_(';')] >> omit[*SP] >> (nvp | name_only);
        content_type_rule = type_subtype_rule >> *any_parameter;
        BOOST_SPIRIT_DEBUG_NODES((qdtext)(quoted_pair)(quoted_string)(value)(token)(separator));
    }
    qi::rule<Iterator, void()> CR, LF, CRLF, SP, HT, LWS, CTL, QUOT;
    qi::rule<Iterator, char()> UPALPHA, LOALPHA, ALPHA, DIGIT, TEXT, qdtext, quoted_pair;
    qi::rule<Iterator, void()> separator, space, end_sequence;
    qi::rule<Iterator, std::string()> quoted_string, token, value;
    qi::rule<Iterator, type_subtype()> type_subtype_rule;
    qi::rule<Iterator, unary_parameter()> name_only;
    qi::rule<Iterator, binary_parameter()> nvp;
    qi::rule<Iterator, parameter()> any_parameter;
    qi::rule<Iterator, content_type()> content_type_rule;
};
TEST(spirit_test, test1)
{
    token_grammar<std::string::const_iterator> grammar{};
    std::string test = R"__test(application/json )__test";
    content_type ct;
    bool r = qi::parse(test.cbegin(), test.cend(), grammar, ct);
    EXPECT_EQ("application", ct.type.type);
    EXPECT_EQ("json", ct.type.subtype);
    EXPECT_EQ(0, ct.params.size());
    ct = {};
    test = R"__test(text/html ; charset = "ISO-8859-5")__test";
    qi::parse(test.cbegin(), test.cend(), grammar, ct);
    EXPECT_EQ("text", ct.type.type);
    EXPECT_EQ("html", ct.type.subtype);
    ASSERT_EQ(1, ct.params.size());
    ASSERT_EQ(typeid(binary_parameter), ct.params[0].type());
    auto& x = boost::get<binary_parameter>(ct.params[0]);
    EXPECT_EQ("charset", x.name);
    EXPECT_EQ("ISO-8859-5", x.value);
}

我已将代码按照OP发布并进行了评论。

  1. 无需指定void()。实际上,在这种情况下最好使用qi::unused_type,如果没有声明属性类型,则将默认为默认情况。

  2. 如果您不希望公开属性,则无需char_。改用lit

  3. 无需包装rule中的每个炭解析器。这损害了性能。最好将原始表达树未经评估,以便Qi可以更优化解析器的表达式,并且编译器可以更多地内联。

    另外,Qi对属性没有移动语义,因此避免冗余规则消除了在包含规则中被串联的子归因的冗余副本。

    示例替代拼写(谨慎,请参阅将解析器分配到自动变量)

    auto CR   = qi::lit('r');
    auto LF   = qi::lit('n');
    auto CRLF = qi::lit("rn");
    auto HT   = qi::lit('t');
    auto SP   = qi::lit(' ');
    auto LWS  = qi::copy(-CRLF >> +(SP | HT)); // deepcopy
    UPALPHA = char_('A', 'Z');
    LOALPHA = char_('a', 'z');
    ALPHA   = UPALPHA | LOALPHA;
    DIGIT   = char_('0', '9');
    //CTL     = char_(0, 31) | char_(127);
    TEXT    = char_("tx20-x7ex80-xff");
    
  4. 由于您不必使用char_,因此您也没有使用qi::omit[]杀死该属性。

  5. 当您在Qi域表达模板中时,Raw String/Char文字被隐式包裹在qi::lit中,因此您可以简单地简单地

    quoted_pair   = omit[char_('')] >> char_;
    quoted_string = omit[char_('"')] >> *(qdtext | quoted_pair) >> omit[char_('"')];
    

    只有

    quoted_pair   = '' >> char_;
    quoted_string = '"' >> *(qdtext | quoted_pair) >> '"';
    
  6. ,而不是一直用omit[*SP]拼写跳过空间,而只需用船长声明规则即可。现在,您可以简化

    nvp               = token >> omit[*SP] >> omit['='] >> omit[*SP] >> value;
    any_parameter     = omit[*SP] >> omit[char_(';')] >> omit[*SP] >> (nvp | name_only);
    content_type_rule = type_subtype_rule >> *any_parameter;
    

    只有

    nvp               = token >> '=' >> value;
    any_parameter     = ';' >> (nvp | name_only);
    content_type_rule = type_subtype_rule >> qi::skip(spaces)[*any_parameter];
    

    请注意,没有船长声明的规则的任何子记录都是:增强精神船长问题

  7. 有许多冗余/未使用的标题

  8. 最近的编译器 boost版本通过使用decltype
  9. 更简单地使BOOST_FUSION_ADAPT_STRUCTION 更简单

简化的结果噪音要少得多:

//#define BOOST_SPIRIT_DEBUG
#include <boost/spirit/include/qi.hpp>
#include <boost/fusion/include/adapted.hpp>
struct parameter {
    boost::optional<std::string> name;
    std::string value;
};
struct type_subtype {
    std::string type;
    std::string subtype;
};
struct content_type {
    type_subtype type;
    std::vector<parameter> params;
};
BOOST_FUSION_ADAPT_STRUCT(type_subtype, type, subtype)
BOOST_FUSION_ADAPT_STRUCT(content_type, type, params)
template<class Iterator>
struct token_grammar : qi::grammar<Iterator, content_type()>
{
    token_grammar() : token_grammar::base_type(content_type_rule)
    {
        using qi::ascii::char_;
        spaces        = char_(' ');
        token         = +~char_( "()<>@,;:\"/[]?={} t");
        quoted_string = '"' >> *('' >> char_ | ~char_('"')) >> '"';
        value         = quoted_string | token;
        type_subtype_rule = token >> '/' >> token;
        name_only         = token;
        nvp               = token >> '=' >> value;
        any_parameter     = ';' >> (nvp | name_only);
        content_type_rule = type_subtype_rule >> qi::skip(spaces) [*any_parameter];
        BOOST_SPIRIT_DEBUG_NODES((nvp)(any_parameter)(content_type_rule)(quoted_string)(token)(value)(type_subtype_rule))
    }
  private:
    using Skipper = qi::space_type;
    Skipper spaces;
    qi::rule<Iterator, binary_parameter(), Skipper> nvp;
    qi::rule<Iterator, parameter(), Skipper>        any_parameter;
    qi::rule<Iterator, content_type()>              content_type_rule;
    // lexemes
    qi::rule<Iterator, std::string()>               quoted_string, token, value;
    qi::rule<Iterator, type_subtype()>              type_subtype_rule;
    qi::rule<Iterator, unary_parameter()>           name_only;
};

请参阅 Live on Coliru (使用相同的测试用例)

奖金

在这种情况下,我更喜欢更简单的AST。通过使用qi::attr注入一些属性值,您可以避免使用boost ::变体和/或避免boost ::可选:

struct parameter {
    bool have_name;
    std::string name;
    std::string value;
};
struct type_subtype {
    std::string type;
    std::string subtype;
};
struct content_type {
    type_subtype type;
    std::vector<parameter> params;
};
BOOST_FUSION_ADAPT_STRUCT(parameter, have_name, name, value)
BOOST_FUSION_ADAPT_STRUCT(type_subtype, type, subtype)
BOOST_FUSION_ADAPT_STRUCT(content_type, type, params)
namespace qi = boost::spirit::qi;
template<class Iterator>
struct token_grammar : qi::grammar<Iterator, content_type()>
{
    token_grammar() : token_grammar::base_type(content_type_rule)
    {
        using qi::ascii::char_;
        spaces        = char_(' ');
        token         = +~char_( "()<>@,;:\"/[]?={} t");
        quoted_string = '"' >> *('' >> char_ | ~char_('"')) >> '"';
        value         = quoted_string | token;
        type_subtype_rule = token >> '/' >> token;
        name_only         = qi::attr(false) >> qi::attr("") >> token;
        nvp               = qi::attr(true)  >> token >> '=' >> value;
        any_parameter     = ';' >> (nvp | name_only);
        content_type_rule = type_subtype_rule >> qi::skip(spaces) [*any_parameter];
        BOOST_SPIRIT_DEBUG_NODES((nvp)(any_parameter)(content_type_rule)(quoted_string)(token)(value)(type_subtype_rule))
    }
  private:
    using Skipper = qi::space_type;
    Skipper spaces;
    qi::rule<Iterator, parameter(), Skipper> nvp, name_only, any_parameter;
    qi::rule<Iterator, content_type()>       content_type_rule;
    // lexemes
    qi::rule<Iterator, std::string()>        quoted_string, token, value;
    qi::rule<Iterator, type_subtype()>       type_subtype_rule;
};