Boost Spirit Qi проверяющий анализатор ввода
У меня есть базовая грамматика Boost Spirit Qi для анализа IP-порта или диапазона IP-портов, т.е. "6322"
или же "6322-6325"
,
Грамматика выглядит так:
template<class It>
void init_port_rule(u16_rule<It>& port)
{
port = boost::spirit::qi::uint_parser<uint16_t, 10, 2, 5>();
port.name("valid port range: (10, 65535)");
}
typedef boost::fusion::vector
< std::uint16_t
, boost::optional<std::uint16_t>
> port_range_type
;
template<class It>
struct port_range_grammar
: boost::spirit::qi::grammar
< It
, port_range_type()
>
{
typedef typename port_range_grammar::base_type::sig_type signature;
port_range_grammar()
: port_range_grammar::base_type(start, "port_range")
{
init_port_rule(port);
using namespace boost::spirit::qi;
start = port > -(lit("-") > port);
}
private:
boost::spirit::qi::rule<It, signature> start;
boost::spirit::qi::rule<It, std::uint16_t()> port;
};
Я немного застрял, чтобы определить, что в диапазоне port1
должно быть меньше чем port2
, Я думаю, что я должен использовать eps
парсер здесь, но, похоже, не нашел правильного способа его указать. Любые предложения очень приветствуются.
2 ответа
Вы действительно можете использовать семантические действия. Вам не всегда нужно прикреплять их к eps
узел, хотя. Вот что вы получите, если сделаете:
port %= uint_parser<uint16_t, 10, 2, 5>() >> eps[ _pass = (_val>=10 && _val<=65535) ];
start = (port >> -('-' >> port)) >> eps(validate(_val));
Обратите внимание, что одно правило использует простую формуeps
с семантическим действием. Это требует operator%=
по- прежнему вызывать автоматическое распространение атрибутов.
Второй экземпляр использует форму семантического предикатаeps
, validate
функция должна быть актером Феникса, я определил его следующим образом:
struct validations {
bool operator()(PortRange const& range) const {
if (range.end)
return range.start<*range.end;
return true;
}
};
boost::phoenix::function<validations> validate;
Более общий / последовательный
Обратите внимание, что вы можете использовать второй стиль правила для обоих правил следующим образом:
port %= uint_parser<Port, 10, 2, 5>() >> eps(validate(_val));
start = (port >> -('-' >> port)) >> eps(validate(_val));
если вы просто добавите перегрузку для проверки одного порта:
struct validations {
bool operator()(Port const& port) const {
return port>=10 && port<=65535;
}
bool operator()(PortRange const& range) const {
if (range.end)
return range.start<*range.end;
return true;
}
};
Первые тесты
Давайте определим некоторые хорошие крайние случаи и протестируем их!
#include <boost/fusion/adapted/struct.hpp>
#include <boost/optional/optional_io.hpp>
#include <boost/spirit/include/qi.hpp>
#include <boost/spirit/include/phoenix.hpp>
namespace qi = boost::spirit::qi;
using Port = std::uint16_t;
struct PortRange {
Port start;
boost::optional<Port> end;
};
BOOST_FUSION_ADAPT_STRUCT(PortRange, start, end)
template <class It, typename Attr = PortRange> struct port_range_grammar : qi::grammar<It, Attr()> {
port_range_grammar() : port_range_grammar::base_type(start, "port_range") {
using namespace qi;
port %= uint_parser<Port, 10, 2, 5>() >> eps(validate(_val));
start = (port >> -('-' >> port)) >> eps(validate(_val));
port.name("valid port range: (10, 65535)");
}
private:
struct validations {
bool operator()(Port const& port) const {
return port>=10 && port<=65535;
}
bool operator()(PortRange const& range) const {
if (range.end)
return range.start<*range.end;
return true;
}
};
boost::phoenix::function<validations> validate;
qi::rule<It, Attr()> start;
qi::rule<It, Port()> port;
};
int main() {
using It = std::string::const_iterator;
port_range_grammar<It> const g;
std::string const valid[] = {"10", "6322", "6322-6325", "65535"};
std::string const invalid[] = {"9", "09", "065535", "65536", "-1", "6325-6322"};
std::cout << " -------- valid cases\n";
for (std::string const input : valid) {
It f=input.begin(), l = input.end();
PortRange range;
bool accepted = parse(f, l, g, range);
if (accepted)
std::cout << "Parsed '" << input << "' to " << boost::fusion::as_vector(range) << "\n";
else
std::cout << "TEST FAILED '" << input << "'\n";
}
std::cout << " -------- invalid cases\n";
for (std::string const input : invalid) {
It f=input.begin(), l = input.end();
PortRange range;
bool accepted = parse(f, l, g, range);
if (accepted)
std::cout << "TEST FAILED '" << input << "' (returned " << boost::fusion::as_vector(range) << ")\n";
}
}
Печать:
-------- valid cases
Parsed '10' to (10 --)
Parsed '6322' to (6322 --)
Parsed '6322-6325' to (6322 6325)
Parsed '65535' to (65535 --)
-------- invalid cases
TEST FAILED '065535' (returned (6553 --))
ПОЗДРАВЛЕНИЯ Мы нашли случай с поломанным краем
Оказывается, ограничивая uint_parser 5 позициями, мы можем оставить символы на входе, так что 065535
разбирает как 6553
(выезд '5'
неанализируемые...). Исправить это просто:
start = (port >> -('-' >> port)) >> eoi >> eps(validate(_val));
Или действительно:
start %= (port >> -('-' >> port)) >> eoi[ _pass = validate(_val) ];
Исправлена версия Live On Coliru
Несколько слов о типе атрибута
Вы заметите, что я изменил ваш тип атрибута. Большая часть этого - "хороший вкус". Обратите внимание, что на практике вы можете представить свой диапазон как однопортовый или диапазон:
using Port = std::uint16_t;
struct PortRange {
Port start, end;
};
using PortOrRange = boost::variant<Port, PortRange>;
Который вы бы потом разобрали как:
port %= uint_parser<Port, 10, 2, 5>() >> eps(validate(_val));
range = (port >> '-' >> port) >> eps(validate(_val));
start = (range | port) >> eoi;
Полная демо-версия Live On Coliru
Вы можете подумать, что это будет неудобно использовать. Согласен!
Упростить вместо
Давай без variant
или же optional
на первом месте. Давайте сделаем один порт только диапазон, который имеет start==end
:
using Port = std::uint16_t;
struct PortRange {
Port start, end;
};
Разобрать это как:
start = port >> -('-' >> port | attr(0)) >> eoi >> eps(validate(_val));
Все, что мы делаем в validate
это проверить, end
является 0
:
bool operator()(PortRange& range) const {
if (range.end == 0)
range.end = range.start;
return range.start <= range.end;
}
А теперь вывод: Live On Coliru
-------- valid cases
Parsed '10' to (10-10)
Parsed '6322' to (6322-6322)
Parsed '6322-6325' to (6322-6325)
Parsed '65535' to (65535-65535)
-------- invalid cases
Обратите внимание, как вы теперь можете всегда перечислять start
..end
не зная, был ли порт или диапазон портов. Это может быть удобно (немного зависит от логики, которую вы реализуете).
Хорошо, я думаю, что я понял это...
port_range_grammar()
: port_range_grammar::base_type(start, "port_range")
{
init_port_rule(port);
using namespace boost::spirit::qi;
namespace pnx = boost::phoenix;
namespace fus = boost::fusion;
start = port > -(lit("-") > port)
> eps( pnx::bind
( [](auto const& parsed)
{
if(!fus::at_c<1>(parsed).is_initialized())
return true;
auto lhs = fus::at_c<0>(parsed);
auto rhs = *fus::at_c<1>(parsed);
return lhs < rhs;
}
, _val
)
)
;
}
Идея состоит в том, чтобы передать проанализированное значение eps
Парсер, который собирается проверить, если построен port_range_type
имеет первый элемент меньше, чем второй элемент.