You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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
问题原因与解决方法

问题原因

核心问题是语义动作与规则自动属性传播的冲突:

  1. EndpointGrammar被声明为返回std::string()类型属性,同时start规则又通过语义动作手动将解析结果赋值给endpoint._host
  2. Boost Spirit中,当规则使用%=赋值且上层规则有返回属性时,会自动将子规则的属性向上传播拼接,再加上语义动作的手动赋值,导致主机名被重复写入
  3. 另外tld规则的*( -char_('-') >> +alnum )写法冗余,不符合RFC对TLD的规范逻辑。

解决方法

  1. 修改EndpointGrammar的模板参数,使其不返回任何属性,避免自动属性传播
  2. 简化tld规则,使其符合RFC规范,同时保持与singleName规则逻辑一致
  3. 确保属性传递路径清晰,避免重复赋值

修改后的完整代码

#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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 08:19:55