Repository: avast/yaramod (language: python)

Issue:
Escaped characters with operators are parsed incorrectly
I've come across an issue where hex-encoded characters with operators are processed incorrectly.
For example, `\x31+`: instead of applying the operator to `\x31` it's incorrectly applied only to `1` and the rest of the character is converted to `RegexpText`.

Installation source: master (`1acefa3`)
POC:

```python
import yaramod

y = yaramod.Yaramod(yaramod.Features.AllCurrent)
yara_file = y.parse_string(r'''
rule regex_test {
    strings:
        $a = /1\x32?3/
    condition:
        $a
}''')
for rule in yara_file.rules:
    for string in rule.strings:
        for unit in string.unit.units:
            print(type(unit), unit.text)
```

```
<class 'yaramod.RegexpText'> 1
<class 'yaramod.RegexpText'> \x
<class 'yaramod.RegexpText'> 3
<class 'yaramod.RegexpOptional'> 2?
<class 'yaramod.RegexpText'> 3
```
Escaped characters with operators are parsed incorrectly
I've come across an issue where hex-encoded characters with operators are processed incorrectly.
For example, `\x31+`: instead of applying the operator to `\x31` it's incorrectly applied only to `1` and the rest of the character is converted to `RegexpText`.

Installation source: master (`1acefa3`)
POC:

```python
import yaramod

y = yaramod.Yaramod(yaramod.Features.AllCurrent)
yara_file = y.parse_string(r'''
rule regex_test {
    strings:
        $a = /1\x32?3/
    condition:
        $a
}''')
for rule in yara_file.rules:
    for string in rule.strings:
        for unit in string.unit.units:
            print(type(unit), unit.text)
```

```
<class 'yaramod.RegexpText'> 1
<class 'yaramod.RegexpText'> \x
<class 'yaramod.RegexpText'> 3
<class 'yaramod.RegexpOptional'> 2?
<class 'yaramod.RegexpText'> 3
```


Discussion / hints:

Fix patch:
diff --git a/src/parser/parser_driver.cpp b/src/parser/parser_driver.cpp
index c71fb641..47d46d72 100644
--- a/src/parser/parser_driver.cpp
+++ b/src/parser/parser_driver.cpp
@@ -375,6 +375,9 @@ void ParserDriver::defineTokens()
 
 		return std::make_pair(range, range);
 	});
+	_parser.token(R"(\\x[a-zA-Z0-9]{2})").states("$regexp").symbol("REGEXP_ESCAPE").description("regexp escaped character").action([](std::string_view str) -> Value {
+		return std::string{str};
+	});
 	_parser.token(R"([^\\\[\(\)\|\$\.\^\+\+*\?])").states("$regexp").symbol("REGEXP_CHAR").description("regexp character").action([](std::string_view str) -> Value {
 		return std::string{str};
 	});
@@ -1014,6 +1017,7 @@ void ParserDriver::defineGrammar()
 	_parser.rule("regexp_single") // shared_ptr<yaramod::RegexpUnit>
 		.production("LP", "regexp_or", "RP", [](auto&& args) -> Value { return Value(std::make_shared<RegexpGroup>(std::move(args[1].getRegexpUnit()))); })
 		.production("REGEXP_ANY_CHAR", [](auto&&) -> Value { return Value(std::make_shared<RegexpAnyChar>()); })
+		.production("REGEXP_ESCAPE", [](auto&& args) -> Value { return Value(std::make_shared<RegexpText>(std::move(args[0].getString()))); })
 		.production("REGEXP_CHAR", [](auto&& args) -> Value { return Value(std::make_shared<RegexpText>(std::move(args[0].getString()))); })
 		.production("REGEXP_WORD_CHAR", [](auto&&) -> Value { return Value(std::make_shared<RegexpWordChar>()); })
 		.production("REGEXP_NON_WORD_CHAR", [](auto&&) -> Value { return Value(std::make_shared<RegexpNonWordChar>()); })


Test patch:
diff --git a/tests/cpp/parser_tests.cpp b/tests/cpp/parser_tests.cpp
index dc90e7ba..77134c7e 100644
--- a/tests/cpp/parser_tests.cpp
+++ b/tests/cpp/parser_tests.cpp
@@ -1300,6 +1300,37 @@ rule regexp_with_custom_negative_class
 	EXPECT_EQ(input_text, driver.getParsedFile().getTextFormatted());
 }
 
+TEST_F(ParserTests,
+RegexpWithOptionalEscapedCharsWorks) {
+	prepareInput(
+R"(
+rule regexp_with_optional_escaped_chars
+{
+	strings:
+		$1 = /1\x32?3/
+	condition:
+		$1
+}
+)");
+
+	EXPECT_TRUE(driver.parse(input));
+	ASSERT_EQ(1u, driver.getParsedFile().getRules().size());
+
+	const auto& rule = driver.getParsedFile().getRules()[0];
+	EXPECT_EQ("regexp_with_optional_escaped_chars", rule->getName());
+	EXPECT_EQ(Rule::Modifier::None, rule->getModifier());
+
+	auto strings = rule->getStrings();
+	ASSERT_EQ(1u, strings.size());
+
+	auto regexp = strings[0];
+	EXPECT_TRUE(regexp->isRegexp());
+	EXPECT_EQ("$1", regexp->getIdentifier());
+	EXPECT_EQ(R"(/1\x32?3/)", regexp->getText());
+
+	EXPECT_EQ(input_text, driver.getParsedFile().getTextFormatted());
+}
+
 TEST_F(ParserTests,
 RegexpWithEscapedSquareBracketsInsideClassWorks) {
 	prepareInput(