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";
"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",
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;
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
-/* 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
"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);
ANY "*"
DATA "data"
ENTERPRISE "enterprise"
+ MATCH "match"
TOPLEVEL_BOOL "top-level bool"
TOPLEVEL_STRING "top-level string"
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
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
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);
+}
+
+}
.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);
+}
-// 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
#include <exceptions/exceptions.h>
#include <dhcp/pkt.h>
+#include <regex>
#include <stack>
namespace isc {
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