Browse Source

[5363] Added ifelse operator

Francis Dupont 7 years ago
parent
commit
373d2d91f4

+ 10 - 0
doc/guide/classify.xml

@@ -579,6 +579,7 @@
 <row><entry>Substring</entry><entry>substring('foobar',0,3)</entry><entry>Return the requested substring</entry></row>
 <row><entry>Substring</entry><entry>substring('foobar',0,3)</entry><entry>Return the requested substring</entry></row>
 <row><entry>Concat</entry><entry>concat('foo','bar')</entry><entry>Return the
 <row><entry>Concat</entry><entry>concat('foo','bar')</entry><entry>Return the
 concatenation of the strings</entry></row>
 concatenation of the strings</entry></row>
+<row><entry>Ifelse</entry><entry>ifelse('foo' == 'bar','us','them')</entry><entry>Return the branch value according to the condition</entry></row>
           </tbody>
           </tbody>
           </tgroup>
           </tgroup>
         </table>
         </table>
@@ -624,6 +625,15 @@ concatenation of the strings</entry></row>
         concat('foo', 'bar') == 'foobar'
         concat('foo', 'bar') == 'foobar'
           </screen>
           </screen>
        </section>
        </section>
+       <section>
+         <title>Ifelse</title>
+         The ifelse function "ifelse(cond, iftrue, ifelse)" returns the
+         "iftrue" or "ifelse" branch value following the boolean
+         condition "cond". For instance:
+           <screen>
+         ifelse(option[230].exists, option[230].hex, 'none')
+           </screen>
+        </section>
     </section>
     </section>
 
 
   <note>
   <note>

+ 6 - 5
src/lib/eval/eval.dox

@@ -132,13 +132,13 @@ instantiated with the appropriate value and put onto the expression vector.
  isc::eval::Token class and represents a certain expression primitive.
  isc::eval::Token class and represents a certain expression primitive.
  Currently supported tokens are:
  Currently supported tokens are:
 
 
- - isc::dhcp::TokenString -- represents a constant string, e.g. "MSFT";
+ - isc::dhcp::TokenString -- represents a constant string, e.g. "MSFT".
  - isc::dhcp::TokenHexString -- represents a constant string, encoded as
  - isc::dhcp::TokenHexString -- represents a constant string, encoded as
-   hex string, e.g. 0x666f6f which is actually "foo";
+   hex string, e.g. 0x666f6f which is actually "foo".
  - isc::dhcp::TokenIpAddress -- represents a constant IP address, encoded as
  - isc::dhcp::TokenIpAddress -- represents a constant IP address, encoded as
    a 4 or 16 byte binary string, e.g., 10.0.0.1 is 0x10000001.
    a 4 or 16 byte binary string, e.g., 10.0.0.1 is 0x10000001.
  - isc::dhcp::TokenOption -- represents an option in a packet, e.g.
  - isc::dhcp::TokenOption -- represents an option in a packet, e.g.
-                    option[123].text;
+                    option[123].text.
  - isc::dhcp::TokenRelay4Option -- represents a sub-option inserted by the
  - isc::dhcp::TokenRelay4Option -- represents a sub-option inserted by the
                     DHCPv4 relay, e.g. relay[123].text or relay[123].hex
                     DHCPv4 relay, e.g. relay[123].text or relay[123].hex
  - isc::dhcp::TokenRelay6Option -- represents a sub-option inserted by
  - isc::dhcp::TokenRelay6Option -- represents a sub-option inserted by
@@ -149,10 +149,11 @@ instantiated with the appropriate value and put onto the expression vector.
  - isc::dhcp::TokenPkt6 -- represents a DHCPv6 packet field (message type
  - isc::dhcp::TokenPkt6 -- represents a DHCPv6 packet field (message type
    or transaction id).
    or transaction id).
  - isc::dhcp::TokenRelay6Field -- represents a DHCPv6 relay information field.
  - isc::dhcp::TokenRelay6Field -- represents a DHCPv6 relay information field.
