Browse Source

[master] Merged trac4088 (client classification expression parser)

Francis Dupont 9 years ago
parent
commit
7058a4085d

+ 48 - 0
configure.ac

@@ -1212,6 +1212,49 @@ AC_SUBST(PERL)
 AC_PATH_PROGS(AWK, gawk awk)
 AC_SUBST(AWK)
 
+AC_ARG_ENABLE(generate_parser, [AC_HELP_STRING([--enable-generate-parser],
+  [indicates that the parsers will be regenerated. This implies that the
+   bison and flex are required [default=no]])],
+   enable_generate_parser=$enableval, enable_generate_parser=no)
+
+# Check if flex is avaible. Flex is not needed for building Kea sources,
+# unless you want to regenerate grammar in src/lib/eval
+AC_PROG_LEX
+
+# Check if bison is available. Bison is not needed for building Kea sources,
+# unless you want to regenerate grammar in src/lib/eval
+AC_PROG_YACC
+
+if test "x$enable_generate_parser" != xno; then
+
+    if test "x$LEX" == "x"; then
+       AC_MSG_ERROR("Flex is required for enable-generate-parser, but was not found")
+    fi
+
+    if test "x$YACC" == "x"; then
+       AC_MSG_ERROR("Bison it required for enable-generate-parser, but was not found")
+    fi
+
+# Ok, let's check if we have at least 3.0.0 version of the bison. The code used
+# to generate src/lib/eval parser is roughly based on bison 3.0 examples.
+   cat > bisontest.y << EOF
+%require "3.0.0"
+%token X
+%%
+%start Y;
+Y: X;
+EOF
+# Try to compile.
+    $YACC bisontest.y -o bisontest.cc
+
+    if test $? -ne 0 ; then
+        $YACC -V
+        $RM -f bisontest.y bisontest.cc
+        AC_MSG_ERROR("Error with $YACC. Possibly incorrect version? Required at least 3.0.0.")
+    fi
+    $RM -f bisontest.y bisontest.cc
+fi
+
 AC_ARG_ENABLE(generate_docs, [AC_HELP_STRING([--enable-generate-docs],
   [regenerate documentation using Docbook [default=no]])],
   enable_generate_docs=$enableval, enable_generate_docs=no)
@@ -1527,6 +1570,10 @@ Log4cplus:
 
 Kea config backend:
   CONFIG_BACKEND:  ${CONFIG_BACKEND}
+
+Flex/bison:
+  FLEX:  ${LEX}
+  BISON: ${YACC}
 END
 
 # Avoid confusion on DNS/DHCP and only mention MySQL if it
@@ -1587,6 +1634,7 @@ Developer:
   C++ Code Coverage: $USE_LCOV
   Logger checks: $enable_logger_checks
   Generate Documentation: $enable_generate_docs
+  Parser Generation: $enable_generate_parser
 
 END
 

+ 32 - 0
src/lib/eval/Makefile.am

@@ -15,6 +15,10 @@ libkea_eval_la_SOURCES  =
 libkea_eval_la_SOURCES += eval_log.cc eval_log.h
 libkea_eval_la_SOURCES += token.cc token.h
 
+libkea_eval_la_SOURCES += parser.cc parser.h
+libkea_eval_la_SOURCES += lexer.cc
+libkea_eval_la_SOURCES += eval_context.cc eval_context.h eval_context_decl.h
+
 nodist_libkea_eval_la_SOURCES = eval_messages.h eval_messages.cc
 
 libkea_eval_la_CXXFLAGS = $(AM_CXXFLAGS)
@@ -48,3 +52,31 @@ s-messages: eval_messages.mes
 BUILT_SOURCES = eval_messages.h eval_messages.cc
 
 CLEANFILES = eval_messages.h eval_messages.cc s-messages
+
+# If we want to get rid of all flex/bison generated files, we need to use
+# make maintainer-clean. The proper way to introduce custom commands for
+# that operation is to define maintainer-clean-local target. However,
+# make maintainer-clean also removes Makefile, so running configure script
+# is required. To make it easy to rebuild flex/bison without going through
+# reconfigure, a new target parser-clean has been added.
+maintainer-clean-local:
+	rm -f location.hh lexer.cc parser.cc parser.h position.hh stack.hh
+
+# To regenerate flex/bison files, one can do:
+#
+# make parser-clean
+# make parser
+#
+# This is needed only when the lexer.ll or parser.yy files are modified.
+# Make sure you have both flex and bison installed.
+parser-clean: maintainer-clean-local
+
+parser: lexer.cc location.hh position.hh stack.hh parser.cc parser.h
+	@echo "Flex/bison files regenerated"
+
+# --- Flex/Bison stuff below --------------------------------------------------
+location.hh position.hh stack.hh parser.cc parser.h: parser.yy
+	$(YACC) --defines=parser.h -o parser.cc parser.yy
+
+lexer.cc: lexer.ll
+	$(LEX) -o lexer.cc lexer.ll

+ 113 - 3
src/lib/eval/eval.dox

