From 16a1ccbc14c0e0edf78e83f91f7ac747f226992f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Milkovi=C4=8D?= Date: Sat, 21 Sep 2024 19:06:41 +0200 Subject: [PATCH] feat: Added support for 'with' expression --- .../yaramod/builder/yara_expression_builder.h | 3 + include/yaramod/types/expressions.h | 106 ++++++++++++++++++ include/yaramod/types/token_type.h | 1 + include/yaramod/utils/modifying_visitor.h | 75 +++++++++++++ include/yaramod/utils/observing_visitor.h | 17 +++ include/yaramod/utils/visitor.h | 4 + src/builder/yara_expression_builder.cpp | 51 +++++++++ src/examples/cpp/dump_rules_ast/dumper.h | 20 ++++ src/parser/parser_driver.cpp | 52 +++++++++ src/python/py_visitor.cpp | 14 ++- src/python/py_visitor.h | 6 + src/python/yaramod_python.cpp | 14 ++- tests/cpp/builder_tests.cpp | 34 ++++++ tests/cpp/parser_tests.cpp | 46 ++++++++ 14 files changed, 439 insertions(+), 4 deletions(-) diff --git a/include/yaramod/builder/yara_expression_builder.h b/include/yaramod/builder/yara_expression_builder.h index abfca969..466226b1 100644 --- a/include/yaramod/builder/yara_expression_builder.h +++ b/include/yaramod/builder/yara_expression_builder.h @@ -280,6 +280,9 @@ YaraExpressionBuilder none(); YaraExpressionBuilder them(); YaraExpressionBuilder regexp(const std::string& text, const std::string& suffixMods = std::string{}); + +YaraExpressionBuilder var_def(const std::string& name, const YaraExpressionBuilder& expr); +YaraExpressionBuilder with(const std::vector& vars, const YaraExpressionBuilder& body); /// @} } diff --git a/include/yaramod/types/expressions.h b/include/yaramod/types/expressions.h index 1735a85e..b56f0b88 100644 --- a/include/yaramod/types/expressions.h +++ b/include/yaramod/types/expressions.h @@ -2407,4 +2407,110 @@ class RegexpExpression : public Expression std::shared_ptr _regexp; ///< Regular expression string }; + +/** + * Class representing variable definition within with expression. + * + * For example: + * @code + * with last_section = pe.sections[pe.number_of_sections - 1] : ( ... ) + * ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + * @endcode + */ +class VariableDefExpression : public Expression +{ +public: + template + VariableDefExpression(TokenIt name, ExpPtr&& expr) + : _name(name), + _expr(std::forward(expr)) + { + } + + virtual VisitResult accept(Visitor* v) override + { + return v->visit(this); + } + + virtual TokenIt getFirstTokenIt() const override { return _name; } + virtual TokenIt getLastTokenIt() const override { return _expr->getLastTokenIt(); } + + const std::string& getName() const { return _name->getString(); } + const Expression::Ptr& getExpression() const { return _expr; } + + virtual std::string getText(const std::string& indent = std::string{}) const override + { + return getName() + " = " + _expr->getText(indent); + } + + void setExpression(const Expression::Ptr& expr) { _expr = expr; } + void setExpression(Expression::Ptr&& expr) { _expr = std::move(expr); } + +private: + TokenIt _name; + Expression::Ptr _expr; +}; + +/** + * Class representing with variable expression. + * + * For example: + * @code + * with last_section = pe.sections[pe.number_of_sections - 1] : ( ... ) + * ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + * @endcode + */ +class WithExpression : public Expression +{ +public: + template + WithExpression(TokenIt with, VarVector&& vars, ExpPtr&& body, TokenIt right_bracket) + : _with(with), + _vars(std::forward(vars)), + _body(std::forward(body)), + _right_bracket(right_bracket) + { + } + + virtual VisitResult accept(Visitor* v) override + { + return v->visit(this); + } + + virtual TokenIt getFirstTokenIt() const override { return _with; } + virtual TokenIt getLastTokenIt() const override { return _right_bracket; } + + const std::vector& getVariables() const { return _vars; } + const Expression::Ptr& getBody() const { return _body; } + + virtual std::string getText(const std::string& indent = std::string{}) const override + { + std::ostringstream ss; + ss << "with "; + for (auto itr = _vars.begin(), end = _vars.end(); itr != end; ++itr) + { + auto& expr = *itr; + ss << expr->getText(indent); + if (itr + 1 != end) + ss << ", "; + else + ss << " : "; + } + ss << "(" << _body->getText(indent) << ")"; + return ss.str(); + } + + void setVariables(const std::vector& vars) { _vars = vars; } + void setVariables(std::vector&& vars) { _vars = std::move(vars); } + + void setBody(const Expression::Ptr& body) { _body = body; } + void setBody(Expression::Ptr&& body) { _body = std::move(body); } + +private: + TokenIt _with; + std::vector _vars; + Expression::Ptr _body; + TokenIt _right_bracket; +}; + } diff --git a/include/yaramod/types/token_type.h b/include/yaramod/types/token_type.h index d37b03d3..2bb481ba 100644 --- a/include/yaramod/types/token_type.h +++ b/include/yaramod/types/token_type.h @@ -156,6 +156,7 @@ enum class TokenType INCLUDE_PATH, FUNCTION_CALL_LP, FUNCTION_CALL_RP, + WITH, INVALID, }; diff --git a/include/yaramod/utils/modifying_visitor.h b/include/yaramod/utils/modifying_visitor.h index 3e9d59e3..4d84ab7d 100644 --- a/include/yaramod/utils/modifying_visitor.h +++ b/include/yaramod/utils/modifying_visitor.h @@ -427,6 +427,27 @@ class ModifyingVisitor : public Visitor } virtual VisitResult visit(RegexpExpression*) override { return {}; } + + virtual VisitResult visit(VariableDefExpression* expr) override + { + TokenStreamContext context{expr}; + auto varExpr = expr->getExpression()->accept(this); + return defaultHandler(context, expr, varExpr); + } + + virtual VisitResult visit(WithExpression* expr) override + { + TokenStreamContext context{expr}; + + std::vector variables; + for (auto& var : expr->getVariables()) + { + variables.push_back(var->accept(this)); + } + + auto body = expr->getBody()->accept(this); + return defaultHandler(context, expr, variables, body); + } /// @} /// @name Default handlers @@ -804,6 +825,60 @@ class ModifyingVisitor : public Visitor return {}; } + + VisitResult defaultHandler(const TokenStreamContext& context, VariableDefExpression* expr, const VisitResult& exprRet) + { + if (auto newExpr = std::get_if(&exprRet)) + { + if (*newExpr) + expr->setExpression(*newExpr); + } + else + expr->setExpression(nullptr); + + if (!expr->getExpression()) + return VisitAction::Delete; + + return {}; + } + + VisitResult defaultHandler(const TokenStreamContext& context, WithExpression* expr, const std::vector& varsRet, const VisitResult& bodyRet) + { + if (auto body = std::get_if(&bodyRet)) + { + if (*body) + expr->setBody(*body); + } + else + expr->setBody(nullptr); + + if (!expr->getBody()) + return VisitAction::Delete; + + if (std::all_of(varsRet.begin(), varsRet.end(), + [](const auto& var) { + auto a = std::get_if(&var); + return a && (*a == nullptr); + })) + { + return {}; + } + + std::vector newVariables; + for (std::size_t i = 0, end = varsRet.size(); i < end; ++i) + { + if (auto var = std::get_if(&varsRet[i])) + { + if (*var) + newVariables.push_back(*var); + else + newVariables.push_back(expr->getVariables()[i]); + } + } + + expr->setVariables(newVariables); + return {}; + } /// @} /** diff --git a/include/yaramod/utils/observing_visitor.h b/include/yaramod/utils/observing_visitor.h index 3473b521..0b9bb808 100644 --- a/include/yaramod/utils/observing_visitor.h +++ b/include/yaramod/utils/observing_visitor.h @@ -380,6 +380,23 @@ class ObservingVisitor : public Visitor } virtual VisitResult visit(RegexpExpression*) override { return {}; } + + virtual VisitResult visit(VariableDefExpression* expr) override + { + expr->getExpression()->accept(this); + return {}; + } + + virtual VisitResult visit(WithExpression* expr) override + { + for (const auto& var : expr->getVariables()) + { + var->accept(this); + } + + expr->getBody()->accept(this); + return {}; + } /// @} protected: diff --git a/include/yaramod/utils/visitor.h b/include/yaramod/utils/visitor.h index f10aa87b..bcc42b74 100644 --- a/include/yaramod/utils/visitor.h +++ b/include/yaramod/utils/visitor.h @@ -75,6 +75,8 @@ class ThemExpression; class ParenthesesExpression; class IntFunctionExpression; class RegexpExpression; +class VariableDefExpression; +class WithExpression; /** * Abstract class representing visitor design pattern for visiting condition expressions @@ -149,6 +151,8 @@ class Visitor virtual VisitResult visit(ParenthesesExpression* expr) = 0; virtual VisitResult visit(IntFunctionExpression* expr) = 0; virtual VisitResult visit(RegexpExpression* expr) = 0; + virtual VisitResult visit(VariableDefExpression* expr) = 0; + virtual VisitResult visit(WithExpression* expr) = 0; /// @} bool resultIsDelete(const VisitResult& result) const { diff --git a/src/builder/yara_expression_builder.cpp b/src/builder/yara_expression_builder.cpp index 61a799ee..88f0871b 100644 --- a/src/builder/yara_expression_builder.cpp +++ b/src/builder/yara_expression_builder.cpp @@ -1614,4 +1614,55 @@ YaraExpressionBuilder regexp(const std::string& text, const std::string& suffixM return YaraExpressionBuilder(std::move(ts), std::make_shared(std::move(regexp)), Expression::Type::Regexp); } +/** + * Creates variable definition for "with" expression. + * + * @param name Name of the token. + * @param expr Expression. + * + * @return Builder. + */ +YaraExpressionBuilder var_def(const std::string& name, const YaraExpressionBuilder& expr) +{ + std::shared_ptr ts = std::make_shared(); + auto name_token = ts->emplace_back(TokenType::ID, name); + ts->emplace_back(TokenType::ASSIGN, "="); + ts->moveAppend(expr.getTokenStream()); + return YaraExpressionBuilder(std::move(ts), std::make_shared(name_token, expr.get())); +} + +/** + * Creates the expression with "with" expression defining variables and body. + * + * @param vars Variable builders. + * @param body Body expression builder. + * + * @return Builder. + */ +YaraExpressionBuilder with(const std::vector& vars, const YaraExpressionBuilder& body) +{ + std::shared_ptr ts = std::make_shared(); + TokenIt with = ts->emplace_back(TokenType::WITH, "with"); + + std::vector varExprs; + varExprs.reserve(vars.size()); + + for (size_t i = 0; i < vars.size(); ++i) + { + ts->moveAppend(vars[i].getTokenStream()); + + if (i + 1 < vars.size()) + ts->emplace_back(TokenType::COMMA, ","); + else + ts->emplace_back(TokenType::COLON, ":"); + + varExprs.push_back(vars[i].get()); + } + + ts->emplace_back(TokenType::LP_WITH_SPACE_AFTER, "("); + ts->moveAppend(body.getTokenStream()); + auto right_bracket = ts->emplace_back(TokenType::RP_WITH_SPACE_BEFORE, ")"); + return YaraExpressionBuilder(std::move(ts), std::make_shared(with, std::move(varExprs), body.get(), right_bracket)); +} + } diff --git a/src/examples/cpp/dump_rules_ast/dumper.h b/src/examples/cpp/dump_rules_ast/dumper.h index 8201f759..0ca927db 100644 --- a/src/examples/cpp/dump_rules_ast/dumper.h +++ b/src/examples/cpp/dump_rules_ast/dumper.h @@ -587,6 +587,26 @@ class Dumper : public yaramod::ObservingVisitor, public yaramod::ObservingRegexp return {}; } + virtual yaramod::VisitResult visit(yaramod::VariableDefExpression* expr) override + { + dump("VariableDef", expr, " name=", expr->getName()); + indentUp(); + expr->getExpression()->accept(this); + indentDown(); + return {}; + } + + virtual yaramod::VisitResult visit(yaramod::WithExpression* expr) override + { + dump("With", expr); + indentUp(); + for (const auto& var : expr->getVariables()) + var->accept(this); + expr->getBody()->accept(this); + indentDown(); + return {}; + } + // ==================== ObservingRegexVisitor ==================== yaramod::RegexpVisitResult observe(const std::shared_ptr& unit) { diff --git a/src/parser/parser_driver.cpp b/src/parser/parser_driver.cpp index 2c6360c8..7c74f60b 100644 --- a/src/parser/parser_driver.cpp +++ b/src/parser/parser_driver.cpp @@ -164,6 +164,7 @@ void ParserDriver::defineTokens() _parser.token("endswith").symbol("ENDSWITH").description("endswith").action([&](std::string_view str) -> Value { return emplace_back(TokenType::ENDSWITH, std::string{str}); }); _parser.token("iendswith").symbol("IENDSWITH").description("iendswith").action([&](std::string_view str) -> Value { return emplace_back(TokenType::IENDSWITH, std::string{str}); }); _parser.token("iequals").symbol("IEQUALS").description("iequals").action([&](std::string_view str) -> Value { return emplace_back(TokenType::IEQUALS, std::string{str}); }); + _parser.token("with").symbol("WITH").description("with").action([&](std::string_view str) -> Value { return emplace_back(TokenType::WITH, std::string{str}); }); // $include _parser.token("include").symbol("INCLUDE_DIRECTIVE").description("include").enter_state("$include").action([&](std::string_view str) -> Value { @@ -1615,9 +1616,24 @@ void ParserDriver::defineGrammar() output->setUid(_uidGen.next()); return output; }) + .production("WITH", "with_variables", "COLON", "LP", "expression", "RP", [&](auto&& args) -> Value { + auto vars = std::move(args[1].getMultipleExpressions()); + auto body = std::move(args[4].getExpression()); + auto type = body->getType(); + for (const auto& var : vars) + { + removeLocalSymbol(std::static_pointer_cast(var)->getName()); + } + auto output = std::make_shared(args[0].getTokenIt(), std::move(vars), std::move(body), args[5].getTokenIt()); + output->setType(type); + output->setTokenStream(currentTokenStream()); + output->setUid(_uidGen.next()); + return output; + }) ; if (_features & Features::AvastOnly) + { expr.production("for_expression", "OF", "expression_iterable", [this](auto&& args) -> Value { auto for_expr = std::move(args[0].getExpression()); TokenIt of = args[1].getTokenIt(); @@ -1629,6 +1645,42 @@ void ParserDriver::defineGrammar() return output; }) ; + } + + _parser.rule("with_variables") + .production("with_variable", [](auto&& args) -> Value { + std::vector vars; + vars.push_back(std::move(args[0].getExpression())); + return vars; + }) + .production("with_variables", "COMMA", "with_variable", [](auto&& args) -> Value { + auto vars = std::move(args[0].getMultipleExpressions()); + vars.push_back(std::move(args[2].getExpression())); + return vars; + }) + ; + + _parser.rule("with_variable") + .production("ID", "ASSIGN", "expression", [&](auto&& args) -> Value { + TokenIt name = args[0].getTokenIt(); + auto expr = args[2].getExpression(); + + std::shared_ptr symbol; + if (expr->isObject()) + symbol = std::make_shared(name->getString(), std::static_pointer_cast(expr)->getSymbol()); + else + symbol = std::make_shared(name->getString(), expr->getType()); + + if (!addLocalSymbol(symbol)) + { + error_handle(currentFileContext()->getLocation(), "Redefinition of identifier " + name->getString()); + } + + auto output = std::make_shared(std::move(name), std::move(expr)); + output->setUid(_uidGen.next()); + return output; + }) + ; _parser.rule("primary_expression") // Expression::Ptr .production("LP", "primary_expression", "RP", [&](auto&& args) -> Value { diff --git a/src/python/py_visitor.cpp b/src/python/py_visitor.cpp index 92ccdc47..33bdbaa2 100644 --- a/src/python/py_visitor.cpp +++ b/src/python/py_visitor.cpp @@ -81,7 +81,9 @@ void addVisitorClasses(py::module& module) .def("visit_ThemExpression", py::overload_cast(&Visitor::visit)) .def("visit_ParenthesesExpression", py::overload_cast(&Visitor::visit)) .def("visit_IntFunctionExpression", py::overload_cast(&Visitor::visit)) - .def("visit_RegexpExpression", py::overload_cast(&Visitor::visit)); + .def("visit_RegexpExpression", py::overload_cast(&Visitor::visit)) + .def("visit_VariableDefExpression", py::overload_cast(&Visitor::visit)) + .def("visit_WithExpression", py::overload_cast(&Visitor::visit)); py::class_(module, "ObservingVisitor") .def(py::init<>()) @@ -148,7 +150,9 @@ void addVisitorClasses(py::module& module) .def("visit_ThemExpression", py::overload_cast(&ObservingVisitor::visit)) .def("visit_ParenthesesExpression", py::overload_cast(&ObservingVisitor::visit)) .def("visit_IntFunctionExpression", py::overload_cast(&ObservingVisitor::visit)) - .def("visit_RegexpExpression", py::overload_cast(&ObservingVisitor::visit)); + .def("visit_RegexpExpression", py::overload_cast(&ObservingVisitor::visit)) + .def("visit_VariableDefExpression", py::overload_cast(&ObservingVisitor::visit)) + .def("visit_WithExpression", py::overload_cast(&ObservingVisitor::visit)); py::class_(module, "ModifyingVisitor") .def(py::init<>()) @@ -217,6 +221,8 @@ void addVisitorClasses(py::module& module) .def("visit_ParenthesesExpression", py::overload_cast(&ModifyingVisitor::visit)) .def("visit_IntFunctionExpression", py::overload_cast(&ModifyingVisitor::visit)) .def("visit_RegexpExpression", py::overload_cast(&ModifyingVisitor::visit)) + .def("visit_VariableDefExpression", py::overload_cast(&ModifyingVisitor::visit)) + .def("visit_WithExpression", py::overload_cast(&ModifyingVisitor::visit)) .def("default_handler", static_cast(&ModifyingVisitor::defaultHandler)) .def("default_handler", static_cast(&ModifyingVisitor::defaultHandler)) .def("default_handler", static_cast(&ModifyingVisitor::defaultHandler)) @@ -263,7 +269,9 @@ void addVisitorClasses(py::module& module) .def("default_handler", static_cast(&ModifyingVisitor::defaultHandler)) .def("default_handler", static_cast&)>(&ModifyingVisitor::defaultHandler)) .def("default_handler", static_cast(&ModifyingVisitor::defaultHandler)) - .def("default_handler", static_cast(&ModifyingVisitor::defaultHandler)); + .def("default_handler", static_cast(&ModifyingVisitor::defaultHandler)) + .def("default_handler", static_cast(&ModifyingVisitor::defaultHandler)) + .def("default_handler", static_cast&, const VisitResult&)>(&ModifyingVisitor::defaultHandler)); } void addRegexpVisitorClasses(py::module& module) diff --git a/src/python/py_visitor.h b/src/python/py_visitor.h index ce3169e9..7754e9fe 100644 --- a/src/python/py_visitor.h +++ b/src/python/py_visitor.h @@ -91,6 +91,8 @@ class PyVisitor : public yaramod::Visitor PURE_VISIT(ParenthesesExpression) PURE_VISIT(IntFunctionExpression) PURE_VISIT(RegexpExpression) + PURE_VISIT(VariableDefExpression) + PURE_VISIT(WithExpression) }; #define VISIT(parent, type) \ @@ -172,6 +174,8 @@ class PyObservingVisitor : public yaramod::ObservingVisitor VISIT(ObservingVisitor, ParenthesesExpression) VISIT(ObservingVisitor, IntFunctionExpression) VISIT(ObservingVisitor, RegexpExpression) + VISIT(ObservingVisitor, VariableDefExpression) + VISIT(ObservingVisitor, WithExpression) }; class PyModifyingVisitor : public yaramod::ModifyingVisitor @@ -242,6 +246,8 @@ class PyModifyingVisitor : public yaramod::ModifyingVisitor VISIT(ModifyingVisitor, ParenthesesExpression) VISIT(ModifyingVisitor, IntFunctionExpression) VISIT(ModifyingVisitor, RegexpExpression) + VISIT(ModifyingVisitor, VariableDefExpression) + VISIT(ModifyingVisitor, WithExpression) }; void addVisitorClasses(pybind11::module& module); diff --git a/src/python/yaramod_python.cpp b/src/python/yaramod_python.cpp index ff0c3974..63f7b4c8 100644 --- a/src/python/yaramod_python.cpp +++ b/src/python/yaramod_python.cpp @@ -263,7 +263,8 @@ void addEnums(py::module& module) .value("IncludePath", TokenType::INCLUDE_PATH) .value("FunctionCallLp", TokenType::FUNCTION_CALL_LP) .value("FunctionCallRp", TokenType::FUNCTION_CALL_RP) - .value("Invalid", TokenType::INVALID); + .value("Invalid", TokenType::INVALID) + .value("With", TokenType::WITH); } void addBasicClasses(py::module& module) @@ -792,6 +793,14 @@ void addExpressionClasses(py::module& module) .def_property("regexp_string", &RegexpExpression::getRegexpString, py::overload_cast&>(&RegexpExpression::setRegexpString)); + + exprClass(module, "WithExpression") + .def_property("variables", + &WithExpression::getVariables, + py::overload_cast&>(&WithExpression::setVariables)) + .def_property("body", + &WithExpression::getBody, + py::overload_cast(&WithExpression::setBody)); } void addBuilderClasses(py::module& module) @@ -975,6 +984,9 @@ void addBuilderClasses(py::module& module) module.def("regexp", ®exp); + module.def("var_def", &var_def); + module.def("with_", &with); + py::class_(module, "YaraHexStringBuilder") .def(py::init<>()) .def(py::init()) diff --git a/tests/cpp/builder_tests.cpp b/tests/cpp/builder_tests.cpp index 859e6f32..8fe01ef7 100644 --- a/tests/cpp/builder_tests.cpp +++ b/tests/cpp/builder_tests.cpp @@ -2379,5 +2379,39 @@ rule endswith_builder )", yaraFile->getTextFormatted()); } +TEST_F(BuilderTests, +WithWorks) { + auto cond = with({ + var_def("a", intVal(5)), + var_def("b", id("a") + intVal(10)), + }, + id("a") < id("b") + ).get(); + + YaraRuleBuilder newRule; + auto rule = newRule + .withName("with_builder") + .withCondition(cond) + .get(); + + YaraFileBuilder newFile; + auto yaraFile = newFile + .withRule(std::move(rule)) + .get(true); + + ASSERT_NE(nullptr, yaraFile); + EXPECT_EQ(R"(rule with_builder { + condition: + with a = 5, b = a + 10 : (a < b) +})", yaraFile->getText()); + + EXPECT_EQ(R"(rule with_builder +{ + condition: + with a = 5, b = a + 10 : ( a < b ) +} +)", yaraFile->getTextFormatted()); +} + } } diff --git a/tests/cpp/parser_tests.cpp b/tests/cpp/parser_tests.cpp index 957dc002..9a2304e0 100644 --- a/tests/cpp/parser_tests.cpp +++ b/tests/cpp/parser_tests.cpp @@ -8517,5 +8517,51 @@ rule module_rule EXPECT_EQ(input_text, driver.getParsedFile().getTextFormatted()); } +TEST_F(ParserTests, +WithExpressionWorks) { + prepareInput( +R"( +rule with_expression +{ + condition: + with a = 1, b = 2, c = 3 + 5 : ( + a + b > a + c + ) +} +)"); + + EXPECT_TRUE(driver.parse(input)); + ASSERT_EQ(1u, driver.getParsedFile().getRules().size()); + + const auto& rule = driver.getParsedFile().getRules()[0]; + EXPECT_EQ(R"(with a = 1, b = 2, c = 3 + 5 : (a + b > a + c))", rule->getCondition()->getText()); + + EXPECT_EQ(input_text, driver.getParsedFile().getTextFormatted()); +} + +TEST_F(ParserTests, +WithExpressionVariableOutOfScope) { + prepareInput( + R"(rule test_rule +{ + condition: + with a = 1 : ( + a > 2 + ) and a < 1 +} +)"); + + try + { + driver.parse(input); + FAIL() << "Parser did not throw an exception."; + } + catch (const ParserError& err) + { + EXPECT_EQ("Error at 6.9: Unrecognized identifier 'a' referenced", err.getErrorMessage()); + EXPECT_EQ("<", driver.getParsedFile().getTokenStream()->back().getPureText()); + } +} + } }