- - isc::dhcp::TokenEqual -- represents the equal (==) operator;
- - isc::dhcp::TokenSubstring -- represents the substring(text, start, length) operator;
+ - isc::dhcp::TokenEqual -- represents the equal (==) operator.
+ - isc::dhcp::TokenSubstring -- represents the substring(text, start, length) operator.
  - isc::dhcp::TokenConcat -- represents the concat operator which
  - isc::dhcp::TokenConcat -- represents the concat operator which
    concatenate two other tokens.
    concatenate two other tokens.
+ - isc::dhcp::TokenIfElse == represents the ifelse(cond, iftrue, ifelse) operator.
  - isc::dhcp::TokenNot -- the logical not operator.
  - isc::dhcp::TokenNot -- the logical not operator.
  - isc::dhcp::TokenAnd -- the logical and (strict) operator.
  - isc::dhcp::TokenAnd -- the logical and (strict) operator.
  - isc::dhcp::TokenOr -- the logical or (strict) operator (strict means
  - isc::dhcp::TokenOr -- the logical or (strict) operator (strict means

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

@@ -34,6 +34,18 @@ the value stack.  The strings are displayed in hex.
 This debug message indicates that the given binary string is being pushed
 This debug message indicates that the given binary string is being pushed
 onto the value stack.  The string is displayed in hex.
 onto the value stack.  The string is displayed in hex.
 
 
+# For use with TokenIfElse
+
+% EVAL_DEBUG_IFELSE_FALSE Popping %1 (false) and %2, leaving %3
+This debug message indicates that the condition is false so
+the iftrue branch value is removed and the ifelse branch value
+is left on the value stack.
+
+% EVAL_DEBUG_IFELSE_TRUE Popping %1 (true) and %2, leaving %3
+This debug message indicates that the condition is true so
+the ifelse branch value is removed and the iftrue branch value
+is left on the value stack.
+
 # For use with TokenIpAddress
 # For use with TokenIpAddress
 
 
 % EVAL_DEBUG_IPADDRESS Pushing IPAddress %1
 % EVAL_DEBUG_IPADDRESS Pushing IPAddress %1

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

@@ -406,6 +406,14 @@ public:
         EXPECT_TRUE(conc);
         EXPECT_TRUE(conc);
     }
     }
 
 
+    /// @brief checks if the given token is an ifelse operator
+    void checkTokenIfElse(const TokenPtr& token) {
+        ASSERT_TRUE(token);
+        boost::shared_ptr<TokenIfElse> alt =
+            boost::dynamic_pointer_cast<TokenIfElse>(token);
+        EXPECT_TRUE(alt);
+    }
+
     /// @brief checks if the given expression raises the expected message
     /// @brief checks if the given expression raises the expected message
     /// when it is parsed.
     /// when it is parsed.
     void checkError(const string& expr, const string& msg) {
     void checkError(const string& expr, const string& msg) {
@@ -1209,6 +1217,26 @@ TEST_F(EvalContextTest, concat) {
     checkTokenConcat(tmp3);
     checkTokenConcat(tmp3);
 }
 }
 
 
+// Test the parsing of an ifelse expression
+TEST_F(EvalContextTest, ifElse) {
+    EvalContext eval(Option::V4);
+
+    EXPECT_NO_THROW(parsed_ =
+        eval.parseString("ifelse('foo' == 'bar', 'us', 'them') == 'you'"));
+
+    ASSERT_EQ(8, eval.expression.size());
+
+    TokenPtr tmp1 = eval.expression.at(2);
+    TokenPtr tmp2 = eval.expression.at(3);
+    TokenPtr tmp3 = eval.expression.at(4);
+    TokenPtr tmp4 = eval.expression.at(5);
+
+    checkTokenEq(tmp1);
+    checkTokenString(tmp2, "us");
+    checkTokenString(tmp3, "them");
+    checkTokenIfElse(tmp4);
+}
+
 //
 //
 // Test some scanner error cases
 // Test some scanner error cases
 TEST_F(EvalContextTest, scanErrors) {
 TEST_F(EvalContextTest, scanErrors) {
@@ -1358,6 +1386,10 @@ TEST_F(EvalContextTest, parseErrors) {
                "<string>:1.16: syntax error, unexpected ), expecting \",\"");
                "<string>:1.16: syntax error, unexpected ), expecting \",\"");
     checkError("concat('foo','bar','') == 'foobar'",
     checkError("concat('foo','bar','') == 'foobar'",
                "<string>:1.19: syntax error, unexpected \",\", expecting )");
                "<string>:1.19: syntax error, unexpected \",\", expecting )");
+    checkError("ifelse('foo'=='bar','foo')",
+               "<string>:1.26: syntax error, unexpected ), expecting \",\"");
+    checkError("ifelse('foo'=='bar','foo','bar','')",
+               "<string>:1.32: syntax error, unexpected \",\", expecting )");
 }
 }
 
 
 // Tests some type error cases
 // Tests some type error cases