@@ -13,8 +13,118 @@
 // PERFORMANCE OF THIS SOFTWARE.
 
 /**
-  @page dhcpEval Expression evaluation (client classification)
+  @page dhcpEval libeval - Expression evaluation and client classification
 
-  @todo: Document how the expression evaluation is implemented.
+  @section dhcpEvalIntroduction Introduction
 
- */
+  The core of the libeval library is a parser that is able to parse an
+  expression (e.g. option[123] == 'APC'). This is currently used for client
+  classification, but in the future may be also used for other applications.
+
+  The external interface to the library is the @ref isc::eval::EvalContext
+  class.  Once instantiated, it offers a major method:
+  @ref isc::eval::EvalContext::parseString, which parses the specified
+  string.  Once the expression is parsed, it is converted to a collection of
+  tokens that are stored in Reverse Polish Notation in
+  EvalContext::expression.
+
+  Internally, the parser code is generated by flex and bison. These two
+  tools convert lexer.ll and parser.yy files into a number of .cc and .hh files.
+  To avoid a build of Kea depending on the presence of flex and bison, the
+  result of the generation is checked into the github repository and is
+  distributed in the tarballs.
+
+  @section dhcpEvalLexer Lexer generation using flex
+
+  Flex is used to generate the lexer, a piece of code that converts input
+  data into a series of tokens. It contains a small number of directives,
+  but the majority of the code consists of the definitions of tokens. These
+  definitions are regular expressions that define various tokens, e.g. strings,
+  numbers, parentheses, etc. Once the expression is matched, the associated
+  action is executed. In the majority of the cases a generator method from
+  @ref isc::eval::EvalParser is called, which returns returns a newly created
+  bison token. The purpose of the lexer is to generate a stream
+  of tokens that are consumed by the parser.
+
+  lexer.cc and lexer.hh must not be edited. If there is a need
+  to introduce changes, lexer.ll must be updated and the .cc and .hh files
+  regenerated.
+
+  @section dhcpEvalParser Parser generation using bison
+
+  Bison is used to generate the parser, a piece of code that consumes a
+  stream of tokens and attempts to match it against a defined grammar.
+  The bison parser is created from parser.yy. It contains
+  a number of directives, but the two most important sections are:
+  a list of tokens (for each token defined here, bison will generate the
+  make_NAMEOFTOKEN method in the @ref isc::eval::EvalParser class) and
+  the grammar. The Grammar is a tree like structure with possible loops.
+
+  Here is an over-simplified version of the grammar:
+
+@code
+01. %start expression;
+02.
+03. expression : token EQUAL token
+04.            | token
+05.            ;
+06.
+07. token : STRING
+08.             {
+09.                 TokenPtr str(new TokenString($1));
+10.                 ctx.expression.push_back(str);
+11.             }
+12.       | HEXSTRING
+13.             {
+14.                 TokenPtr hex(new TokenHexString($1));
+15.                 ctx.expression.push_back(hex);
+16.             }
+17.       | OPTION '[' INTEGER ']'
+18.             {
+19.                 TokenPtr opt(new TokenOption($3));
+20.                 ctx.expression.push_back(opt);
+21.              }
+22.       ;
+@endcode
+
+This code determines that the grammar starts from expression (line 1).
+The actual definition of expression (lines 3-5) may either be a
+single token or an expression "token == token" (EQUAL has been defined as
+"==" elsewhere). Token is further
+defined in lines 7-22: it may either be a string (lines 7-11),
+a hex string (lines 12-16) or option (lines 17-21).
+When the actual case is determined, the respective C++ action
+is executed. For example, if the token is a string, the TokenString class is
+instantiated with the appropriate value and put onto the expression vector.
+
+@section dhcpEvalMakefile Generating parser files
+
+ In the general case, we want to avoid generating parser files, so an
+ average user interested in just compiling Kea would not need flex or
+ bison. Therefore the generated files are already included in the
+ git repository and will be included in the tarball releases.
+
+ However, there will be cases when one of the developers would want
+ to tweak the lexer.ll and parser.yy files and then regenerate
+ the code. For this purpose, two makefile targets are defined:
+ @code
+ make parser
+ @endcode
+ will generate the parsers and
+ @code
+ make parser-clean
+ @endcode
+ will remove the files. Generated files removal was also hooked
+ into the maintainer-clean target.
+
+@section dhcpEvalConfigure Configure options
+
+ Since the flex/bison tools are not necessary for a regular compilation,
+ checks conducted during configure, but the lack of flex or
+ bison tools does not stop the configure process. There is a flag
+ (--enable-generate-parser) that tells configure script that the
+ parser will be generated. With this flag, the checks for flex/bison
+ are mandatory. If either tool is missing or at too early a version, the
+ configure process will terminate with an error.
+
+*/

+ 52 - 0
src/lib/eval/eval_context.cc

@@ -0,0 +1,52 @@
+// Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <eval/eval_context.h>
+#include <eval/parser.h>
+#include <exceptions/exceptions.h>
+#include <fstream>
+
+EvalContext::EvalContext()
+  : trace_scanning_(false), trace_parsing_(false)
+{
+}
+
+EvalContext::~EvalContext()
+{
+}
+
+bool
+EvalContext::parseString(const std::string& str)
+{
+    file_ = "<string>";
+    string_ = str;
+    scanStringBegin();
+    isc::eval::EvalParser parser(*this);
+    parser.set_debug_level(trace_parsing_);
+    int res = parser.parse();
+    scanStringEnd();
+    return (res == 0);
+}
+
+void
+EvalContext::error(const isc::eval::location& loc, const std::string& what)
+{
+    isc_throw(EvalParseError, loc << ": " << what);
+}
+
+void
+EvalContext::error (const std::string& what)
+{
+    isc_throw(EvalParseError, what);
+}

+ 96 - 0
src/lib/eval/eval_context.h

@@ -0,0 +1,96 @@
+// Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef EVAL_CONTEXT_H
+#define EVAL_CONTEXT_H
+#include <string>
+#include <map>
+#include <eval/parser.h>
+#include <eval/eval_context_decl.h>
+#include <exceptions/exceptions.h>
+
+// Tell Flex the lexer's prototype ...
+#define YY_DECL isc::eval::EvalParser::symbol_type yylex (EvalContext& driver)
+
+// ... and declare it for the parser's sake.
+YY_DECL;
+
+namespace isc {
+namespace eval {
+
+/// @brief Evaluation error exception raised when trying to parse an axceptions.
+class EvalParseError : public isc::Exception {
+public:
+    EvalParseError(const char* file, size_t line, const char* what) :
+        isc::Exception(file, line, what) { };
+};
+
+
+/// @brief Evaluation context, an interface to the expression evaluation.
+class EvalContext
+{
+public:
+    /// @brief Default constructor.
+    EvalContext();
+
+    /// @brief destructor
+    virtual ~EvalContext();
+
+    /// @brief Parsed expression (output tokens are stored here)
+    isc::dhcp::Expression expression;
+
+    /// @brief Method called before scanning starts on a string.
+    void scanStringBegin();
+
+    /// @brief Method called after the last tokens are scanned from a string.
+    void scanStringEnd();
+    
+    /// @brief Run the parser on the string specified.
+    ///
+    /// @param str string to be written
+    /// @return true on success.
+    bool parseString(const std::string& str);
+
+    /// @brief The name of the file being parsed.
+    /// Used later to pass the file name to the location tracker.
+    std::string file_;
+
+    /// @brief The string being parsed.
+    std::string string_;
+
+    /// @brief Error handler
+    ///
+    /// @param loc location within the parsed file when experienced a problem.
+    /// @param what string explaining the nature of the error.
+    void error(const isc::eval::location& loc, const std::string& what);
+
+    /// @brief Error handler
+    ///
+    /// This is a simplified error reporting tool for possible future
+    /// cases when the EvalParser is not able to handle the packet.
+    void error(const std::string& what);
+
+ private:
+    /// @brief Flag determining scanner debugging.
+    bool trace_scanning_;
+
+    /// @brief Flag determing parser debugging.
+    bool trace_parsing_;
+  
+};
+
+}; // end of isc::eval namespace
+}; // end of isc namespace
+
+#endif

+ 28 - 0
src/lib/eval/eval_context_decl.h

@@ -0,0 +1,28 @@
+// Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef EVAL_CONTEXT_DECL_H
+#define EVAL_CONTEXT_DECL_H
+
+/// @file eval_context_decl.h Forward declaration of the EvalContext class
+
+namespace isc {
+namespace eval {
+
+class EvalContext;
+
+}; // end of isc::eval namespace
+}; // end of isc namespace
+
+#endif

+ 0 - 5
src/lib/eval/eval_messages.mes

@@ -18,8 +18,3 @@ $NAMESPACE isc::dhcp
 This debug message indicates that the expression has been evaluated
 to said value. This message is mostly useful during debugging of the
 client classification expressions.
