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 имеет первый элемент меньше, чем второй элемент.

Другие вопросы по тегам