From: Francis Dupont Date: Fri, 7 Jun 2024 20:00:36 +0000 (+0200) Subject: [#3170] Checkpoint: created and added TokenMatch X-Git-Tag: Kea-2.7.0~53 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=d955eac72fb09e3e325c8b876868853edd3801fe;p=thirdparty%2Fkea.git [#3170] Checkpoint: created and added TokenMatch --- diff --git a/src/lib/eval/eval_messages.cc b/src/lib/eval/eval_messages.cc index 04e7931b88..5b9312b4b4 100644 --- a/src/lib/eval/eval_messages.cc +++ b/src/lib/eval/eval_messages.cc @@ -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", diff --git a/src/lib/eval/eval_messages.h b/src/lib/eval/eval_messages.h index 6bb42ebc39..def72149f8 100644 --- a/src/lib/eval/eval_messages.h +++ b/src/lib/eval/eval_messages.h @@ -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; diff --git a/src/lib/eval/eval_messages.mes b/src/lib/eval/eval_messages.mes index 6a25645983..1c62431c51 100644 --- a/src/lib/eval/eval_messages.mes +++ b/src/lib/eval/eval_messages.mes @@ -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 diff --git a/src/lib/eval/lexer.ll b/src/lib/eval/lexer.ll index 542557408d..3ab706a802 100644 --- a/src/lib/eval/lexer.ll +++ b/src/lib/eval/lexer.ll @@ -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); diff --git a/src/lib/eval/parser.yy b/src/lib/eval/parser.yy index 07caecd387..98285853d1 100644 --- a/src/lib/eval/parser.yy +++ b/src/lib/eval/parser.yy @@ -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('', ) + // + // 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 diff --git a/src/lib/eval/tests/token_unittest.cc b/src/lib/eval/tests/token_unittest.cc index d1c658f32b..cfadbc0318 100644 --- a/src/lib/eval/tests/token_unittest.cc +++ b/src/lib/eval/tests/token_unittest.cc @@ -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); +} + +} diff --git a/src/lib/eval/token.cc b/src/lib/eval/token.cc index b453119487..b98805a10a 100644 --- a/src/lib/eval/token.cc +++ b/src/lib/eval/token.cc @@ -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); +} diff --git a/src/lib/eval/token.h b/src/lib/eval/token.h index 1d54203597..bfb8572efe 100644 --- a/src/lib/eval/token.h +++ b/src/lib/eval/token.h @@ -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 #include +#include #include 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