-
-% EVAL_SUBSTRING_BAD_PARAM_CONVERSION starting %1, length %2
-This debug message indicates that the parameter for the starting postion
-or length of the substring couldn't be converted to an integer.  In this
-case the substring routine returns an empty string.

File diff suppressed because it is too large
+ 2251 - 0
src/lib/eval/lexer.cc


+ 156 - 0
src/lib/eval/lexer.ll

@@ -0,0 +1,156 @@
+/* Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
+
+   Permission to use, copy, modify, and/or distribute this software for any
+   purpose with or without fee is hereby granted, provided that the above
+   copyright notice and this permission notice appear in all copies.
+
+   THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+   REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+   AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+   INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+   LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+   OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+   PERFORMANCE OF THIS SOFTWARE. */
+
+%{ /* -*- C++ -*- */
+#include <cerrno>
+#include <climits>
+#include <cstdlib>
+#include <string>
+#include <eval/eval_context.h>
+#include <eval/parser.h>
+#include <boost/lexical_cast.hpp>
+
+// Work around an incompatibility in flex (at least versions
+// 2.5.31 through 2.5.33): it generates code that does
+// not conform to C89.  See Debian bug 333231
+// <http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=333231>.
+# undef yywrap
+# define yywrap() 1
+
+// The location of the current token. The lexer will keep updating it. This
+// variable will be useful for logging errors.
+static isc::eval::location loc;
+%}
+
+/* noyywrap disables automatic rewinding for the next file to parse. Since we
+   always parse only a single string, there's no need to do any wraps. And
+   using yywrap requires linking with -lfl, which provides the default yywrap
+   implementation that always returns 1 anyway. */
+%option noyywrap
+
+/* nounput simplifies the lexer, by removing support for putting a character
+   back into the input stream. We never use such capability anyway. */
+%option nounput
+
+/* batch means that we'll never use the generated lexer interactively. */
+%option batch
+
+/* Enables debug mode. To see the debug messages, one needs to also set
+   yy_flex_debug to 1, then the debug messages will be printed on stderr. */
+%option debug
+
+/* I have no idea what this option does, except it was specified in the bison
+   examples and Postgres folks added it to remove gcc 4.3 warnings. Let's
+   be on the safe side and keep it. */
+%option noinput
+
+/* This line tells flex to track the line numbers. It's not really that
+   useful for client classes, which typically are one-liners, but it may be
+   useful in more complex cases. */
+%option yylineno
+
+/* These are not token expressions yet, just convenience expressions that
+   can be used during actual token definitions. */
+int   \-?[0-9]+
+hex   [0-9a-fA-F]+
+blank [ \t]
+
+%{
+// This code run each time a pattern is matched. It updates the location
+// by moving it ahead by yyleng bytes. yyleng specifies the length of the
+// currently matched token.
+#define YY_USER_ACTION  loc.columns(yyleng);
+%}
+
+%%
+
+%{
+    // Code run each time yylex is called.
+    loc.step();
+%}
+
+{blank}+   {
+    // Ok, we found a with space. Let's ignore it and update loc variable.
+    loc.step();
+}
+[\n]+      {
+    // Newline found. Let's update the location and continue.
+    loc.lines(yyleng);
+    loc.step();
+}
+
+\'[^\'\n]*\' {
+    // A string has been matched. It contains the actual string and single quotes.
+    // We need to get those quotes out of the way and just use its content, e.g.
+    // for 'foo' we should get foo
+    std::string tmp(yytext+1);
+    tmp.resize(tmp.size() - 1);
+
+    return isc::eval::EvalParser::make_STRING(tmp, loc);
+}
+
+0[xX]{hex} {
+    // A hex string has been matched. It contains the '0x' or '0X' header
+    // followed by at least one hexadecimal digit.
+    return isc::eval::EvalParser::make_HEXSTRING(yytext, loc);
+}
+
+{int} {
+    // An integer was found.
+    std::string tmp(yytext);
+
+    try {
+        static_cast<void>(boost::lexical_cast<int>(tmp));
+    } catch (const boost::bad_lexical_cast &) {
+        driver.error(loc, "Failed to convert " + tmp + " to an integer.");
+    }
+
+    // The parser needs the string form as double conversion is no lossless
+    return isc::eval::EvalParser::make_INTEGER(tmp, loc);
+}
+
+"=="        return isc::eval::EvalParser::make_EQUAL(loc);
+"option"    return isc::eval::EvalParser::make_OPTION(loc);
+"substring" return isc::eval::EvalParser::make_SUBSTRING(loc);
+"all"       return isc::eval::EvalParser::make_ALL(loc);
+"("         return isc::eval::EvalParser::make_LPAREN(loc);
+")"         return isc::eval::EvalParser::make_RPAREN(loc);
+"["         return isc::eval::EvalParser::make_LBRACKET(loc);
+"]"         return isc::eval::EvalParser::make_RBRACKET(loc);
+","         return isc::eval::EvalParser::make_COMA(loc);
+
+.          driver.error (loc, "Invalid character: " + std::string(yytext));
+<<EOF>>    return isc::eval::EvalParser::make_END(loc);
+%%
+
+using namespace isc::eval;
+
+void
+EvalContext::scanStringBegin()
+{
+    loc.initialize(&file_);
+    yy_flex_debug = trace_scanning_;
+    YY_BUFFER_STATE buffer;
+    buffer = yy_scan_bytes(string_.c_str(), string_.size());
+    if (!buffer) {
+        error("cannot scan string");
+        exit(EXIT_FAILURE);
+    }
+}
+
+void
+EvalContext::scanStringEnd()
+{
+    yy_delete_buffer(YY_CURRENT_BUFFER);
+}

+ 193 - 0
src/lib/eval/location.hh