@@ -1388,6 +1420,14 @@ TEST_F(EvalContextTest, typeErrors) {
                "<string>:1.8-10: syntax error, unexpected and, expecting ==");
                "<string>:1.8-10: syntax error, unexpected and, expecting ==");
     checkError("'true' or 'false'",
     checkError("'true' or 'false'",
                "<string>:1.8-9: syntax error, unexpected or, expecting ==");
                "<string>:1.8-9: syntax error, unexpected or, expecting ==");
+    // Ifelse requires a boolean condition and string branches.
+    checkError("ifelse('foobar','foo','bar')",
+               "<string>:1.16: syntax error, unexpected \",\", expecting ==");
+    checkError("ifelse('foo'=='bar','foo'=='foo','bar')",
+               "<string>:1.26-27: syntax error, unexpected ==, "
+               "expecting \",\"");
+    checkError("ifelse('foo'=='bar','foo','bar'=='bar')",
+               "<string>:1.32-33: syntax error, unexpected ==, expecting )");
 }
 }
 
 
 
 

+ 7 - 0
src/lib/eval/tests/evaluate_unittest.cc

@@ -490,6 +490,13 @@ TEST_F(ExpressionsTest, evaluateString) {
                                            EvalContext::PARSER_STRING);
                                            EvalContext::PARSER_STRING);
     testExpressionNegative<EvalParseError>("pkt6.msgtype == 1", Option::V6,
     testExpressionNegative<EvalParseError>("pkt6.msgtype == 1", Option::V6,
                                            EvalContext::PARSER_STRING);
                                            EvalContext::PARSER_STRING);
+
+    // Check that ifelse works as expecting (it was added explicitely for
+    // the string evaluation).
+    testExpressionString(Option::V4,
+                         "ifelse(option[100].exists,'foo','bar')", "foo");
+    testExpressionString(Option::V4,
+                         "ifelse(option[200].exists,'foo','bar')", "bar");
 }
 }
 
 
 };
 };

+ 39 - 0
src/lib/eval/tests/token_unittest.cc

@@ -1954,6 +1954,45 @@ TEST_F(TokenTest, concat) {
     EXPECT_TRUE(checkFile());
     EXPECT_TRUE(checkFile());
 }
 }
 
 
