【问题标题】:decode http header value fully with boost spirit以提升精神完全解码 http 标头值
【发布时间】:2016-06-15 16:14:23
【问题描述】:

再一次,我发现自己正在努力提升精神。我又一次发现自己被它打败了。

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
...

等等。

我敢肯定,对于任何知道怎么做的人来说,boost::spirit::qi 的语法都是微不足道的。

我谦虚地请求您的帮助。

例如,这里是解码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;
}

【问题讨论】:

  • 你能链接 RFC 吗?我不认识那个规范。
  • @sehe 非常感谢。我可能写得不太好...w3.org/Protocols/rfc2616/rfc2616-sec2.html#sec2
  • @sehe 在我看来,关于 HTTP 标头的有趣(实际上非​​常复杂)的事情是,'/' 是一个分隔符,它将值中的值分开。所以实际上 value[0] 本身应该是一个值向量:“text”、“html”以及添加的属性(“q”="1.0")等等。
  • @RichardHodges:在与boost::asio 相关联的示例代码中给出了一个非常简单的http 标头解析器:boost.org/doc/libs/1_61_0/doc/html/boost_asio/example/cpp11/… 但是,它的优点和精神的缺点就是那种如果解析器用完了要解析的文本,则解析器可以“中断”并恢复。 Spirit 不支持“三态”解析,即“好、失败、未完成”。在精神上 AFAIK 如果您用完了要解析的文本,解析器的状态将在返回时丢失。所以,我认为你实际上并不需要精神。
  • 我猜你可能没有将它用于服务器或其他东西,或者你在某种情况下使用它,你将始终拥有完整的标头,或者可以负担得起重新解析?跨度>

标签: c++ boost boost-spirit boost-spirit-qi


【解决方案1】:

好的,经过 24 小时的英勇奋斗(好吧,不是真的 - 更像是一遍又一遍地阅读手册......),我找到了一种行之有效的方法。 p>

我绝对不能胜任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);

}

【讨论】:

    【解决方案2】:

    我已将代码设为posted by OP 并对其进行了审核。

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

    2. 如果您不想公开该属性,则不需要char_。请改用lit

    3. 无需将每个字符解析器包装在rule 中。这会损害性能。最好不要评估原始表达式树,这样 Qi 可以更多地优化解析器表达式,并且编译器可以内联更多。

      此外,Qi 在属性上没有移动语义,因此避免冗余规则可以消除在包含规则中连接的子属性的冗余副本。

      替代拼写示例(注意,请参阅Assigning parsers to auto variables

      auto CR   = qi::lit('\r');
      auto LF   = qi::lit('\n');
      auto CRLF = qi::lit("\r\n");
      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_("\t\x20-\x7e\x80-\xff");
      
    4. 由于您不必使用char_,因此您也不必使用qi::omit[] 杀死属性。

    5. 当您在 Qi 域表达式模板中时,原始字符串/字符文字被隐式包装在 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] 拼出跳过空格,而是用一个skipper 声明规则。现在,您可以简化

      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];
      

      请注意,任何声明的不带船长的规则的子规则调用都是隐式词位Boost spirit skipper issues

    7. 有很多冗余/未使用的标头

    8. 最近的编译器 + 增强版本通过使用 decltype 使 BOOST_FUSION_ADAPT_STRUCT 变得更加简单

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

    //#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 注入一些属性值,您可以avoid using boost::variant 和/或even avoid boost::optional

    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;
    };
    

    【讨论】:

    • 非常感谢。一个问题。唯一不允许使用空格的地方是类型/子类型规则。在这种情况下,是否可以在 content_type 规则中添加船长?我认为外部规则中存在跳过者不会强加给非跳过子规则?
    • 是的。我提到过-带有指向与船长相关的背景答案的链接:)
    • 您需要更加明确,并为我用更简单的术语。我发现规则之间的相互关系有点莫名其妙:)
    • 这是 AST 类型中的一个版本 without using variants 和一个 without even using boost::oprional
    • 哇,新的融合代码有点刺耳……必须查看文档才能了解其实际工作原理。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多