@@ -0,0 +1,193 @@
+// Generated 2015114
+// A Bison parser, made by GNU Bison 3.0.4.
+
+// Locations for Bison parsers in C++
+
+// Copyright (C) 2002-2015 Free Software Foundation, Inc.
+
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+// As a special exception, you may create a larger work that contains
+// part or all of the Bison parser skeleton and distribute that work
+// under terms of your choice, so long as that work isn't itself a
+// parser generator using the skeleton or a modified version thereof
+// as a parser skeleton.  Alternatively, if you modify or redistribute
+// the parser skeleton itself, you may (at your option) remove this
+// special exception, which will cause the skeleton and the resulting
+// Bison output files to be licensed under the GNU General Public
+// License without this special exception.
+
+// This special exception was added by the Free Software Foundation in
+// version 2.2 of Bison.
+
+/**
+ ** \file location.hh
+ ** Define the isc::eval::location class.
+ */
+
+#ifndef YY_YY_LOCATION_HH_INCLUDED
+# define YY_YY_LOCATION_HH_INCLUDED
+
+# include "position.hh"
+
+#line 21 "parser.yy" // location.cc:337
+namespace isc { namespace eval {
+#line 46 "location.hh" // location.cc:337
+  /// Abstract a location.
+  class location
+  {
+  public:
+
+    /// Construct a location from \a b to \a e.
+    location (const position& b, const position& e)
+      : begin (b)
+      , end (e)
+    {
+    }
+
+    /// Construct a 0-width location in \a p.
+    explicit location (const position& p = position ())
+      : begin (p)
+      , end (p)
+    {
+    }
+
+    /// Construct a 0-width location in \a f, \a l, \a c.
+    explicit location (std::string* f,
+                       unsigned int l = 1u,
+                       unsigned int c = 1u)
+      : begin (f, l, c)
+      , end (f, l, c)
+    {
+    }
+
+
+    /// Initialization.
+    void initialize (std::string* f = YY_NULLPTR,
+                     unsigned int l = 1u,
+                     unsigned int c = 1u)
+    {
+      begin.initialize (f, l, c);
+      end = begin;
+    }
+
+    /** \name Line and Column related manipulators
+     ** \{ */
+  public:
+    /// Reset initial location to final location.
+    void step ()
+    {
+      begin = end;
+    }
+
+    /// Extend the current location to the COUNT next columns.
+    void columns (int count = 1)
+    {
+      end += count;
+    }
+
+    /// Extend the current location to the COUNT next lines.
+    void lines (int count = 1)
+    {
+      end.lines (count);
+    }
+    /** \} */
+
+
+  public:
+    /// Beginning of the located region.
+    position begin;
+    /// End of the located region.
+    position end;
+  };
+
+  /// Join two locations, in place.
+  inline location& operator+= (location& res, const location& end)
+  {
+    res.end = end.end;
+    return res;
+  }
+
+  /// Join two locations.
+  inline location operator+ (location res, const location& end)
+  {
+    return res += end;
+  }
+
+  /// Add \a width columns to the end position, in place.
+  inline location& operator+= (location& res, int width)
+  {
+    res.columns (width);
+    return res;
+  }
+
+  /// Add \a width columns to the end position.
+  inline location operator+ (location res, int width)
+  {
+    return res += width;
+  }
+
+  /// Subtract \a width columns to the end position, in place.
+  inline location& operator-= (location& res, int width)
+  {
+    return res += -width;
+  }
+
+  /// Subtract \a width columns to the end position.
+  inline location operator- (location res, int width)
+  {
+    return res -= width;
+  }
+
+  /// Compare two location objects.
+  inline bool
+  operator== (const location& loc1, const location& loc2)
+  {
+    return loc1.begin == loc2.begin && loc1.end == loc2.end;
+  }
+
+  /// Compare two location objects.
+  inline bool
+  operator!= (const location& loc1, const location& loc2)
+  {
+    return !(loc1 == loc2);
+  }
+
+  /** \brief Intercept output stream redirection.
+   ** \param ostr the destination output stream
+   ** \param loc a reference to the location to redirect
+   **
+   ** Avoid duplicate information.
+   */
+  template <typename YYChar>
+  inline std::basic_ostream<YYChar>&
+  operator<< (std::basic_ostream<YYChar>& ostr, const location& loc)
+  {
+    unsigned int end_col = 0 < loc.end.column ? loc.end.column - 1 : 0;
+    ostr << loc.begin;
+    if (loc.end.filename
+        && (!loc.begin.filename
+            || *loc.begin.filename != *loc.end.filename))
+      ostr << '-' << loc.end.filename << ':' << loc.end.line << '.' << end_col;
+    else if (loc.begin.line < loc.end.line)
+      ostr << '-' << loc.end.line << '.' << end_col;
+    else if (loc.begin.column < end_col)
+      ostr << '-' << end_col;
+    return ostr;
+  }
+
+#line 21 "parser.yy" // location.cc:337
+} } // isc::eval
+#line 192 "location.hh" // location.cc:337
+#endif // !YY_YY_LOCATION_HH_INCLUDED

File diff suppressed because it is too large
+ 1039 - 0
src/lib/eval/parser.cc


File diff suppressed because it is too large
+ 1058 - 0
src/lib/eval/parser.h


+ 141 - 0
src/lib/eval/parser.yy

@@ -0,0 +1,141 @@
+/* Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
+
+   Permission to use, copy, modify, and/or distribute this software for any
+   purpose with or without fee is hereby granted, provided that the above
+   copyright notice and this permission notice appear in all copies.
+
+   THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+   REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+   AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+   INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+   LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+   OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+   PERFORMANCE OF THIS SOFTWARE. */
+
+%skeleton "lalr1.cc" /* -*- C++ -*- */
+%require "3.0.0"
+%defines
+%define parser_class_name {EvalParser}
+%define api.token.constructor
+%define api.value.type variant
+%define api.namespace {isc::eval}
+%define parse.assert
+%code requires
+{
+#include <string>
+#include <eval/token.h>
+#include <eval/eval_context_decl.h>
+#include <boost/lexical_cast.hpp>
+
+using namespace isc::dhcp;
+using namespace isc::eval;
+}
+// The parsing context.
+%param { EvalContext& ctx }
+%locations
+%define parse.trace
+%define parse.error verbose
+%code
+{
+# include "eval_context.h"
+}
+%define api.token.prefix {TOKEN_}
+%token
+  END  0  "end of file"
+  EQUAL "=="
+  OPTION "option"
+  SUBSTRING "substring"
+  ALL "all"
+  COMA ","
+  LPAREN  "("
+  RPAREN  ")"
+  LBRACKET "["
+  RBRACKET "]"
+;
+
+%token <std::string> STRING "constant string"
+%token <std::string> INTEGER "integer"
+%token <std::string> HEXSTRING "constant hexstring"
+%token <std::string> TOKEN
+
+%printer { yyoutput << $$; } <*>;
+%%
+
+// The whole grammar starts with an expression.
+%start expression;
+
+// Expression can either be a single token or a (something == something) expression
+
+expression : bool_expr
+           ;
+
+bool_expr : string_expr EQUAL string_expr
+                {
+                    TokenPtr eq(new TokenEqual());
+                    ctx.expression.push_back(eq);
+                }
+          ;
+
+string_expr : STRING
+                  {
+                      TokenPtr str(new TokenString($1));
+                      ctx.expression.push_back(str);
+                  }
+            | HEXSTRING
+                  {
+                      TokenPtr hex(new TokenHexString($1));
+                      ctx.expression.push_back(hex);
+                  }
+            | OPTION "[" INTEGER "]"
+                  {
+                      int n;
+                      try {
+                          n  = boost::lexical_cast<int>($3);
+                      } catch (const boost::bad_lexical_cast &) {
+                          // This can't happen...
+                          ctx.error(@3,
+                                    "Option code has invalid value in " + $3);
+                      }
+                      if (n < 0 || n > 65535) {
+                          ctx.error(@3,
+                                    "Option code has invalid value in "
+                                    + $3 + ". Allowed range: 0..65535");
+                      }
+                      TokenPtr opt(new TokenOption(static_cast<uint16_t>(n)));
+                      ctx.expression.push_back(opt);
+                  }
+            | SUBSTRING "(" string_expr "," start_expr "," length_expr ")"
+                  {
+                      TokenPtr sub(new TokenSubstring());
+                      ctx.expression.push_back(sub);
+                  }
+            | TOKEN
+                // Temporary unused token to avoid explict but long errors
+            ;
+
+start_expr : INTEGER
+                 {
+                     TokenPtr str(new TokenString($1));
+                     ctx.expression.push_back(str);
+                 }
+           ;
+
+length_expr : INTEGER
+                  {
+                      TokenPtr str(new TokenString($1));
+                      ctx.expression.push_back(str);
+                  }
+            | ALL
+                 {
+                     TokenPtr str(new TokenString("all"));
+                     ctx.expression.push_back(str);
+                 }
+            ;
+
+%%
+void
+isc::eval::EvalParser::error(const location_type& loc,
+                             const std::string& what)
+{
+    ctx.error(loc, what);
+}

