blob: 515cd8f4f0bcf6f1e6e4bc728580b572bcbd8023 [file] [log] [blame]
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @file pattern.cc
* @brief Regex related classes.
* @see pattern.h
*/
#include "pattern.h"
static void
replaceString(String &str, const String &from, const String &to)
{
if (from.empty()) {
return;
}
String::size_type start_pos = 0;
while ((start_pos = str.find(from, start_pos)) != String::npos) {
str.replace(start_pos, from.length(), to);
start_pos += to.length();
}
}
Pattern::Pattern() : _pattern(""), _replacement("") {}
/**
* @brief Initializes Regex pattern by providing the subject and replacement strings.
* @param pattern Regex pattern, a string containing regex patterns, capturing groups.
* @param replacement Regex replacement, a string where $0 ... $9 will be replaced with the corresponding capturing groups
* @return true if successful, false if failure
*/
bool
Pattern::init(const String &pattern, const String &replacement, bool replace)
{
_pattern.assign(pattern);
_replacement.assign(replacement);
_replace = replace;
_tokenCount = 0;
if (!compile()) {
CacheKeyDebug("failed to initialize pattern:'%s', replacement:'%s'", pattern.c_str(), replacement.c_str());
return false;
}
return true;
}
/**
* @brief Initializes Regex pattern by providing the pattern only or pattern+replacement in a single configuration string.
* @see init()
* @param config Regex pattern <pattern> or Regex pattern + replacement in format /<pattern>/<replacement>/
* @return true if successful, false if failure
*/
bool
Pattern::init(const String &config)
{
if (config[0] == '/') {
/* This is a config in format /regex/replacement/ */
String pattern;
String replacement;
size_t start = 1;
size_t current = 0;
size_t next = 1;
do {
current = next + 1;
next = config.find_first_of('/', current);
} while (next != String::npos && '\\' == config[next - 1]);
if (next != String::npos) {
pattern = config.substr(start, next - start);
} else {
/* Error, no closing '/' */
CacheKeyError("failed to parse the pattern in '%s'", config.c_str());
return false;
}
start = next + 1;
do {
current = next + 1;
next = config.find_first_of('/', current);
} while (next != String::npos && '\\' == config[next - 1]);
if (next != String::npos) {
replacement = config.substr(start, next - start);
} else {
/* Error, no closing '/' */
CacheKeyError("failed to parse the replacement in '%s'", config.c_str());
return false;
}
// Remove '\' which escaped '/' inside the pattern and replacement strings.
::replaceString(pattern, "\\/", "/");
::replaceString(replacement, "\\/", "/");
return this->init(pattern, replacement, /* replace */ true);
} else {
return this->init(config, /* replacement */ "", /*replace */ false);
}
/* Should never get here. */
return false;
}
/**
* @brief Checks if the pattern object was initialized with a meaningful regex pattern.
* @return true if initialized, false if not.
*/
bool
Pattern::empty() const
{
return _pattern.empty() || _re.empty();
}
/**
* @brief Capture or capture-and-replace depending on whether a replacement string is specified.
* @see replace()
* @see capture()
* @param subject PCRE2 subject string
* @param result vector of strings where the result of captures or the replacements will be returned.
* @return true if there was a match and capture or replacement succeeded, false if failure.
*/
bool
Pattern::process(const String &subject, StringVector &result)
{
if (_replace) {
/* Replacement pattern was provided in the configuration - capture and replace. */
String element;
if (replace(subject, element)) {
result.push_back(element);
} else {
return false;
}
} else {
/* Replacement was not provided so return all capturing groups except the group zero. */
StringVector captures;
if (capture(subject, captures)) {
if (captures.size() == 1) {
result.push_back(captures[0]);
} else {
StringVector::iterator it = captures.begin() + 1;
for (; it != captures.end(); it++) {
result.push_back(*it);
}
}
} else {
return false;
}
}
return true;
}
/**
* @brief Regex matches a subject string against the regex pattern.
* @param subject Regex subject
* @return true - matched, false - did not.
*/
bool
Pattern::match(const String &subject)
{
CacheKeyDebug("matching '%s' to '%s'", _pattern.c_str(), subject.c_str());
if (_re.empty()) {
return false;
}
RegexMatches matches;
int matchCount = _re.exec(subject, matches, RE_NOTEMPTY);
if (matchCount < 0) {
if (matchCount != RE_ERROR_NOMATCH) {
CacheKeyError("matching error %d", matchCount);
}
return false;
}
return true;
}
/**
* @brief Return all Regex capture groups that matched in the subject string
* @param subject Regex subject string
* @param result reference to vector of strings containing all capture groups
*/
bool
Pattern::capture(const String &subject, StringVector &result)
{
CacheKeyDebug("capturing '%s' from '%s'", _pattern.c_str(), subject.c_str());
if (_re.empty()) {
CacheKeyError("regular expression not initialized");
return false;
}
RegexMatches matches;
int matchCount = _re.exec(subject, matches, RE_NOTEMPTY);
if (matchCount < 0) {
if (matchCount != RE_ERROR_NOMATCH) {
CacheKeyError("matching error %d", matchCount);
}
return false;
}
for (int i = 0; i < matchCount; i++) {
std::string_view capture = matches[i];
String dst(capture.data(), capture.length());
CacheKeyDebug("capturing '%s' %d", dst.c_str(), i);
result.push_back(dst);
}
return true;
}
/**
* @brief Replaces all replacements found in the replacement string with what matched in the Regex capturing groups.
* @param subject Regex subject string
* @param result reference to A string where the result of the replacement will be stored
* @return true - success, false - nothing matched or failure.
*/
bool
Pattern::replace(const String &subject, String &result)
{
CacheKeyDebug("replacing:'%s' in pattern:'%s', subject:'%s'", _replacement.c_str(), _pattern.c_str(), subject.c_str());
if (_re.empty() || !_replace) {
CacheKeyError("regular expression not initialized or not configured to replace");
return false;
}
RegexMatches matches;
int matchCount = _re.exec(subject, matches, RE_NOTEMPTY);
if (matchCount < 0) {
if (matchCount != RE_ERROR_NOMATCH) {
CacheKeyError("matching error %d", matchCount);
}
return false;
}
/* Verify the replacement has the right number of matching groups */
for (int i = 0; i < _tokenCount; i++) {
if (_tokens[i] >= matchCount) {
CacheKeyError("invalid reference in replacement string: $%d", _tokens[i]);
return false;
}
}
int previous = 0;
for (int i = 0; i < _tokenCount; i++) {
int replIndex = _tokens[i];
std::string_view capture = matches[replIndex];
String src(_replacement, _tokenOffset[i], 2);
String dst(capture.data(), capture.length());
CacheKeyDebug("replacing '%s' with '%s'", src.c_str(), dst.c_str());
result.append(_replacement, previous, _tokenOffset[i] - previous);
result.append(dst);
previous = _tokenOffset[i] + 2; /* 2 is the size of $0 or $1 or $2, ... or $9 */
}
result.append(_replacement, previous, _replacement.length() - previous);
CacheKeyDebug("replacing '%s' resulted in '%s'", _replacement.c_str(), result.c_str());
return true;
}
/**
* @brief Compiles the regex, called only during initialization.
* @return true if successful, false if not.
*/
bool
Pattern::compile()
{
std::string error;
int errOffset;
CacheKeyDebug("compiling pattern:'%s', replace: %s, replacement:'%s'", _pattern.c_str(), _replace ? "true" : "false",
_replacement.c_str());
if (!_re.compile(_pattern, error, errOffset)) {
CacheKeyError("compile of regex '%s' at char %d: %s", _pattern.c_str(), errOffset, error.c_str());
return false;
}
if (!_replace) {
/* No replacement necessary - we are done. */
return true;
}
_tokenCount = 0;
bool success = true;
for (unsigned i = 0; i < _replacement.length(); i++) {
if (_replacement[i] == '$') {
if (_tokenCount >= TOKENCOUNT) {
CacheKeyError("too many tokens in replacement string: %s", _replacement.c_str());
success = false;
break;
} else if (_replacement[i + 1] < '0' || _replacement[i + 1] > '9') {
CacheKeyError("invalid replacement token $%c in %s: should be $0 - $9", _replacement[i + 1], _replacement.c_str());
success = false;
break;
} else {
/* Store the location of the replacement */
/* Convert '0' to 0 */
_tokens[_tokenCount] = _replacement[i + 1] - '0';
_tokenOffset[_tokenCount] = i;
_tokenCount++;
/* Skip the next char */
i++;
}
}
}
return success;
}
/**
* @brief Destructor, deletes all patterns.
*/
MultiPattern::~MultiPattern() {}
/**
* @brief Check if empty.
* @return true if the classification contains any patterns, false otherwise
*/
bool
MultiPattern::empty() const
{
return _list.empty();
}
/**
* @brief Adds a pattern to the multi-pattern
*
* The order of addition matters during the classification
* @param pattern pattern pointer
*/
void
MultiPattern::add(std::unique_ptr<Pattern> pattern)
{
this->_list.push_back(std::move(pattern));
}
/**
* @brief Matches the subject string against all patterns.
* @param subject subject string.
* @return true if any matches, false if nothing matches.
*/
bool
MultiPattern::match(const String &subject) const
{
for (auto &p : this->_list) {
if (nullptr != p && p->match(subject)) {
return true;
}
}
return false;
}
/**
* @brief Returns the name of the multi-pattern (set during the instantiation only).
*/
const String &
MultiPattern::name() const
{
return _name;
}
bool
MultiPattern::process(const String &subject, StringVector &result) const
{
bool res = false;
for (auto &p : this->_list) {
if (nullptr != p && p->process(subject, result)) {
res = true;
}
}
return res;
}
/**
* @brief Destructor, deletes all multi-patterns.
*/
Classifier::~Classifier() {}
/**
* @brief Classifies a subject string by matching against the vector of named multi-patterns
* in the order they were added and returns the first matching multi-pattern name.
* @param subject string subject being classified.
* @param name reference to a string where the name of the class that matched first will be stored.
* @return true if something matched, false otherwise.
*/
bool
Classifier::classify(const String &subject, String &name) const
{
bool matched = false;
for (auto &p : _list) {
if (p->empty()) {
continue;
} else if (p->match(subject)) {
name = p->name();
matched = true;
break;
}
}
return matched;
}
/**
* @brief Adds a multi-pattern to the classifier.
*
* The order of addition matters during the classification
* @param pattern multi-pattern pointer
*/
void
Classifier::add(std::unique_ptr<MultiPattern> pattern)
{
_list.push_back(std::move(pattern));
}