+// This test checks if a token representing an ifelse is able
+// to select the branch following the condition.
+TEST_F(TokenTest, ifElse) {
+    ASSERT_NO_THROW(t_.reset(new TokenIfElse()));
+
+    // Ifelse requires three values on the stack, try
+    // with 0, 1 and 2 all should throw an exception
+    EXPECT_THROW(t_->evaluate(*pkt4_, values_), EvalBadStack);
+
+    values_.push("bar");
+    EXPECT_THROW(t_->evaluate(*pkt4_, values_), EvalBadStack);
+
+    values_.push("foo");
+    EXPECT_THROW(t_->evaluate(*pkt4_, values_), EvalBadStack);
+
+    // The condition must be a boolean
+    values_.push("bar");
+    EXPECT_THROW(t_->evaluate(*pkt4_, values_), EvalTypeError);
+
+    // Check if what it returns
+    clearStack();
+    ASSERT_NO_THROW(t_.reset(new TokenIfElse()));
+    values_.push("true");
+    values_.push("foo");
+    values_.push("bar");
+    EXPECT_NO_THROW(t_->evaluate(*pkt4_, values_));
+    ASSERT_EQ(1, values_.size());
+    EXPECT_EQ("foo", values_.top());
+
+    clearStack();
+    ASSERT_NO_THROW(t_.reset(new TokenIfElse()));
+    values_.push("false");
+    values_.push("foo");
+    values_.push("bar");
+    EXPECT_NO_THROW(t_->evaluate(*pkt4_, values_));
+    ASSERT_EQ(1, values_.size());
+    EXPECT_EQ("bar", values_.top());
+}
+
 // This test checks if a token representing a not is able to
 // This test checks if a token representing a not is able to
 // negate a boolean value (with incorrectly built stack).
 // negate a boolean value (with incorrectly built stack).
 TEST_F(TokenTest, operatorNotInvalid) {
 TEST_F(TokenTest, operatorNotInvalid) {

+ 36 - 0
src/lib/eval/token.cc

@@ -592,6 +592,42 @@ TokenConcat::evaluate(Pkt& /*pkt*/, ValueStack& values) {
 }
 }
 
 
 void
 void
+TokenIfElse::evaluate(Pkt& /*pkt*/, ValueStack& values) {
+
+    if (values.size() < 3) {
+        isc_throw(EvalBadStack, "Incorrect stack order. Expected at least "
+                  "3 values for ifelse, got " << values.size());
+    }
+
+    string iffalse = values.top();
+    values.pop();
+    string iftrue = values.top();
+    values.pop();
+    string cond = values.top();
+    values.pop();
+    bool val = toBool(cond);
+
+    if (val) {
+        values.push(iftrue);
+    } else {
+        values.push(iffalse);
+    }
+
+    // Log what we popped and pushed
+    if (val) {
+        LOG_DEBUG(eval_logger, EVAL_DBG_STACK, EVAL_DEBUG_IFELSE_TRUE)
+            .arg('\'' + cond + '\'')
+            .arg(toHex(iffalse))
+            .arg(toHex(iftrue));
+    } else {
+        LOG_DEBUG(eval_logger, EVAL_DBG_STACK, EVAL_DEBUG_IFELSE_FALSE)
+            .arg('\'' +cond + '\'')
+            .arg(toHex(iftrue))
+            .arg(toHex(iffalse));
+    }
+}
+
+void
 TokenNot::evaluate(Pkt& /*pkt*/, ValueStack& values) {
 TokenNot::evaluate(Pkt& /*pkt*/, ValueStack& values) {
 
 
     if (values.size() == 0) {
     if (values.size() == 0) {

+ 31 - 0
src/lib/eval/token.h

@@ -694,6 +694,37 @@ public:
     void evaluate(Pkt& pkt, ValueStack& values);
     void evaluate(Pkt& pkt, ValueStack& values);
 };
 };
 
 
+/// @brief Token that represents an alternative
+///
+/// For example in the sub-expression "ifelse(cond, iftrue, iffalse)"
+/// the boolean "cond" expression is evaluated, if it is true then
+/// the "iftrue" value is returned else the "iffalse" value is returned.
+/// Please note that "iftrue" and "iffalse" must be plain string (vs. boolean)
+/// expressions and they are always evaluated. If you want a similar
+/// operator on boolean expressions it can be built from "and", "or" and
+/// "not" boolean operators.
+class TokenIfElse : public Token {
+public:
+    /// @brief Constructor (does nothing)
+    TokenIfElse() { }
+
+    /// @brief Alternative.
+    ///
+    /// Evaluation does not use packet information, but rather consumes the
+    /// last three results. It does a simple string comparison on the
+    /// condition (third value on the stack) which is required to be
+    /// either "true" or "false", and leaves the second and first
+    /// value if the condition is "true" or "false".
+    ///
+    /// @throw EvalBadStack if there are less than 3 values on stack
+    /// @throw EvalTypeError if the third value (the condition) is not
+    ///        either "true" or "false"
+    ///
+    /// @param pkt (unused)
+    /// @param values - stack of values (two items are removed)
+    void evaluate(Pkt& pkt, ValueStack& values);
+};
+
 /// @brief Token that represents logical negation operator
 /// @brief Token that represents logical negation operator
 ///
 ///
 /// For example in the expression "not(option[vendor-class].text == 'MSF')"
 /// For example in the expression "not(option[vendor-class].text == 'MSF')"