+ 181 - 0
src/lib/eval/position.hh

@@ -0,0 +1,181 @@
+// Generated 2015114
+// A Bison parser, made by GNU Bison 3.0.4.
+
+// Positions for Bison parsers in C++
+
+// Copyright (C) 2002-2015 Free Software Foundation, Inc.
+
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+// As a special exception, you may create a larger work that contains
+// part or all of the Bison parser skeleton and distribute that work
+// under terms of your choice, so long as that work isn't itself a
+// parser generator using the skeleton or a modified version thereof
+// as a parser skeleton.  Alternatively, if you modify or redistribute
+// the parser skeleton itself, you may (at your option) remove this
+// special exception, which will cause the skeleton and the resulting
+// Bison output files to be licensed under the GNU General Public
+// License without this special exception.
+
+// This special exception was added by the Free Software Foundation in
+// version 2.2 of Bison.
+
+/**
+ ** \file position.hh
+ ** Define the isc::eval::position class.
+ */
+
+#ifndef YY_YY_POSITION_HH_INCLUDED
+# define YY_YY_POSITION_HH_INCLUDED
+
+# include <algorithm> // std::max
+# include <iostream>
+# include <string>
+
+# ifndef YY_NULLPTR
+#  if defined __cplusplus && 201103L <= __cplusplus
+#   define YY_NULLPTR nullptr
+#  else
+#   define YY_NULLPTR 0
+#  endif
+# endif
+
+#line 21 "parser.yy" // location.cc:337
+namespace isc { namespace eval {
+#line 56 "position.hh" // location.cc:337
+  /// Abstract a position.
+  class position
+  {
+  public:
+    /// Construct a position.
+    explicit position (std::string* f = YY_NULLPTR,
+                       unsigned int l = 1u,
+                       unsigned int c = 1u)
+      : filename (f)
+      , line (l)
+      , column (c)
+    {
+    }
+
+
+    /// Initialization.
+    void initialize (std::string* fn = YY_NULLPTR,
+                     unsigned int l = 1u,
+                     unsigned int c = 1u)
+    {
+      filename = fn;
+      line = l;
+      column = c;
+    }
+
+    /** \name Line and Column related manipulators
+     ** \{ */
+    /// (line related) Advance to the COUNT next lines.
+    void lines (int count = 1)
+    {
+      if (count)
+        {
+          column = 1u;
+          line = add_ (line, count, 1);
+        }
+    }
+
+    /// (column related) Advance to the COUNT next columns.
+    void columns (int count = 1)
+    {
+      column = add_ (column, count, 1);
+    }
+    /** \} */
+
+    /// File name to which this position refers.
+    std::string* filename;
+    /// Current line number.
+    unsigned int line;
+    /// Current column number.
+    unsigned int column;
+
+  private:
+    /// Compute max(min, lhs+rhs) (provided min <= lhs).
+    static unsigned int add_ (unsigned int lhs, int rhs, unsigned int min)
+    {
+      return (0 < rhs || -static_cast<unsigned int>(rhs) < lhs
+              ? rhs + lhs
+              : min);
+    }
+  };
+
+  /// Add \a width columns, in place.
+  inline position&
+  operator+= (position& res, int width)
+  {
+    res.columns (width);
+    return res;
+  }
+
+  /// Add \a width columns.
+  inline position
+  operator+ (position res, int width)
+  {
+    return res += width;
+  }
+
+  /// Subtract \a width columns, in place.
+  inline position&
+  operator-= (position& res, int width)
+  {
+    return res += -width;
+  }
+
+  /// Subtract \a width columns.
+  inline position
+  operator- (position res, int width)
+  {
+    return res -= width;
+  }
+
+  /// Compare two position objects.
+  inline bool
+  operator== (const position& pos1, const position& pos2)
+  {
+    return (pos1.line == pos2.line
+            && pos1.column == pos2.column
+            && (pos1.filename == pos2.filename
+                || (pos1.filename && pos2.filename
+                    && *pos1.filename == *pos2.filename)));
+  }
+
+  /// Compare two position objects.
+  inline bool
+  operator!= (const position& pos1, const position& pos2)
+  {
+    return !(pos1 == pos2);
+  }
+
+  /** \brief Intercept output stream redirection.
+   ** \param ostr the destination output stream
+   ** \param pos a reference to the position to redirect
+   */
+  template <typename YYChar>
+  inline std::basic_ostream<YYChar>&
+  operator<< (std::basic_ostream<YYChar>& ostr, const position& pos)
+  {
+    if (pos.filename)
+      ostr << *pos.filename << ':';
+    return ostr << pos.line << '.' << pos.column;
+  }
+
+#line 21 "parser.yy" // location.cc:337
+} } // isc::eval
+#line 180 "position.hh" // location.cc:337
+#endif // !YY_YY_POSITION_HH_INCLUDED

+ 158 - 0
src/lib/eval/stack.hh

