rizin/librz/util/regex.c
Rot127 d90576e09a Add rz_regex_new_bytes(), rz_regex_match_all_overlap() and rz_regex_parse_flag_desc().
- rz_regex_new_bytes() doesn't set the UTF flag and allows to match raw bytes.
- rz_regex_match_all_overlap() also matches overlapping results.
- rz_regex_parse_flag_desc() parses a string of characters and returns regex flags according to the chars in it.
2025-02-14 00:27:53 +08:00

651 lines
20 KiB
C

// SPDX-FileCopyrightText: 2023 Rot127 <unisono@quyllur.org>
// SPDX-License-Identifier: LGPL-3.0-only
#define PCRE2_STATIC
#define PCRE2_CODE_UNIT_WIDTH 8
#include <pcre2.h>
#include <rz_util/rz_strbuf.h>
#include <rz_vector.h>
#include <rz_util/rz_regex.h>
#include <rz_types.h>
#include <rz_util/rz_assert.h>
#include <rz_util.h>
typedef pcre2_general_context RzRegexGeneralContext; ///< General context.
// typedef pcre2_compile_context RzRegexCompContext; ///< The context for compiling.
typedef pcre2_match_context RzRegexMatchContext; ///< The context for matching.
typedef struct {
RzRegexGeneralContext *general;
RzRegexCompContext *compile;
RzRegexMatchContext *match;
} RzRegexContexts;
static void print_pcre2_err(RZ_NULLABLE const char *pattern, RzRegexStatus err_num, size_t err_off) {
PCRE2_UCHAR buffer[256];
pcre2_get_error_message(err_num, buffer, sizeof(buffer));
RZ_LOG_ERROR("Regex compilation for '%s' failed at %" PFMTSZu ": %s\n", pattern ? pattern : "(null)", err_off,
buffer);
}
/**
* \brief Compile a regex pattern to a RzRegex and return it.
* In case of an error, an error message is printed and NULL is returned.
*
* \param pattern The regex pattern string.
* \param cflags The compilation flags or zero for default.
* \param jflags The compilation flags for the JIT compiler.
* You can pass RZ_REGEX_JIT_PARTIAL_SOFT or RZ_REGEX_JIT_PARTIAL_HARD if you
* intend to use the pattern for partial matching. Otherwise set it to 0.
* \param ccontext A compile context or NULL.
*
* \return The compiled regex or NULL in case of failure.
*/
RZ_API RZ_OWN RzRegex *rz_regex_new(RZ_NONNULL const char *pattern, RzRegexFlags cflags, RzRegexFlags jflags,
RzRegexCompContext *ccontext) {
rz_return_val_if_fail(pattern, NULL);
RzRegexStatus err_num;
RzRegexSize err_off;
ut32 supported = 0;
pcre2_config(PCRE2_CONFIG_UNICODE, &supported);
if (supported != 1) {
RZ_LOG_ERROR("Unicode not supported by PCRE2 library.\n");
return NULL;
}
char *fixed_pat = NULL;
const char *pat = NULL;
if ((cflags & RZ_REGEX_EXTENDED) || (cflags & RZ_REGEX_EXTENDED_MORE)) {
if (!strchr(pattern, ' ')) {
pat = pattern;
} else {
// In PCRE2 with the extended flag set, ascii space characters ' ' are skipped.
// We need to replace them with \s unfortunately to keep our API stable.
fixed_pat = rz_str_replace(rz_str_dup(pattern), " ", "\\s", 1);
pat = fixed_pat;
}
} else {
pat = pattern;
}
RzRegex *regex = pcre2_compile(
(PCRE2_SPTR)pat,
PCRE2_ZERO_TERMINATED,
cflags | PCRE2_UTF | PCRE2_MATCH_INVALID_UTF,
&err_num,
&err_off,
ccontext);
if (!regex) {
print_pcre2_err(pat, err_num, err_off);
free(fixed_pat);
return NULL;
}
#ifdef SUPPORTS_PCRE2_JIT
RzRegexStatus jit_err = pcre2_jit_compile(regex, jflags | PCRE2_JIT_COMPLETE);
if (jit_err < 0) {
print_pcre2_err(pat, jit_err, 0);
}
#endif
free(fixed_pat);
return regex;
}
/**
* \brief Compile a regex pattern with raw bytes.
* In case of an error, an error message is printed and NULL is returned.
*
* Unlike `rz_regex_new()` it doesn't compile the pattern with the PCRE2_UTF flag.
* So any byte can be part of a pattern, not just UTF-8 compatible ones.
*
* \param pattern The pattern. Bytes must not be escaped.
* \param pattern_len The pattern length.
* \param cflags The compilation flags.
* \param jflags The compilation flags for the JIT compiler.
* You can pass RZ_REGEX_JIT_PARTIAL_SOFT or RZ_REGEX_JIT_PARTIAL_HARD if you
* intend to use the pattern for partial matching. Otherwise set it to 0.
* \param ccontext A compile context or NULL.
*
* \return The compiled regex or NULL in case of failure.
*/
RZ_API RZ_OWN RzRegex *rz_regex_new_bytes(RZ_NONNULL const ut8 *pattern, size_t pattern_len, RzRegexFlags cflags, RzRegexFlags jflags,
RzRegexCompContext *ccontext) {
rz_return_val_if_fail(pattern, NULL);
RzRegexStatus err_num;
RzRegexSize err_off;
RzRegex *regex = pcre2_compile(
pattern,
pattern_len,
cflags,
&err_num,
&err_off,
ccontext);
if (!regex) {
print_pcre2_err((const char *)pattern, err_num, err_off);
return NULL;
}
#ifdef SUPPORTS_PCRE2_JIT
RzRegexStatus jit_err = pcre2_jit_compile(regex, jflags | PCRE2_JIT_COMPLETE);
if (jit_err < 0) {
print_pcre2_err((const char *)pattern, jit_err, 0);
}
#endif
return regex;
}
/**
* \brief Frees a given RzRegex.
*
* \param regex The RzRegex to free.
*/
RZ_API void rz_regex_free(RZ_OWN RzRegex *regex) {
pcre2_code_free(regex);
}
static void rz_regex_match_data_free(RZ_OWN RzRegexMatchData *match_data) {
pcre2_match_data_free(match_data);
}
/**
* \brief Matches the \p regex in the \p text and returns a status code with the result.
*
* \param regex The regex pattern to match.
* \param text The text to search in.
* \param text_size The length of the buffer pointed to by \p text.
* Can be set to RZ_REGEX_ZERO_TERMINATED if the buffer is a zero terminated string.
* \param text_offset The offset into \p text from where the search starts.
* \param mflags Match flags.
*
* \return A status code which describes the result.
*/
RZ_API RzRegexStatus rz_regex_match(RZ_NONNULL const RzRegex *regex, RZ_NONNULL const char *text,
RzRegexSize text_size,
RzRegexSize text_offset,
RzRegexFlags mflags) {
rz_return_val_if_fail(regex && text, RZ_REGEX_ERROR_NOMATCH);
pcre2_match_data *mdata = pcre2_match_data_create_from_pattern(regex, NULL);
RzRegexStatus rc = pcre2_match(regex, (PCRE2_SPTR)text, text_size, text_offset, mflags | PCRE2_NO_UTF_CHECK, mdata, NULL);
pcre2_match_data_free(mdata);
return rc;
}
/**
* \brief Generates the error message to \p errcode.
*
* \param errcode The error code.
* \param errbuf The error message buffer.
* \param errbuf_size The error message buffer size in bytes.
*/
RZ_API void rz_regex_error_msg(RzRegexStatus errcode, RZ_OUT char *errbuf, RzRegexSize errbuf_size) {
pcre2_get_error_message(errcode, (PCRE2_UCHAR *)errbuf, errbuf_size);
}
/**
* \brief Returns the name of a group.
*
* \param regex The regex expression with named groups.
* \param group_idx The index of the group to get the name for.
*
* \return The name of the group or NULL in case of failure or non is was set.
*/
RZ_API const ut8 *rz_regex_get_match_name(RZ_NONNULL const RzRegex *regex, ut32 group_idx) {
rz_return_val_if_fail(regex, NULL);
ut32 namecount;
ut32 name_entry_size;
PCRE2_SPTR nametable_ptr;
pcre2_pattern_info(
regex,
PCRE2_INFO_NAMECOUNT,
&namecount);
pcre2_pattern_info(
regex,
PCRE2_INFO_NAMETABLE,
&nametable_ptr);
pcre2_pattern_info(
regex,
PCRE2_INFO_NAMEENTRYSIZE,
&name_entry_size);
for (size_t i = 0; i < namecount; i++) {
int n = (nametable_ptr[0] << 8) | nametable_ptr[1];
if (n == group_idx) {
return nametable_ptr + 2;
}
nametable_ptr += name_entry_size;
}
return NULL;
}
/**
* \brief Returns the index of a group in the pattern \p regex.
*
* \param regex The regex expression with named groups.
* \param group The group name to get the index for.
*
* \return The index of the group or RZ_REGEX_ERROR_NOMATCH in case of failure or if no name was given.
*/
RZ_API RzRegexStatus rz_regex_get_group_idx_by_name(RZ_NONNULL const RzRegex *regex, const char *group) {
rz_return_val_if_fail(regex, RZ_REGEX_ERROR_NOMATCH);
ut32 namecount;
ut32 name_entry_size;
PCRE2_SPTR nametable_ptr;
pcre2_pattern_info(
regex,
PCRE2_INFO_NAMECOUNT,
&namecount);
pcre2_pattern_info(
regex,
PCRE2_INFO_NAMETABLE,
&nametable_ptr);
pcre2_pattern_info(
regex,
PCRE2_INFO_NAMEENTRYSIZE,
&name_entry_size);
for (size_t i = 0; i < namecount; i++) {
int n = (nametable_ptr[0] << 8) | nametable_ptr[1];
if (RZ_STR_EQ((const char *)nametable_ptr + 2, group)) {
return n;
}
nametable_ptr += name_entry_size;
}
return RZ_REGEX_ERROR_NOMATCH;
}
/**
* \brief Finds the first match in a text and returns it as a pvector.
* First element in the vector is always the whole match, the following possible groups.
*
* \param regex The regex pattern to match.
* \param text The text to search in.
* \param text_size The length of the buffer pointed to by \p text.
* Can be set to RZ_REGEX_ZERO_TERMINATED if the buffer is a zero terminated string.
* \param text_offset The offset into \p text from where the search starts.
* \param mflags Match flags.
*
* \return The matches as pvector. NULL in case of failure. Empty for no matches or regex related errors.
*/
RZ_API RZ_OWN RzPVector /*<RzRegexMatch *>*/ *rz_regex_match_first(
RZ_NONNULL const RzRegex *regex,
RZ_NONNULL const char *text,
RzRegexSize text_size,
RzRegexSize text_offset,
RzRegexFlags mflags) {
rz_return_val_if_fail(regex && text, NULL);
RzPVector *matches = rz_pvector_new(free);
RzRegexMatchData *mdata = pcre2_match_data_create_from_pattern(regex, NULL);
RzRegexStatus rc = pcre2_match(regex, (PCRE2_SPTR)text, text_size, text_offset, mflags | PCRE2_NO_UTF_CHECK, mdata, NULL);
if (rc == PCRE2_ERROR_NOMATCH) {
// Nothing matched return empty vector.
goto fini;
}
if (rc < 0) {
// Some error happend. Inform the user.
PCRE2_UCHAR buffer[256];
pcre2_get_error_message(rc, buffer, sizeof(buffer));
RZ_LOG_WARN("Regex matching failed: %s\n", buffer);
goto fini;
}
// Add groups to vector
PCRE2_SIZE *ovector = pcre2_get_ovector_pointer(mdata);
ut32 name_entry_size;
PCRE2_SPTR nametable_ptr;
pcre2_pattern_info(
regex,
PCRE2_INFO_NAMETABLE,
&nametable_ptr);
pcre2_pattern_info(
regex,
PCRE2_INFO_NAMEENTRYSIZE,
&name_entry_size);
for (size_t i = 0; i < rc; i++) {
if (ovector[2 * i] > ovector[2 * i + 1]) {
// This happens for \K lookaround. We fail if used.
// See pcre2demo.c for details.
RZ_LOG_ERROR("Usage of \\K to set start of the pattern later than the end, is not implemented.\n");
goto fini;
}
// Offset and length of match
RzRegexMatch *match = RZ_NEW0(RzRegexMatch);
match->start = ovector[2 * i];
match->len = ovector[2 * i + 1] - match->start;
match->group_idx = i;
nametable_ptr += name_entry_size;
rz_pvector_push(matches, match);
}
fini:
rz_regex_match_data_free(mdata);
return matches;
}
/**
* \brief Finds all matches in a text and returns them as vector.
* The result is a flat vector of matches. A single match with multiple
* groups is simply appeneded to the resulting vector.
*
* \param regex The regex pattern to match.
* \param text The text to search in.
* \param text_size The length of the buffer pointed to by \p text.
* Can be set to RZ_REGEX_ZERO_TERMINATED if the buffer is a zero terminated string.
* \param text_offset The offset into \p text from where the search starts.
* \param mflags Match flags.
*
* \return A vector of all matches or NULL in case of failure.
* Sub-groups of a match are appended after their main match.
*/
RZ_API RZ_OWN RzPVector /*<RzRegexMatch *>*/ *rz_regex_match_all_not_grouped(
RZ_NONNULL const RzRegex *regex,
RZ_NONNULL const char *text,
RzRegexSize text_size,
RzRegexSize text_offset,
RzRegexFlags mflags) {
rz_return_val_if_fail(regex && text, NULL);
RzPVector *all_matches = rz_pvector_new(free);
RzPVector *matches = rz_regex_match_first(regex, text, text_size, text_offset, mflags);
while (matches && rz_pvector_len(matches) > 0) {
RzRegexMatch *whole_match = rz_pvector_head(matches);
text_offset = whole_match->start + whole_match->len;
size_t mlen = rz_pvector_len(matches);
for (size_t i = 0; i < mlen; ++i) {
RzRegexMatch *m = rz_pvector_pop_front(matches);
rz_pvector_push(all_matches, m);
}
rz_pvector_free(matches);
// Search again after the whole first match.
matches = rz_regex_match_first(regex, text, text_size, text_offset, mflags);
}
// Free last vector without matches.
rz_pvector_free(matches);
return all_matches;
}
static RZ_OWN RzPVector /*<RzVector<RzRegexMatch *> *>*/ *rz_regex_match_all_internal(
RZ_NONNULL const RzRegex *regex,
RZ_NONNULL const char *text,
RzRegexSize text_size,
RzRegexSize text_offset,
RzRegexFlags mflags,
bool allow_overlap) {
rz_return_val_if_fail(regex && text, NULL);
RzPVector *all_matches = rz_pvector_new((RzPVectorFree)rz_pvector_free);
RzPVector *matches = rz_regex_match_first(regex, text, text_size, text_offset, mflags);
while (matches && rz_pvector_len(matches) > 0) {
rz_pvector_push(all_matches, matches);
RzRegexMatch *m = rz_pvector_head(matches);
// Search again after the last match.
text_offset = allow_overlap ? m->start + 1 : m->start + m->len;
matches = rz_regex_match_first(regex, text, text_size, text_offset, mflags);
}
// Free last vector without matches.
rz_pvector_free(matches);
return all_matches;
}
/**
* \brief Finds all matches in a text and returns them as vector of vector matches.
*
* \param pattern The regex pattern to match.
* \param text The text to search in.
* \param text_size The length of the buffer pointed to by \p text.
* Can be set to RZ_REGEX_ZERO_TERMINATED if the buffer is a zero terminated string.
* \param text_offset The offset into \p text from where the search starts.
* \param mflags Match flags.
* \param allow_overlap If true it will match also overlaping patterns.
*
* \return PVector of every match in the given string or NULL in case of failure.
* One match with all its groups is again assembled in a pvector.
*/
RZ_API RZ_OWN RzPVector /*<RzVector<RzRegexMatch *> *>*/ *rz_regex_match_all_overlap(
RZ_NONNULL const RzRegex *regex,
RZ_NONNULL const char *text,
RzRegexSize text_size,
RzRegexSize text_offset,
RzRegexFlags mflags) {
return rz_regex_match_all_internal(regex, text, text_size, text_offset, mflags, true);
}
/**
* \brief Finds all matches in a text and returns them as vector of vector matches.
*
* \param pattern The regex pattern to match.
* \param text The text to search in.
* \param text_size The length of the buffer pointed to by \p text.
* Can be set to RZ_REGEX_ZERO_TERMINATED if the buffer is a zero terminated string.
* \param text_offset The offset into \p text from where the search starts.
* \param mflags Match flags.
*
* \return PVector of every match in the given string or NULL in case of failure.
* One match with all its groups is again assembled in a pvector.
*/
RZ_API RZ_OWN RzPVector /*<RzVector<RzRegexMatch *> *>*/ *rz_regex_match_all(
RZ_NONNULL const RzRegex *regex,
RZ_NONNULL const char *text,
RzRegexSize text_size,
RzRegexSize text_offset,
RzRegexFlags mflags) {
return rz_regex_match_all_internal(regex, text, text_size, text_offset, mflags, false);
}
/**
* \brief Checks if \p pattern can be found in \p text.
*
* \param pattern The regex pattern to match.
* \param text The text to search in.
* \param text_size The length of the buffer pointed to by \p text.
* Can be set to RZ_REGEX_ZERO_TERMINATED if the buffer is a zero terminated string.
* \param text_offset The offset into \p text from where the search starts.
* \param cflags Compile flags.
* \param mflags Match flags.
*
* \return true if the text contains the patterns.
* \return false Otherwise
*/
RZ_API bool rz_regex_contains(RZ_NONNULL const char *pattern, RZ_NONNULL const char *text,
RzRegexSize text_size,
RzRegexFlags cflags, RzRegexFlags mflags) {
RzRegex *re = rz_regex_new(pattern, cflags, 0, NULL);
if (!re) {
return false;
}
RzPVector *matches = rz_regex_match_first(re, text, text_size, 0, mflags);
bool found = matches != NULL && !rz_pvector_empty(matches);
rz_pvector_free(matches);
rz_regex_free(re);
return found;
}
/**
* \brief Searches for a \p pattern in \p text and returns all matches as concatenated string.
* Only complete matches are concatenated. Sub-groups are skipped.
*
* \param pattern The regex pattern to match.
* \param text The text to search in.
* \param text_size The length of the buffer pointed to by \p text.
* Can be set to RZ_REGEX_ZERO_TERMINATED if the buffer is a zero terminated string.
* \param cflags Compile flags.
* \param mflags Match flags.
* \param separator A string to separate the matches.
*
* \return A string with all matches concatenated or NULL in case of failure.
*/
RZ_API RZ_OWN RzStrBuf *rz_regex_full_match_str(RZ_NONNULL const char *pattern, RZ_NONNULL const char *text,
RzRegexSize text_size,
RzRegexFlags cflags, RzRegexFlags mflags, RZ_NONNULL const char *separator) {
rz_return_val_if_fail(pattern && text && separator, NULL);
RzRegex *re = rz_regex_new(pattern, cflags, 0, NULL);
RzStrBuf *sbuf = rz_strbuf_new("");
RzPVector *matches = rz_regex_match_all(re, text, text_size, 0, mflags);
if (!matches || !sbuf) {
goto fini;
}
size_t i = 1;
void **m;
rz_pvector_foreach (matches, m) {
RzPVector *match_groups = *m;
RzRegexMatch *match = rz_pvector_head(match_groups);
const char *t = text + match->start;
if (((int)match->len) < 0) {
goto fini;
}
// No separator in case of only one match
if (i == rz_pvector_len(matches)) {
rz_strbuf_appendf(sbuf, "%-.*s", (int)match->len, t);
} else if (!rz_strbuf_appendf(sbuf, "%-.*s%s", (int)match->len, t, separator)) {
goto fini;
}
++i;
}
fini:
rz_regex_free(re);
rz_pvector_free(matches);
return sbuf;
}
/**
* \brief Searches for the given \p pattern in \p text and
* returns an byte offset into the string where the pattern was found.
*
* \param pattern The regex pattern to match.
* \param text The text to search in.
* \param text_size The length of the buffer pointed to by \p text.
* Can be set to RZ_REGEX_ZERO_TERMINATED if the buffer is a zero terminated string.
* \param text_offset The offset into \p text from where the search starts.
* \param cflags Compile flags.
* \param mflags Match flags.
*
* \return Offset into the string, or SZT_MAX if pattern was not found.
*/
RZ_API RzRegexSize rz_regex_find(RZ_NONNULL const char *pattern, RZ_NONNULL RZ_BORROW char *text,
RzRegexSize text_size, RzRegexSize text_offset,
RzRegexFlags cflags, RzRegexFlags mflags) {
rz_return_val_if_fail(pattern && text, SZT_MAX);
RzRegex *regex = rz_regex_new(pattern, cflags, RZ_REGEX_DEFAULT, NULL);
RzPVector *matches = rz_regex_match_first(regex, text, text_size, text_offset, mflags);
if (rz_pvector_empty(matches)) {
rz_pvector_free(matches);
rz_regex_free(regex);
return SZT_MAX;
}
RzRegexSize off = ((RzRegexMatch *)rz_pvector_head(matches))->start;
rz_pvector_free(matches);
rz_regex_free(regex);
return off;
}
/**
* \brief Returns a compile context.
*
* \return A PCRE2 compile context, or NULL in case of failure.
*/
RZ_API RZ_OWN RzRegexCompContext *rz_regex_compile_context_new() {
return pcre2_compile_context_create(NULL);
}
/**
* \brief Frees a compile context.
*
* \param A PCRE2 compile context.
*/
RZ_API void rz_regex_compile_context_free(RzRegexCompContext *ccontext) {
pcre2_compile_context_free(ccontext);
}
/**
* \brief Sets the newline convention of a compile context to the NUL character (\0).
*
* \param A PCRE2 compile context.
*/
RZ_API void rz_regex_set_nul_as_newline(RZ_NONNULL RzRegexCompContext *ccontext) {
pcre2_set_newline(ccontext, PCRE2_NEWLINE_NUL);
}
/**
* \brief Parses a string with regex flags characters and returns the numerical
* value of it.
*
* - ''/NULL/'r' - Default: RZ_REGEX_DEFAULT
* - 'l' - RZ_REGEX_LITERAL
* - 'i' - RZ_REGEX_CASELESS
* - 'e' - RZ_REGEX_EXTENDED
* - 'E' - RZ_REGEX_EXTENDED_MORE
* - 'm' - RZ_REGEX_MULTILINE
*
* \param re_flags_desc The string containing several characters.
*
* \return The flags as numerical value. Or ~RZ_REGEX_DEFAULT if flags are invalid.
*
* Examples:
*
* - "ri" => RZ_REGEX_DEFAULT | RZ_REGEX_CASELESS
* - "li" => RZ_REGEX_LITERAL | RZ_REGEX_CASELESS
* - "Eim" => RZ_REGEX_EXTENDED | RZ_REGEX_CASELESS | RZ_REGEX_MULTILINE
* - "rl" => ~RZ_REGEX_DEFAULT => Error: Flag combination is invalid.
* - "xX" => ~RZ_REGEX_DEFAULT => Error: Flag combination is invalid.
*/
RZ_API RzRegexFlags rz_regex_parse_flag_desc(RZ_NULLABLE const char *re_flags_desc) {
RzRegexFlags flags = RZ_REGEX_DEFAULT;
if (RZ_STR_ISEMPTY(re_flags_desc)) {
return flags;
}
size_t fcount = 0;
if (strchr(re_flags_desc, 'i')) {
fcount++;
flags |= RZ_REGEX_CASELESS;
}
if (strchr(re_flags_desc, 'l')) {
fcount++;
flags |= RZ_REGEX_LITERAL;
goto return_flags;
}
if (strchr(re_flags_desc, 'r')) {
fcount++;
goto return_flags;
}
if (strchr(re_flags_desc, 'e')) {
fcount++;
flags |= RZ_REGEX_EXTENDED;
}
if (strchr(re_flags_desc, 'E')) {
fcount++;
flags |= RZ_REGEX_EXTENDED_MORE;
}
if (strchr(re_flags_desc, 'm')) {
fcount++;
flags |= RZ_REGEX_MULTILINE;
}
return_flags:
if (fcount != strlen(re_flags_desc)) {
RZ_LOG_ERROR("Flag combination '%s' is invalid.\n", re_flags_desc);
return ~RZ_REGEX_DEFAULT;
}
return flags;
}