]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[#3170] Checkpoint: created and added TokenMatch
authorFrancis Dupont <fdupont@isc.org>
Fri, 7 Jun 2024 20:00:36 +0000 (22:00 +0200)
committerFrancis Dupont <fdupont@isc.org>
Tue, 11 Jun 2024 14:31:50 +0000 (16:31 +0200)
src/lib/eval/eval_messages.cc
src/lib/eval/eval_messages.h
src/lib/eval/eval_messages.mes
src/lib/eval/lexer.ll
src/lib/eval/parser.yy
src/lib/eval/tests/token_unittest.cc
src/lib/eval/token.cc
src/lib/eval/token.h

index 04e7931b88db54c1416271f7443705a6a912473d..5b9312b4b4bdc8ce1dd41e4d3c1d8fdfffaf1228 100644 (file)
@@ -19,6 +19,8 @@ extern const isc::log::MessageID EVAL_DEBUG_INT8TOTEXT = "EVAL_DEBUG_INT8TOTEXT"
 extern const isc::log::MessageID EVAL_DEBUG_IPADDRESS = "EVAL_DEBUG_IPADDRESS";
 extern const isc::log::MessageID EVAL_DEBUG_IPADDRESSTOTEXT = "EVAL_DEBUG_IPADDRESSTOTEXT";
 extern const isc::log::MessageID EVAL_DEBUG_LCASE = "EVAL_DEBUG_LCASE";
+extern const isc::log::MessageID EVAL_DEBUG_MATCH = "EVAL_DEBUG_MATCH";
+extern const isc::log::MessageID EVAL_DEBUG_MATCH_ERROR = "EVAL_DEBUG_MATCH_ERROR";
 extern const isc::log::MessageID EVAL_DEBUG_MEMBER = "EVAL_DEBUG_MEMBER";
 extern const isc::log::MessageID EVAL_DEBUG_NOT = "EVAL_DEBUG_NOT";
 extern const isc::log::MessageID EVAL_DEBUG_OPTION = "EVAL_DEBUG_OPTION";
@@ -73,6 +75,8 @@ const char* values[] = {
     "EVAL_DEBUG_IPADDRESS", "%1: Pushing IPAddress %2",
     "EVAL_DEBUG_IPADDRESSTOTEXT", "%1: Pushing IPAddress %2",
     "EVAL_DEBUG_LCASE", "%1: Popping string %2 and pushing converted value to lower case %3",
+    "EVAL_DEBUG_MATCH", "Matching '%1' on %2, result %3",
+    "EVAL_DEBUG_MATCH_ERROR", "Matching '%1' on %2 raised an error: %3",
     "EVAL_DEBUG_MEMBER", "%1: Checking membership of '%2', pushing result %3",
     "EVAL_DEBUG_NOT", "%1: Popping %2 pushing %3",
     "EVAL_DEBUG_OPTION", "%1: Pushing option %2 with value %3",
index 6bb42ebc3952c854befcb507f4c4e600075c786e..def72149f812936c068f7d12a2208e78a4cffc95 100644 (file)
@@ -20,6 +20,8 @@ extern const isc::log::MessageID EVAL_DEBUG_INT8TOTEXT;
 extern const isc::log::MessageID EVAL_DEBUG_IPADDRESS;
 extern const isc::log::MessageID EVAL_DEBUG_IPADDRESSTOTEXT;
 extern const isc::log::MessageID EVAL_DEBUG_LCASE;
+extern const isc::log::MessageID EVAL_DEBUG_MATCH;
+extern const isc::log::MessageID EVAL_DEBUG_MATCH_ERROR;
 extern const isc::log::MessageID EVAL_DEBUG_MEMBER;
 extern const isc::log::MessageID EVAL_DEBUG_NOT;
 extern const isc::log::MessageID EVAL_DEBUG_OPTION;
index 6a2564598394b80e16c4e579aa2355a3866ecbb8..1c62431c5160eebdfbbe6cd3fd4467a64d37fc6f 100644 (file)
@@ -84,7 +84,17 @@ address.
 This debug message indicates that the given string representation is being
 converted to lower case and pushed onto the value stack.
 
-# For use with TokenUpperCase
+# For use with TokenMatch
+
+% EVAL_DEBUG_MATCH Matching '%1' on %2, result %3
+This debug message indicates that the given regular expression was matched
+with the popped value. The result was pushed onto the value stack.
+
+% EVAL_DEBUG_MATCH_ERROR Matching '%1' on %2 raised an error: %3
+This error message indicates that the given regular expression was matched
+with the popped value. An error was raised.
+
+# For use with TokenMember
 
 % EVAL_DEBUG_MEMBER %1: Checking membership of '%2', pushing result %3
 This debug message indicates that the membership of the packet for
index 542557408dcd2498d18d45596f1059d783704494..3ab706a8022bc094e9eddf377ec93e998a6ac049 100644 (file)
@@ -1,4 +1,4 @@
-/* Copyright (C) 2015-2023 Internet Systems Consortium, Inc. ("ISC")
+/* Copyright (C) 2015-2024 Internet Systems Consortium, Inc. ("ISC")
 
    This Source Code Form is subject to the terms of the Mozilla Public
    License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -233,6 +233,7 @@ addr6 [0-9a-fA-F]*\:[0-9a-fA-F]*\:[0-9a-fA-F:.]*
 "and"          return isc::eval::EvalParser::make_AND(loc);
 "or"           return isc::eval::EvalParser::make_OR(loc);
 "member"       return isc::eval::EvalParser::make_MEMBER(loc);
+"match"        return isc::eval::EvalParser::make_MATCH(loc);
 "."            return isc::eval::EvalParser::make_DOT(loc);
 "("            return isc::eval::EvalParser::make_LPAREN(loc);
 ")"            return isc::eval::EvalParser::make_RPAREN(loc);
index 07caecd387dc0e492a2a00522d16530d78213ac8..98285853d176375f7c9c4c560acb098efa7576b4 100644 (file)
@@ -99,6 +99,7 @@ using namespace isc::eval;
   ANY "*"
   DATA "data"
   ENTERPRISE "enterprise"
+  MATCH "match"
 
   TOPLEVEL_BOOL "top-level bool"
   TOPLEVEL_STRING "top-level string"
@@ -252,6 +253,15 @@ bool_expr : "(" bool_expr ")"
                   TokenPtr member(new TokenMember(cc));
                   ctx.expression.push_back(member);
               }
+          | MATCH "(" STRING "," string_expr ")"
+              {
+                  // Expression match('<regex>', <string_expr>)
+                  //
+                  // This token will check if the regular expression matches
+                  // the string expression.
+                  TokenPtr match(new TokenMatch($3));
+                  ctx.expression.push_back(match);
+              }
           ;
 
 string_expr : STRING
index d1c658f32b5c4020effa10523773e4483b39eb30..cfadbc03189c33ad867063c1ea8b5dfdaabe8f2d 100644 (file)
@@ -548,6 +548,23 @@ public:
 
         clearStack(true);
     }
+
+    /// @brief Tests if TokenMatch works as expected.
+    ///
+    /// @param reg_exp regular expression
+    /// @param value value to match
+    /// @param matched true or false
+    void testMatch(const std::string& reg_exp, const std::string& value,
+                   bool matched) {
+        clearStack();
+
+        ASSERT_NO_THROW(t_.reset(new TokenMatch(reg_exp)));
+        values_.push(value);
+        // BTW not known easy way to get a runtime error with C++ regex.
+        ASSERT_NO_THROW(evaluate(Option::V4, matched ? "true" : "false"));
+
+        clearStack();
+    }
 };
 
 // This tests the toBool() conversions
@@ -3766,4 +3783,51 @@ TEST_F(TokenTest, splitMultipleDelims) {
     EXPECT_TRUE(checkFile());
 }
 
-};
+// Verify TokenMatch raises an error on invalid regular expression.
+TEST_F(TokenTest, invalidRegEx) {
+    try {
+        TokenMatch t("[bad");
+        ADD_FAILURE() << "expected exception, got none";
+    } catch (const EvalParseError& ex) {
+        string msg(ex.what());
+        string expected = "invalid regular expression '[bad': ";
+        EXPECT_EQ(expected, msg.substr(0, expected.size()))
+            << "expected to start with " << expected
+            << "\nbut got: " << msg;
+    } catch (const exception& ex) {
+        ADD_FAILURE() << "unexpected expection: " << ex.what();
+    }
+}
+
+// Verify TokenMatch works as expected.
+TEST_F(TokenTest, match) {
+    testMatch("foo", "foo", true);
+    // Full match is required.
+    testMatch("foo", "foobar", false);
+    testMatch("^foo", "foo", true);
+    testMatch("foo$", "foo", true);
+    testMatch("^foo$", "foo", true);
+    testMatch("foo.*", "foo", true);
+    testMatch("foo.*", "foobar", true);
+    // Case sensitive.
+    testMatch("foo", "Foo", false);
+    testMatch("Foo", "foo", false);
+    // Contain is a subcase.
+    testMatch(".*foo.*", "foo", true);
+    testMatch(".*foo.*", "foobar", true);
+    testMatch(".*foo.*", "barfoo", true);
+    testMatch(".*foo.*", "foofoofoo", true);
+    // Character range.
+    testMatch("[fo]{3}", "foo", true);
+    testMatch("[abfor]{6}", "foobar", true);
+    testMatch("[abfo]{6}", "foobar", false);
+    testMatch("[^xyz]+", "foobar", true);
+    // Group.
+    testMatch("f(.)\\1bar", "foobar", true);
+    // Classes.
+    testMatch("[[:alpha:]]{1,9}", "Foobar", true);
+    testMatch("[[:alpha:]]{1,9}", "FoObar", true);
+    testMatch("[[:alpha:]]{1,9}", "Fo0bar", false);
+}
+
+}
index b4531194877f993209fa172a11876af2696481f7..b98805a10a3d033a46ec63185a26316ba3f11685 100644 (file)
@@ -1401,3 +1401,38 @@ TokenSubOption::evaluate(Pkt& pkt, ValueStack& values) {
             .arg('\'' + txt + '\'');
     }
 }
+
+TokenMatch::TokenMatch(const std::string& reg_exp) : reg_exp_str_(reg_exp) {
+    try {
+        reg_exp_ = regex(reg_exp);
+    } catch (const exception& ex) {
+        isc_throw(EvalParseError, "invalid regular expression '" << reg_exp
+                  << "': " << ex.what());
+    }
+}
+
+void
+TokenMatch::evaluate(Pkt& pkt, ValueStack& values) {
+    if (values.size() == 0) {
+        isc_throw(EvalBadStack, "Incorrect empty stack.");
+    }
+
+    string val = values.top();
+    values.pop();
+    string txt = "false";
+    try {
+        if (regex_match(val, reg_exp_)) {
+            txt = "true";
+        }
+        LOG_DEBUG(eval_logger, EVAL_DBG_STACK, EVAL_DEBUG_MATCH)
+            .arg(reg_exp_str_)
+            .arg(val)
+            .arg(txt);
+    } catch (const exception& ex) {
+        LOG_ERROR(eval_logger, EVAL_DEBUG_MATCH_ERROR)
+            .arg(reg_exp_str_)
+            .arg(val)
+            .arg(ex.what());
+    }
+    values.push(txt);   
+}
index 1d542035979c27333cfb34713bc7e2adba74bb24..bfb8572efe7029a1e45a30043b7a8912dde7f5ff 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2015-2023 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2015-2024 Internet Systems Consortium, Inc. ("ISC")
 //
 // This Source Code Form is subject to the terms of the Mozilla Public
 // License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -9,6 +9,7 @@
 
 #include <exceptions/exceptions.h>
 #include <dhcp/pkt.h>
+#include <regex>
 #include <stack>
 
 namespace isc {
@@ -1323,6 +1324,35 @@ protected:
     uint16_t sub_option_code_; ///< Code of the sub-option to be extracted
 };
 
+/// @brief Token that represents regular expression (regex) matching
+///
+/// For example "match('foo', '_foobar_')" is true
+class TokenMatch : public Token {
+public:
+    /// @brief Constructor
+    ///
+    /// @param reg_exp regular expression string
+    /// @throw EvalParseError when the regular expression is not valid
+    TokenMatch(const std::string& reg_exp);
+
+    /// @brief Match regular expression
+    ///
+    /// Evaluation uses only the last parameter (top of stack) which is popped.
+    /// "true" when the regular expression or "false" otherwise is pushed.
+    ///
+    /// @param pkt (unused)
+    /// @param values - stack of values (1 popped, 1 pushed)
+    /// @throw EvalBadStack if there is no value on the stack
+    void evaluate(Pkt& pkt, ValueStack& values);
+
+private:
+    /// @brief The regular expression as a string.
+    std::string reg_exp_str_;
+
+    /// @brief The regular expression
+    std::regex reg_exp_;
+};
+
 } // end of isc::dhcp namespace
 } // end of isc namespace