@@ -0,0 +1,158 @@
+// Generated 2015114
+// A Bison parser, made by GNU Bison 3.0.4.
+
+// Stack handling for Bison parsers in C++
+
+// Copyright (C) 2002-2015 Free Software Foundation, Inc.
+
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+// As a special exception, you may create a larger work that contains
+// part or all of the Bison parser skeleton and distribute that work
+// under terms of your choice, so long as that work isn't itself a
+// parser generator using the skeleton or a modified version thereof
+// as a parser skeleton.  Alternatively, if you modify or redistribute
+// the parser skeleton itself, you may (at your option) remove this
+// special exception, which will cause the skeleton and the resulting
+// Bison output files to be licensed under the GNU General Public
+// License without this special exception.
+
+// This special exception was added by the Free Software Foundation in
+// version 2.2 of Bison.
+
+/**
+ ** \file stack.hh
+ ** Define the isc::eval::stack class.
+ */
+
+#ifndef YY_YY_STACK_HH_INCLUDED
+# define YY_YY_STACK_HH_INCLUDED
+
+# include <vector>
+
+#line 21 "parser.yy" // stack.hh:151
+namespace isc { namespace eval {
+#line 46 "stack.hh" // stack.hh:151
+  template <class T, class S = std::vector<T> >
+  class stack
+  {
+  public:
+    // Hide our reversed order.
+    typedef typename S::reverse_iterator iterator;
+    typedef typename S::const_reverse_iterator const_iterator;
+
+    stack ()
+      : seq_ ()
+    {
+      seq_.reserve (200);
+    }
+
+    stack (unsigned int n)
+      : seq_ (n)
+    {}
+
+    inline
+    T&
+    operator[] (unsigned int i)
+    {
+      return seq_[seq_.size () - 1 - i];
+    }
+
+    inline
+    const T&
+    operator[] (unsigned int i) const
+    {
+      return seq_[seq_.size () - 1 - i];
+    }
+
+    /// Steal the contents of \a t.
+    ///
+    /// Close to move-semantics.
+    inline
+    void
+    push (T& t)
+    {
+      seq_.push_back (T());
+      operator[](0).move (t);
+    }
+
+    inline
+    void
+    pop (unsigned int n = 1)
+    {
+      for (; n; --n)
+        seq_.pop_back ();
+    }
+
+    void
+    clear ()
+    {
+      seq_.clear ();
+    }
+
+    inline
+    typename S::size_type
+    size () const
+    {
+      return seq_.size ();
+    }
+
+    inline
+    const_iterator
+    begin () const
+    {
+      return seq_.rbegin ();
+    }
+
+    inline
+    const_iterator
+    end () const
+    {
+      return seq_.rend ();
+    }
+
+  private:
+    stack (const stack&);
+    stack& operator= (const stack&);
+    /// The wrapped container.
+    S seq_;
+  };
+
+  /// Present a slice of the top of a stack.
+  template <class T, class S = stack<T> >
+  class slice
+  {
+  public:
+    slice (const S& stack, unsigned int range)
+      : stack_ (stack)
+      , range_ (range)
+    {}
+
+    inline
+    const T&
+    operator [] (unsigned int i) const
+    {
+      return stack_[range_ - i];
+    }
+
+  private:
+    const S& stack_;
+    unsigned int range_;
+  };
+
+#line 21 "parser.yy" // stack.hh:151
+} } // isc::eval
+#line 156 "stack.hh" // stack.hh:151
+
+#endif // !YY_YY_STACK_HH_INCLUDED

+ 3 - 1
src/lib/eval/tests/Makefile.am

@@ -26,7 +26,9 @@ if HAVE_GTEST
 
 TESTS += libeval_unittests
 
-libeval_unittests_SOURCES  = token_unittest.cc run_unittests.cc
+libeval_unittests_SOURCES  = context_unittest.cc
+libeval_unittests_SOURCES += token_unittest.cc
+libeval_unittests_SOURCES += run_unittests.cc
 libeval_unittests_CXXFLAGS = $(AM_CXXFLAGS)
 libeval_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES)
 libeval_unittests_LDFLAGS  = $(AM_LDFLAGS) $(CRYPTO_LDFLAGS) $(GTEST_LDFLAGS)

+ 310 - 0
src/lib/eval/tests/context_unittest.cc

