Boost Spirit QI语法解析主机名时末尾部分重复输出问题排查
问题描述
我正在调试用于解析host:port格式endpoint的Boost QI语法,其中host可以是主机名、IPv4或IPv6地址。目前端口、IPv4及IPv6部分解析正常,但主机名部分存在问题:虽然能按RFC规则正确识别合法与非法主机名,但解析结果中主机名的末尾部分会重复输出,例如"foo"解析为"foofoo","foo.net"解析为"foo.netnet"。
以下是简化后的可运行代码(基于Wandbox,C++17、Boost 1.79.0):
#include <iostream> #include <string> #include <boost/fusion/include/std_pair.hpp> #include <boost/fusion/include/boost_array.hpp> #include <boost/phoenix.hpp> #include <boost/spirit/include/qi.hpp> #include <boost/algorithm/string.hpp> class Endpoint { public: Endpoint(): _host(""), _port(0) {}; std::string _host; unsigned short _port; }; template < typename ITERATOR > struct HostnameGrammar : boost::spirit::qi::grammar< ITERATOR, std::string() > { HostnameGrammar() : HostnameGrammar::base_type( start ) { using boost::spirit::qi::alnum; using boost::spirit::qi::alpha; using boost::spirit::qi::char_; using boost::spirit::_1; start %= dottedName | singleName ; dottedName %= +( singleName >> char_( '.' ) ) >> tld ; singleName %= +alnum >> *( char_('-') >> +alnum ) ; tld %= +alpha >> *( -char_('-') >> +alnum ) ; } boost::spirit::qi::rule< ITERATOR, std::string() > start; boost::spirit::qi::rule< ITERATOR, std::string() > dottedName; boost::spirit::qi::rule< ITERATOR, std::string() > singleName; boost::spirit::qi::rule< ITERATOR, std::string() > tld; }; template < typename ITERATOR > struct EndpointGrammar : boost::spirit::qi::grammar< ITERATOR, std::string() > { EndpointGrammar( Endpoint & endpoint ) : EndpointGrammar::base_type( start ), endpoint_( endpoint ) { using boost::spirit::qi::ushort_; using boost::spirit::_1; start = -address[ boost::phoenix::ref( endpoint._host ) = _1 ] >> -( ':' >> ushort_[ boost::phoenix::ref( endpoint._port ) = _1 ] ) ; address %= hostname; } Endpoint & endpoint_; boost::spirit::qi::rule< ITERATOR, std::string() > start; boost::spirit::qi::rule< ITERATOR, std::string() > address; HostnameGrammar< ITERATOR > hostname; }; int main() { std::vector< std::string > endpointStrings { // 预期解析成功的用例 "0foo", "foo", "foo.net", "foo:1234", "foo.net:5678", "foo.example.net", "foo.example.net:9012", "foo-bar", "foo-bar.com", "foo-bar:1234", "foo-bar.net-0:5678", "foo-bar.example.com-1:9012", // 预期解析失败的用例 "foo.0bar", "foo.0bar:1234", "foo.bar-", "foo.bar-:1234", "foo-", "-foo" }; for ( auto const & endpointString : endpointStrings ) { Endpoint tempEndpoint; std::string::const_iterator beginIt( endpointString.begin() ); std::string::const_iterator endIt( endpointString.end() ); EndpointGrammar< std::string::const_iterator > grammar( tempEndpoint ); if ( !boost::spirit::qi::parse( beginIt, endIt, grammar ) || beginIt != endIt ) { std::cout << "Failed: " << endpointString << std::endl; } else { std::cout << "Succeeded: " << endpointString << " = " << tempEndpoint._host << " / " << tempEndpoint._port << std::endl; } } return 0; }
运行输出示例:
Succeeded: 0foo = 0foo0foo / 0 Succeeded: foo = foofoo / 0 Succeeded: foo.net = foo.netnet / 0 Succeeded: foo:1234 = foofoo / 1234 Succeeded: foo.net:5678 = foo.netnet / 5678 Succeeded: foo.example.net = foo.example.netnet / 0 Succeeded: foo.example.net:9012 = foo.example.netnet / 9012 Succeeded: foo-bar = foo-barfoo-bar / 0 Succeeded: foo-bar.com = foo-bar.comcom / 0 Succeeded: foo-bar:1234 = foo-barfoo-bar / 1234 Succeeded: foo-bar.net-0:5678 = foo-bar.net-0net-0 / 5678 Succeeded: foo-bar.example.com-1:9012 = foo-bar.example.com-1com-1 / 9012 Failed: foo.0bar Failed: foo.0bar:1234 Failed: foo.bar- Failed: foo.bar-:1234 Failed: foo- Failed: -foo
问题原因与解决方法
问题原因
核心问题是语义动作与规则自动属性传播的冲突:
EndpointGrammar被声明为返回std::string()类型属性,同时start规则又通过语义动作手动将解析结果赋值给endpoint._host- Boost Spirit中,当规则使用
%=赋值且上层规则有返回属性时,会自动将子规则的属性向上传播拼接,再加上语义动作的手动赋值,导致主机名被重复写入 - 另外
tld规则的*( -char_('-') >> +alnum )写法冗余,不符合RFC对TLD的规范逻辑。
解决方法
- 修改
EndpointGrammar的模板参数,使其不返回任何属性,避免自动属性传播 - 简化
tld规则,使其符合RFC规范,同时保持与singleName规则逻辑一致 - 确保属性传递路径清晰,避免重复赋值
修改后的完整代码
#include <iostream> #include <string> #include <boost/fusion/include/std_pair.hpp> #include <boost/fusion/include/boost_array.hpp> #include <boost/phoenix.hpp> #include <boost/spirit/include/qi.hpp> #include <boost/algorithm/string.hpp> class Endpoint { public: Endpoint(): _host(""), _port(0) {}; std::string _host; unsigned short _port; }; template < typename ITERATOR > struct HostnameGrammar : boost::spirit::qi::grammar< ITERATOR, std::string() > { HostnameGrammar() : HostnameGrammar::base_type( start ) { using boost::spirit::qi::alnum; using boost::spirit::qi::alpha; using boost::spirit::qi::char_; start %= dottedName | singleName; dottedName %= +( singleName >> char_('.') ) >> tld; singleName %= +alnum >> *( char_('-') >> +alnum ); // 简化TLD规则:字母开头,可含连字符,结尾为字母数字,符合RFC规范 tld %= alpha >> *( (alnum | char_('-')) ) >> alnum; } boost::spirit::qi::rule< ITERATOR, std::string() > start; boost::spirit::qi::rule< ITERATOR, std::string() > dottedName; boost::spirit::qi::rule< ITERATOR, std::string() > singleName; boost::spirit::qi::rule< ITERATOR, std::string() > tld; }; template < typename ITERATOR > struct EndpointGrammar : // 修改为无返回属性的grammar boost::spirit::qi::grammar< ITERATOR > { EndpointGrammar(Endpoint & endpoint ) : EndpointGrammar::base_type( start ), endpoint_( endpoint ) { using boost::spirit::qi::ushort_; using boost::spirit::_1; start = -address[ boost::phoenix::ref( endpoint._host ) = _1 ] >> -( ':' >> ushort_[ boost::phoenix::ref( endpoint._port ) = _1 ] ); address %= hostname; } Endpoint & endpoint_; // start规则无返回属性 boost::spirit::qi::rule< ITERATOR > start; boost::spirit::qi::rule< ITERATOR, std::string() > address; HostnameGrammar< ITERATOR > hostname; }; int main() { std::vector< std::string > endpointStrings { // 预期解析成功的用例 "0foo", "foo", "foo.net", "foo:1234", "foo.net:5678", "foo.example.net", "foo.example.net:9012", "foo-bar", "foo-bar.com", "foo-bar:1234", "foo-bar.net-0:5678", "foo-bar.example.com-1:9012", // 预期解析失败的用例 "foo.0bar", "foo.0bar:1234", "foo.bar-", "foo.bar-:1234", "foo-", "-foo" }; for ( auto const & endpointString : endpointStrings ) { Endpoint tempEndpoint; std::string::const_iterator beginIt( endpointString.begin() ); std::string::const_iterator endIt( endpointString.end() ); EndpointGrammar< std::string::const_iterator > grammar( tempEndpoint ); if ( !boost::spirit::qi::parse( beginIt, endIt, grammar ) || beginIt != endIt ) { std::cout << "Failed: " << endpointString << std::endl; } else { std::cout << "Succeeded: " << endpointString << " = " << tempEndpoint._host << " / " << tempEndpoint._port << std::endl; } } return 0; }
修改后的运行输出
Succeeded: 0foo = 0foo / 0 Succeeded: foo = foo / 0 Succeeded: foo.net = foo.net / 0 Succeeded: foo:1234 = foo / 1234 Succeeded: foo.net:5678 = foo.net / 5678 Succeeded: foo.example.net = foo.example.net / 0 Succeeded: foo.example.net:9012 = foo.example.net / 9012 Succeeded: foo-bar = foo-bar / 0 Succeeded: foo-bar.com = foo-bar.com / 0 Succeeded: foo-bar:1234 = foo-bar / 1234 Succeeded: foo-bar.net-0:5678 = foo-bar.net-0 / 5678 Succeeded: foo-bar.example.com-1:9012 = foo-bar.example.com-1 / 9012 Failed: foo.0bar Failed: foo.0bar:1234 Failed: foo.bar- Failed: foo.bar-:1234 Failed: foo- Failed: -foo
额外说明
- 当使用语义动作手动处理属性时,应避免让规则同时自动返回属性,否则容易出现重复赋值问题
- 修改后的TLD规则严格符合RFC 1123和RFC 952规范:主机名的每个标签必须以字母数字开头和结尾,中间可包含连字符
- 后续若需添加IPv4/IPv6解析逻辑,只需在
address规则中新增对应的分支即可,不会影响现有主机名解析逻辑
内容的提问来源于stack exchange,提问作者Nick Williams
相关产品推荐
相关产品推荐