@@ -0,0 +1,310 @@
+// Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <config.h>
+#include <eval/token.h>
+#include <eval/eval_context.h>
+#include <eval/token.h>
+#include <dhcp/pkt4.h>
+
+#include <boost/shared_ptr.hpp>
+#include <boost/scoped_ptr.hpp>
+#include <gtest/gtest.h>
+
+using namespace std;
+using namespace isc::dhcp;
+
+namespace {
+
+/// @brief Test class for testing EvalContext aka class test parsing
+class EvalContextTest : public ::testing::Test {
+public:
+    /// @brief checks if the given token is a string with the expected value
+    void checkTokenString(const TokenPtr& token, const std::string& expected) {
+        ASSERT_TRUE(token);
+        boost::shared_ptr<TokenString> str =
+            boost::dynamic_pointer_cast<TokenString>(token);
+        ASSERT_TRUE(str);
+
+        Pkt4Ptr pkt4(new Pkt4(DHCPDISCOVER, 12345));
+        ValueStack values;
+
+        EXPECT_NO_THROW(token->evaluate(*pkt4, values));
+
+        ASSERT_EQ(1, values.size());
+
+        EXPECT_EQ(expected, values.top());
+    }
+
+    /// @brief checks if the given token is a hex string with the expected value
+    void checkTokenHexString(const TokenPtr& token,
+                             const std::string& expected) {
+        ASSERT_TRUE(token);
+        boost::shared_ptr<TokenHexString> hex =
+            boost::dynamic_pointer_cast<TokenHexString>(token);
+        ASSERT_TRUE(hex);
+
+        Pkt4Ptr pkt4(new Pkt4(DHCPDISCOVER, 12345));
+        ValueStack values;
+
+        EXPECT_NO_THROW(token->evaluate(*pkt4, values));
+
+        ASSERT_EQ(1, values.size());
+
+        EXPECT_EQ(expected, values.top());
+    }
+
+    /// @brief checks if the given token is an equal operator
+    void checkTokenEq(const TokenPtr& token) {
+        ASSERT_TRUE(token);
+        boost::shared_ptr<TokenEqual> eq =
+            boost::dynamic_pointer_cast<TokenEqual>(token);
+        EXPECT_TRUE(eq);
+    }
+
+    /// @brief checks if the given token is an option with the expected code
+    void checkTokenOption(const TokenPtr& token, uint16_t expected_code) {
+        ASSERT_TRUE(token);
+        boost::shared_ptr<TokenOption> opt =
+            boost::dynamic_pointer_cast<TokenOption>(token);
+        ASSERT_TRUE(opt);
+
+        EXPECT_EQ(expected_code, opt->getCode());
+    }
+
+    /// @brief checks if the given token is a substring operator
+    void checkTokenSubstring(const TokenPtr& token) {
+        ASSERT_TRUE(token);
+        boost::shared_ptr<TokenSubstring> sub =
+            boost::dynamic_pointer_cast<TokenSubstring>(token);
+        EXPECT_TRUE(sub);
+    }
+
+    /// @brief checks if the given expression raises the expected message
+    /// when it is parsed.
+    void checkError(const string& expr, const string& msg) {
+        EvalContext eval;
+        parsed_ = false;
+        try {
+            parsed_ = eval.parseString(expr);
+            FAIL() << "Expected EvalParseError but nothing was raised";
+        }
+        catch (const EvalParseError& ex) {
+            EXPECT_EQ(msg, ex.what());
+            EXPECT_FALSE(parsed_);
+        }
+        catch (...) {
+            FAIL() << "Expected EvalParseError but something else was raised";
+        }
+    }
+
+    bool parsed_; ///< Parsing status
+};
+
+// Test the parsing of a basic expression
+TEST_F(EvalContextTest, basic) {
+
+    EvalContext tmp;
+
+    EXPECT_NO_THROW(parsed_ = tmp.parseString("option[123] == 'MSFT'"));
+    EXPECT_TRUE(parsed_);
+}
+
+// Test the parsing of a string terminal
+TEST_F(EvalContextTest, string) {
+    EvalContext eval;
+
+    EXPECT_NO_THROW(parsed_ = eval.parseString("'foo' == 'bar'"));
+    EXPECT_TRUE(parsed_);
+
+    ASSERT_EQ(3, eval.expression.size());
+
+    TokenPtr tmp1  = eval.expression.at(0);
+    TokenPtr tmp2  = eval.expression.at(1);
+
+    checkTokenString(tmp1, "foo");
+    checkTokenString(tmp2, "bar");
+}
+
+// Test the parsing of a basic expression using integers
+TEST_F(EvalContextTest, integer) {
+
+    EvalContext eval;
+
+    EXPECT_NO_THROW(parsed_ =
+        eval.parseString("substring(option[123], 0, 2) == '42'"));
+    EXPECT_TRUE(parsed_);
+}
+
+// Test the parsing of a hexstring terminal
+TEST_F(EvalContextTest, hexstring) {
+    EvalContext eval;
+
+    EXPECT_NO_THROW(parsed_ = eval.parseString("0x666f6f == 'foo'"));
+    EXPECT_TRUE(parsed_);
+
+    ASSERT_EQ(3, eval.expression.size());
+
+    TokenPtr tmp = eval.expression.at(0);
+
+    checkTokenHexString(tmp, "foo");
+}
+
+// Test the parsing of a hexstring terminal with an odd number of
+// hexadecimal digits
+TEST_F(EvalContextTest, oddHexstring) {
+    EvalContext eval;
+
+    EXPECT_NO_THROW(parsed_ = eval.parseString("0X7 == 'foo'"));
+    EXPECT_TRUE(parsed_);
+
+    ASSERT_EQ(3, eval.expression.size());
+
+    TokenPtr tmp = eval.expression.at(0);
+
+    checkTokenHexString(tmp, "\a");
+}
+
+// Test the parsing of an equal expression
+TEST_F(EvalContextTest, equal) {
+    EvalContext eval;
+
+    EXPECT_NO_THROW(parsed_ = eval.parseString("'foo' == 'bar'"));
+    EXPECT_TRUE(parsed_);
+
+    ASSERT_EQ(3, eval.expression.size());
+
+    TokenPtr tmp1 = eval.expression.at(0);
+    TokenPtr tmp2 = eval.expression.at(1);
+    TokenPtr tmp3 = eval.expression.at(2);
+
+    checkTokenString(tmp1, "foo");
+    checkTokenString(tmp2, "bar");
+    checkTokenEq(tmp3);
+}
+
+// Test the parsing of an option terminal
+TEST_F(EvalContextTest, option) {
+    EvalContext eval;
+
+    EXPECT_NO_THROW(parsed_ = eval.parseString("option[123] == 'foo'"));
+    EXPECT_TRUE(parsed_);
+    ASSERT_EQ(3, eval.expression.size());
+    checkTokenOption(eval.expression.at(0), 123);
+}
+
+// Test the parsing of a substring expression
+TEST_F(EvalContextTest, substring) {
+    EvalContext eval;
+
+    EXPECT_NO_THROW(parsed_ =
+        eval.parseString("substring('foobar',2,all) == 'obar'"));
+    EXPECT_TRUE(parsed_);
+
+    ASSERT_EQ(6, eval.expression.size());
+
+    TokenPtr tmp1 = eval.expression.at(0);
+    TokenPtr tmp2 = eval.expression.at(1);
+    TokenPtr tmp3 = eval.expression.at(2);
+    TokenPtr tmp4 = eval.expression.at(3);
+
+    checkTokenString(tmp1, "foobar");
+    checkTokenString(tmp2, "2");
+    checkTokenString(tmp3, "all");
+    checkTokenSubstring(tmp4);
+}
+
+// Test some scanner error cases
+TEST_F(EvalContextTest, scanErrors) {
+    checkError("'", "<string>:1.1: Invalid character: '");
+    checkError("'\''", "<string>:1.3: Invalid character: '");
+    checkError("'\n'", "<string>:1.1: Invalid character: '");
+    checkError("0x123h", "<string>:1.6: Invalid character: h");
+    checkError("=", "<string>:1.1: Invalid character: =");
+    checkError("subtring", "<string>:1.1: Invalid character: s");
+    checkError("foo", "<string>:1.1: Invalid character: f");
+    checkError(" bar", "<string>:1.2: Invalid character: b");
+}
+
+// Tests some scanner/parser error cases
+TEST_F(EvalContextTest, scanParseErrors) {
+    checkError("", "<string>:1.1: syntax error, unexpected end of file");
+    checkError(" ", "<string>:1.2: syntax error, unexpected end of file");
+    checkError("0x", "<string>:1.1: syntax error, unexpected integer");
+    checkError("0abc",
+               "<string>:1.1: syntax error, unexpected integer");
+    checkError("===", "<string>:1.1-2: syntax error, unexpected ==");
+    checkError("option[-1]",
+               "<string>:1.8-9: Option code has invalid "
+               "value in -1. Allowed range: 0..65535");
+    checkError("option[65536]",
+               "<string>:1.8-12: Option code has invalid "
+               "value in 65536. Allowed range: 0..65535");
+    checkError("option[12345678901234567890]",
+               "<string>:1.8-27: Failed to convert 12345678901234567890 "
+               "to an integer.");
+    checkError("option[123] < 'foo'", "<string>:1.13: Invalid character: <");
+    checkError("substring('foo',12345678901234567890,1)",
+               "<string>:1.17-36: Failed to convert 12345678901234567890 "
+               "to an integer.");
+}
+
+// Tests some parser error cases
+TEST_F(EvalContextTest, parseErrors) {
+    checkError("'foo''bar'",
+               "<string>:1.6-10: syntax error, unexpected constant string, "
+               "expecting ==");
+    checkError("== 'ab'", "<string>:1.1-2: syntax error, unexpected ==");
+    checkError("'foo' ==",
+               "<string>:1.9: syntax error, unexpected end of file");
+    checkError("option 'ab'",
+               "<string>:1.8-11: syntax error, unexpected "
+               "constant string, expecting [");
+    checkError("option(10) == 'ab'",
+               "<string>:1.7: syntax error, "
+               "unexpected (, expecting [");
+    checkError("option['ab'] == 'foo'",
+               "<string>:1.8-11: syntax error, "
+               "unexpected constant string, "
+               "expecting integer");
+    checkError("option[0xa] == 'ab'",
+               "<string>:1.8-10: syntax error, "
+               "unexpected constant hexstring, "
+               "expecting integer");
+    checkError("substring('foobar') == 'f'",
+               "<string>:1.19: syntax error, "
+               "unexpected ), expecting \",\"");
+    checkError("substring('foobar',3) == 'bar'",
+               "<string>:1.21: syntax error, unexpected ), expecting \",\"");
+    checkError("substring('foobar','3',3) == 'bar'",
+               "<string>:1.20-22: syntax error, unexpected constant string, "
+               "expecting integer");
+    checkError("substring('foobar',1,a) == 'foo'",
+               "<string>:1.22: Invalid character: a");
+}
+
+// Tests some type error cases (caught only by the strongly typed parser)
+TEST_F(EvalContextTest, typeErrors) {
+    checkError("'foobar'",
+               "<string>:1.9: syntax error, unexpected end of file, "
+               "expecting ==");
+    checkError("substring('foobar',all,1) == 'foo'",
+               "<string>:1.20-22: syntax error, unexpected all, "
+               "expecting integer");
+    checkError("substring('foobar',0x32,1) == 'foo'",
+               "<string>:1.20-23: syntax error, unexpected constant "
+               "hexstring, expecting integer");
+}
+
+};

+ 23 - 16
src/lib/eval/tests/token_unittest.cc

@@ -72,10 +72,12 @@ public:
     /// @param test_start The postion to start when getting a substring
     /// @param test_length The length of the substring to get
     /// @param result_string The expected result of the eval
+    /// @param should_throw The eval will throw
     void verifySubstringEval(const std::string& test_string,
                              const std::string& test_start,
                              const std::string& test_length,
-                             const std::string& result_string) {
+                             const std::string& result_string,
+                             bool should_throw = false) {
 
         // create the token
         ASSERT_NO_THROW(t_.reset(new TokenSubstring()));
@@ -86,14 +88,19 @@ public:
         values_.push(test_length);
 
         // evaluate the token
-        EXPECT_NO_THROW(t_->evaluate(*pkt4_, values_));
-
-        // verify results
-        ASSERT_EQ(1, values_.size());
-        EXPECT_EQ(result_string, values_.top());
-
-        // remove result
-        values_.pop();
+        if (should_throw) {
+            EXPECT_THROW(t_->evaluate(*pkt4_, values_), EvalTypeError);
+            ASSERT_EQ(0, values_.size());
+        } else {
+            EXPECT_NO_THROW(t_->evaluate(*pkt4_, values_));
+
+            // verify results
+            ASSERT_EQ(1, values_.size());
+            EXPECT_EQ(result_string, values_.top());
+
+            // remove result
+            values_.pop();
+        }
     }
 
     /// @todo: Add more option types here
@@ -443,13 +450,13 @@ TEST_F(TokenTest, substringStartingPosition) {
 // Check what happens if we use strings that aren't numbers for start or length
 // We should return the empty string
 TEST_F(TokenTest, substringBadParams) {
-    verifySubstringEval("foobar", "0ick", "all", "");
-    verifySubstringEval("foobar", "ick0", "all", "");
-    verifySubstringEval("foobar", "ick", "all", "");
-    verifySubstringEval("foobar", "0", "ick", "");
-    verifySubstringEval("foobar", "0", "0ick", "");
-    verifySubstringEval("foobar", "0", "ick0", "");
-    verifySubstringEval("foobar", "0", "allaboard", "");
+    verifySubstringEval("foobar", "0ick", "all", "", true);
+    verifySubstringEval("foobar", "ick0", "all", "", true);
+    verifySubstringEval("foobar", "ick", "all", "", true);
+    verifySubstringEval("foobar", "0", "ick", "", true);
+    verifySubstringEval("foobar", "0", "0ick", "", true);
+    verifySubstringEval("foobar", "0", "ick0", "", true);
+    verifySubstringEval("foobar", "0", "allaboard", "", true);
 }
 
 // lastly check that we don't get anything if the string is empty or

+ 9 - 7
src/lib/eval/token.cc

@@ -121,19 +121,21 @@ TokenSubstring::evaluate(const Pkt& /*pkt*/, ValueStack& values) {
     int length;
     try {
         start_pos = boost::lexical_cast<int>(start_str);
+    } catch (const boost::bad_lexical_cast&) {
+        isc_throw(EvalTypeError, "the parameter '" << start_str
+                  << "' for the starting postion of the substring "
+                  << "couldn't be converted to an integer.");
+    }
+    try {
         if (len_str == "all") {
             length = string_str.length();
         } else {
             length = boost::lexical_cast<int>(len_str);
         }
     } catch (const boost::bad_lexical_cast&) {
-        LOG_DEBUG(eval_logger, EVAL_DBG_TRACE,
-                  EVAL_SUBSTRING_BAD_PARAM_CONVERSION)
-            .arg(start_str)
-            .arg(len_str);
-
-        values.push("");
-        return;
+        isc_throw(EvalTypeError, "the parameter '" << len_str
+                  << "' for the length of the substring "
+                  << "couldn't be converted to an integer.");
     }
 
     const int string_length = string_str.length();

+ 21 - 1
src/lib/eval/token.h

@@ -40,7 +40,7 @@ typedef boost::shared_ptr<Expression> ExpressionPtr;
 /// Evaluated values are stored as a stack of strings
 typedef std::stack<std::string> ValueStack;
 
-/// @brief EvalStackError is thrown when more or less parameters are on the
+/// @brief EvalBadStack is thrown when more or less parameters are on the
 ///        stack than expected.
 class EvalBadStack : public Exception {
 public:
@@ -48,6 +48,14 @@ public:
         isc::Exception(file, line, what) { };
 };
 
+/// @brief EvalTypeError is thrown when a value on the stack has a content
+///        with an unexpected type.
+class EvalTypeError : public Exception {
+public:
+    EvalTypeError(const char* file, size_t line, const char* what) :
+        isc::Exception(file, line, what) { };
+};
+
 /// @brief Base class for all tokens
 ///
 /// It provides an interface for all tokens and storage for string representation
@@ -157,6 +165,16 @@ public:
     /// @param values value of the option will be pushed here (or "")
     void evaluate(const Pkt& pkt, ValueStack& values);
 
+    /// @brief Returns option-code
+    ///
+    /// This method is used in testing to determine if the parser had
+    /// instantiated TokenOption with correct parameters.
+    ///
+    /// @return option-code of the option this token expects to extract.
+    uint16_t getCode() const {
+        return (option_code_);
+    }
+
 private:
     uint16_t option_code_; ///< code of the option to be extracted
 };
@@ -233,6 +251,8 @@ public:
     /// - -1, -4  => "ooba"
     ///
     /// @throw EvalBadStack if there are less than 3 values on stack
+    /// @throw EvalTypeError if start is not a number or length a number or
+    ///        the special value "all".
     ///
     /// @param pkt (unused)
     /// @param values - stack of values (3 arguments will be popped, 1 result