]> granicus.if.org Git - neomutt/commitdiff
Refactor regex with custom wrapper 1780/head
authorSimon Symeonidis <lethaljellybean@gmail.com>
Fri, 19 Jul 2019 19:02:28 +0000 (15:02 -0400)
committerRichard Russon <rich@flatcap.org>
Tue, 30 Jul 2019 09:54:35 +0000 (10:54 +0100)
20 files changed:
browser.c
commands.c
email/parse.c
email/rfc2047.c
hook.c
imap/browse.c
index.c
mutt/charset.c
mutt/path.c
mutt/regex.c
mutt/regex3.h
muttlib.c
ncrypt/pgp.c
nntp/browse.c
notmuch/mutt_notmuch.c
pager.c
send.c
test/Makefile.autosetup
test/main.c
test/regex/mutt_regex_match.c [new file with mode: 0644]

index 4700c33d285ab8a108a3f368e2c0cf503913a156..db6602b5295b57e3c3c071ed96c061238eff110f 100644 (file)
--- a/browser.c
+++ b/browser.c
@@ -707,8 +707,7 @@ static int examine_directory(struct Menu *menu, struct BrowserState *state,
         continue;
       if (prefix && *prefix && !mutt_str_startswith(mdata->group, prefix, CASE_MATCH))
         continue;
-      if (C_Mask && C_Mask->regex &&
-          !((regexec(C_Mask->regex, mdata->group, 0, NULL, 0) == 0) ^ C_Mask->pat_not))
+      if (!mutt_regex_match(C_Mask, mdata->group))
       {
         continue;
       }
@@ -767,8 +766,7 @@ static int examine_directory(struct Menu *menu, struct BrowserState *state,
       {
         continue;
       }
-      if (C_Mask && C_Mask->regex &&
-          !((regexec(C_Mask->regex, de->d_name, 0, NULL, 0) == 0) ^ C_Mask->pat_not))
+      if (!mutt_regex_match(C_Mask, de->d_name))
       {
         continue;
       }
index 0fe02aa515ab3ca6ce9ff9b1af2f69373fe27d9e..c7160eb73c929ec37779a2e0c5909bd3cc3e7ef8 100644 (file)
@@ -150,7 +150,7 @@ static void update_protected_headers(struct Email *e)
 
     mutt_str_replace(&e->env->subject, prot_headers->subject);
     FREE(&e->env->disp_subj);
-    if (regexec(C_ReplyRegex->regex, e->env->subject, 1, pmatch, 0) == 0)
+    if (mutt_regex_capture(C_ReplyRegex, e->env->subject, 1, pmatch))
       e->env->real_subj = e->env->subject + pmatch[0].rm_eo;
     else
       e->env->real_subj = e->env->subject;
index 6607ede52b1f564643004567a0ff8cffc688da39..59af6ee15afe93cbcf5c51898e8f17a7ed029571 100644 (file)
@@ -1135,8 +1135,7 @@ struct Envelope *mutt_rfc822_read_header(FILE *fp, struct Email *e, bool user_hd
     {
       regmatch_t pmatch[1];
 
-      if (C_ReplyRegex && C_ReplyRegex->regex &&
-          (regexec(C_ReplyRegex->regex, env->subject, 1, pmatch, 0) == 0))
+      if (mutt_regex_capture(C_ReplyRegex, env->subject, 1, pmatch))
       {
         env->real_subj = env->subject + pmatch[0].rm_eo;
       }
index e337dde484572105a8f71fd7193877ae5f17847b..99ce89875af570710fa3686a198e590e6bf71491 100644 (file)
@@ -153,8 +153,8 @@ static char *parse_encoded_word(char *str, enum ContentEncoding *enc, char **cha
   assert(re && "Something is wrong with your RE engine.");
 
   char *res = NULL;
-  int rc = regexec(re->regex, str, nmatch, match, 0);
-  if (rc == 0)
+
+  if (mutt_regex_capture(re, str, nmatch, match))
   {
     /* Charset */
     *charset = str + match[1].rm_so;
diff --git a/hook.c b/hook.c
index 0d3578a4422050a0a2dde973249b5c63dbf8327d..6f70bebe3163c76d3cc44fc91a07e0fe46492c09 100644 (file)
--- a/hook.c
+++ b/hook.c
@@ -530,8 +530,7 @@ void mutt_folder_hook(const char *path, const char *desc)
     if (!(tmp->type & MUTT_FOLDER_HOOK))
       continue;
 
-    if ((path && (regexec(tmp->regex.regex, path, 0, NULL, 0) == 0) ^ tmp->regex.pat_not) ||
-        (desc && (regexec(tmp->regex.regex, desc, 0, NULL, 0) == 0) ^ tmp->regex.pat_not))
+    if (mutt_regex_match(&tmp->regex, path) || mutt_regex_match(&tmp->regex, desc))
     {
       if (mutt_parse_rc_line(tmp->command, token, err) == MUTT_CMD_ERROR)
       {
@@ -562,7 +561,7 @@ char *mutt_find_hook(HookFlags type, const char *pat)
   {
     if (tmp->type & type)
     {
-      if (regexec(tmp->regex.regex, pat, 0, NULL, 0) == 0)
+      if (mutt_regex_match(&tmp->regex, pat))
         return tmp->command;
     }
   }
@@ -730,9 +729,7 @@ static void list_hook(struct ListHead *matches, const char *match, HookFlags hoo
 
   TAILQ_FOREACH(tmp, &Hooks, entries)
   {
-    if ((tmp->type & hook) &&
-        ((match && (regexec(tmp->regex.regex, match, 0, NULL, 0) == 0)) ^
-         tmp->regex.pat_not))
+    if ((tmp->type & hook) && mutt_regex_match(&tmp->regex, match))
     {
       mutt_list_insert_tail(matches, mutt_str_strdup(tmp->command));
     }
@@ -775,7 +772,7 @@ void mutt_account_hook(const char *url)
     if (!(hook->command && (hook->type & MUTT_ACCOUNT_HOOK)))
       continue;
 
-    if ((regexec(hook->regex.regex, url, 0, NULL, 0) == 0) ^ hook->regex.pat_not)
+    if (mutt_regex_match(&hook->regex, url))
     {
       inhook = true;
 
index edf0efa16459eda17215a22dd227033176b416f2..bc18a6c9471193fb5a7e40dc18a56c617f8165f8 100644 (file)
@@ -88,8 +88,7 @@ static void add_folder(char delim, char *folder, bool noselect, bool noinferiors
   /* apply filemask filter. This should really be done at menu setup rather
    * than at scan, since it's so expensive to scan. But that's big changes
    * to browser.c */
-  if (C_Mask && C_Mask->regex &&
-      !((regexec(C_Mask->regex, relpath, 0, NULL, 0) == 0) ^ C_Mask->pat_not))
+  if (!mutt_regex_match(C_Mask, relpath))
   {
     return;
   }
diff --git a/index.c b/index.c
index 702a0f42246ccde572c268f85a71e1ea3f1ab121..c3c1b9ccb99b4479154cd7cfc93a1835cecb168a 100644 (file)
--- a/index.c
+++ b/index.c
@@ -3683,8 +3683,7 @@ int mutt_reply_observer(struct NotifyCallback *nc)
     if (!e || !e->subject)
       continue;
 
-    if (C_ReplyRegex && C_ReplyRegex->regex &&
-        (regexec(C_ReplyRegex->regex, e->subject, 1, pmatch, 0) == 0))
+    if (mutt_regex_capture(C_ReplyRegex, e->subject, 1, pmatch))
     {
       e->real_subj = e->subject + pmatch[0].rm_eo;
       continue;
index 7b4ff618631c8fac4f3c73de060a8760874749f8..edf8596072a354a8dd5ab3c0f1277fed583a844e 100644 (file)
@@ -252,7 +252,7 @@ static const char *lookup_charset(enum LookupType type, const char *cs)
   {
     if (l->type != type)
       continue;
-    if (regexec(l->regex.regex, cs, 0, NULL, 0) == 0)
+    if (mutt_regex_match(&l->regex, cs))
       return l->replacement;
   }
   return NULL;
index 1f1a72902a5b397731d8cdc7e2798b498190d6af..9e31a0d4c516167a69ffeca3a795b64b54260f47 100644 (file)
@@ -263,7 +263,8 @@ bool mutt_path_canon(char *buf, size_t buflen, const char *homedir)
     size_t dirlen = mutt_str_strlen(dir);
     if ((len + dirlen) >= buflen)
     {
-      mutt_debug(LL_DEBUG3, "result too big for the buffer %ld >= %ld\n", len + dirlen, buflen);
+      mutt_debug(LL_DEBUG3, "result too big for the buffer %ld >= %ld\n",
+                 len + dirlen, buflen);
       return false;
     }
 
index e3c1365c838ccc48004aac80ce667a943fe59eb6..5179c29b51fd4afa81a8262a22bbaf2ffb2fba15 100644 (file)
@@ -4,6 +4,7 @@
  *
  * @authors
  * Copyright (C) 2017 Richard Russon <rich@flatcap.org>
+ * Copyright (C) 2019 Simon Symeonidis <lethaljellybean@gmail.com>
  *
  * @copyright
  * This program is free software: you can redistribute it and/or modify it under
@@ -194,9 +195,7 @@ bool mutt_regexlist_match(struct RegexList *rl, const char *str)
   struct RegexListNode *np = NULL;
   STAILQ_FOREACH(np, rl, entries)
   {
-    if (!np->regex || !np->regex->regex)
-      continue;
-    if (regexec(np->regex->regex, str, 0, NULL, 0) == 0)
+    if (mutt_regex_match(np->regex, str))
     {
       mutt_debug(LL_DEBUG5, "%s matches %s\n", str, np->regex->pattern);
       return true;
@@ -383,7 +382,7 @@ char *mutt_replacelist_apply(struct ReplaceList *rl, char *buf, size_t buflen, c
       nmatch = np->nmatch;
     }
 
-    if (regexec(np->regex->regex, src, np->nmatch, pmatch, 0) == 0)
+    if (mutt_regex_capture(np->regex, src, np->nmatch, pmatch))
     {
       tlen = 0;
       switcher ^= 1;
@@ -494,7 +493,7 @@ bool mutt_replacelist_match(struct ReplaceList *rl, char *buf, size_t buflen, co
     }
 
     /* Does this pattern match? */
-    if (regexec(np->regex->regex, str, (size_t) np->nmatch, pmatch, 0) == 0)
+    if (mutt_regex_capture(np->regex, str, (size_t) np->nmatch, pmatch))
     {
       mutt_debug(LL_DEBUG5, "%s matches %s\n", str, np->regex->pattern);
       mutt_debug(LL_DEBUG5, "%d subs\n", (int) np->regex->regex->re_nsub);
@@ -583,3 +582,33 @@ int mutt_replacelist_remove(struct ReplaceList *rl, const char *pat)
 
   return nremoved;
 }
+
+/**
+ * mutt_regex_capture - match a regex against a string, with provided options
+ * @param regex   Regex to execute
+ * @param str     String to apply regex on
+ * @param nmatch  Length of matches
+ * @param matches regmatch_t to hold match indices
+ * @param flags   Type flags, e.g. #DT_REGEX_MATCH_CASE
+ * @retval bool true if str match, false if str does not match
+ */
+bool mutt_regex_capture(const struct Regex *regex, const char *str,
+                        size_t nmatch, regmatch_t matches[])
+{
+  if (!regex || !str || !regex->regex)
+    return false;
+
+  int rc = regexec(regex->regex, str, nmatch, matches, 0);
+  return ((rc == 0) ^ regex->pat_not);
+}
+
+/**
+ * mutt_regex_match - Shorthand to mutt_regex_capture()
+ * @param regex Regex which is desired to match against
+ * @param str   String to search with given regex
+ * @retval bool true if str match, false if str does not match
+ */
+bool mutt_regex_match(const struct Regex *regex, const char *str)
+{
+  return mutt_regex_capture(regex, str, 0, NULL);
+}
index a45e26ab8e2008604220d558a107d44da6b28573..34afcb8584275045ba6a4aa66b7ae9132adae037 100644 (file)
@@ -100,4 +100,7 @@ bool                    mutt_replacelist_match(struct ReplaceList *rl, char *buf
 struct ReplaceListNode *mutt_replacelist_new(void);
 int                     mutt_replacelist_remove(struct ReplaceList *rl, const char *pat);
 
+bool mutt_regex_match  (const struct Regex *regex, const char *str);
+bool mutt_regex_capture(const struct Regex *regex, const char *str, size_t num, regmatch_t matches[]);
+
 #endif /* MUTT_LIB_REGEX_H */
index 90918e1102a151139d7cab7af01f7757bba6a094..390342aae65e488def4949a69e92e518e01b60ce 100644 (file)
--- a/muttlib.c
+++ b/muttlib.c
@@ -370,13 +370,10 @@ char *mutt_gecos_name(char *dest, size_t destlen, struct passwd *pw)
 
   memset(dest, 0, destlen);
 
-  if (C_GecosMask && C_GecosMask->regex)
+  if (mutt_regex_capture(C_GecosMask, pw->pw_gecos, 1, pat_match))
   {
-    if (regexec(C_GecosMask->regex, pw->pw_gecos, 1, pat_match, 0) == 0)
-    {
-      mutt_str_strfcpy(dest, pw->pw_gecos + pat_match[0].rm_so,
-                       MIN(pat_match[0].rm_eo - pat_match[0].rm_so + 1, destlen));
-    }
+    mutt_str_strfcpy(dest, pw->pw_gecos + pat_match[0].rm_so,
+                     MIN(pat_match[0].rm_eo - pat_match[0].rm_so + 1, destlen));
   }
   else if ((p = strchr(pw->pw_gecos, ',')))
     mutt_str_strfcpy(dest, pw->pw_gecos, MIN(destlen, p - pw->pw_gecos + 1));
index c163dc40fa8b61618350c20dfe03e39d2d98b967..be98a6c1ca06cdb61b22c868277bfc28394c75e3 100644 (file)
@@ -263,7 +263,7 @@ static int pgp_copy_checksig(FILE *fp_in, FILE *fp_out)
 
     while ((line = mutt_file_read_line(line, &linelen, fp_in, &lineno, 0)))
     {
-      if (regexec(C_PgpGoodSign->regex, line, 0, NULL, 0) == 0)
+      if (mutt_regex_match(C_PgpGoodSign, line))
       {
         mutt_debug(LL_DEBUG2, "\"%s\" matches regex\n", line);
         rc = 0;
@@ -310,7 +310,7 @@ static int pgp_check_pgp_decryption_okay_regex(FILE *fp_in)
 
     while ((line = mutt_file_read_line(line, &linelen, fp_in, &lineno, 0)))
     {
-      if (regexec(C_PgpDecryptionOkay->regex, line, 0, NULL, 0) == 0)
+      if (mutt_regex_match(C_PgpDecryptionOkay, line))
       {
         mutt_debug(LL_DEBUG2, "\"%s\" matches regex\n", line);
         rc = 0;
index e4979beb92c7eda4e7f4ec5274a634136fe41175..3358aabd75d30358b4fb544e146e3908932192f7 100644 (file)
@@ -106,7 +106,7 @@ const char *group_index_format_str(char *buf, size_t buflen, size_t col, int col
 
     case 'n':
       if (C_MarkOld && (folder->ff->nd->last_cached >= folder->ff->nd->first_message) &&
-               (folder->ff->nd->last_cached <= folder->ff->nd->last_message))
+          (folder->ff->nd->last_cached <= folder->ff->nd->last_message))
       {
         snprintf(fmt, sizeof(fmt), "%%%sd", prec);
         snprintf(buf, buflen, fmt, folder->ff->nd->last_message - folder->ff->nd->last_cached);
index aa814fba03ed10ec19eb34e22848702bc26016b4..071cc50fb0d42982408a63c873e688eec336243e 100644 (file)
@@ -2225,7 +2225,8 @@ static int nm_mbox_check(struct Mailbox *m, int *index_hint)
 
   if (m->mtime.tv_sec >= mtime)
   {
-    mutt_debug(LL_DEBUG2, "nm: check unnecessary (db=%lu mailbox=%lu)\n", mtime, m->mtime.tv_sec);
+    mutt_debug(LL_DEBUG2, "nm: check unnecessary (db=%lu mailbox=%lu)\n", mtime,
+               m->mtime.tv_sec);
     return 0;
   }
 
diff --git a/pager.c b/pager.c
index 63484a807791388955b643407c311b4aefa16cd3..47f51924b1c7ceba889ee1292fda3061579205c6 100644 (file)
--- a/pager.c
+++ b/pager.c
@@ -951,18 +951,16 @@ int mutt_is_quote_line(char *line, regmatch_t *pmatch)
   if (!pmatch)
     pmatch = pmatch_internal;
 
-  if (C_QuoteRegex && C_QuoteRegex->regex &&
-      (regexec(C_QuoteRegex->regex, line, 1, pmatch, 0) == 0))
+  if (mutt_regex_capture(C_QuoteRegex, line, 1, pmatch))
   {
-    if (C_Smileys && C_Smileys->regex &&
-        (regexec(C_Smileys->regex, line, 1, smatch, 0) == 0))
+    if (mutt_regex_capture(C_Smileys, line, 1, smatch))
     {
       if (smatch[0].rm_so > 0)
       {
         char c = line[smatch[0].rm_so];
         line[smatch[0].rm_so] = 0;
 
-        if (regexec(C_QuoteRegex->regex, line, 1, pmatch, 0) == 0)
+        if (mutt_regex_capture(C_QuoteRegex, line, 1, pmatch))
           is_quote = true;
 
         line[smatch[0].rm_so] = c;
@@ -1730,8 +1728,8 @@ static int display_line(FILE *fp, LOFF_T *last_pos, struct Line **line_info,
         (*last)--;
       goto out;
     }
-    if (C_QuoteRegex && C_QuoteRegex->regex &&
-        (regexec(C_QuoteRegex->regex, (char *) fmt, 1, pmatch, 0) == 0))
+
+    if (mutt_regex_capture(C_QuoteRegex, (char *) fmt, 1, pmatch))
     {
       (*line_info)[n].quote =
           classify_quote(quote_list, (char *) fmt + pmatch[0].rm_so,
diff --git a/send.c b/send.c
index 13c5c251f9fa154a21dbe640cdf0c808c2d98d5b..3c3daba9e045ebcd4e53337c0a5ba11916d97bb2 100644 (file)
--- a/send.c
+++ b/send.c
@@ -1542,7 +1542,7 @@ static bool search_attach_keyword(char *filename)
   {
     fgets(inputline, 1024, fp_att);
     if (!mutt_is_quote_line(inputline, NULL) &&
-        (regexec(C_AbortNoattachRegex->regex, inputline, 0, NULL, 0) == 0))
+        mutt_regex_match(C_AbortNoattachRegex, inputline))
     {
       found = true;
       break;
index efa5553b373f952788593a0cbcc0e281fd94c56d..fac39715cc525929c88e0b414600d3e842866b20 100644 (file)
@@ -323,6 +323,7 @@ PATTERN_OBJS        = test/pattern/comp.o \
 
 REGEX_OBJS     = test/regex/mutt_regex_compile.o \
                  test/regex/mutt_regex_free.o \
+                 test/regex/mutt_regex_match.o \
                  test/regex/mutt_regexlist_add.o \
                  test/regex/mutt_regexlist_free.o \
                  test/regex/mutt_regexlist_match.o \
index 2f36947ec27c0c873e05697bdac5525716390403..2bac7ba4db0453f68eceb80bca23e581fae18c89 100644 (file)
   NEOMUTT_TEST_ITEM(test_mutt_pattern_comp)                                    \
   NEOMUTT_TEST_ITEM(test_mutt_regex_compile)                                   \
   NEOMUTT_TEST_ITEM(test_mutt_regex_free)                                      \
+  NEOMUTT_TEST_ITEM(test_mutt_regex_match)                                     \
   NEOMUTT_TEST_ITEM(test_mutt_regexlist_add)                                   \
   NEOMUTT_TEST_ITEM(test_mutt_regexlist_free)                                  \
   NEOMUTT_TEST_ITEM(test_mutt_regexlist_match)                                 \
diff --git a/test/regex/mutt_regex_match.c b/test/regex/mutt_regex_match.c
new file mode 100644 (file)
index 0000000..d85130a
--- /dev/null
@@ -0,0 +1,210 @@
+#define TEST_NO_MAIN
+
+#include "acutest.h"
+#include "mutt/buffer.h"
+#include "mutt/regex3.h"
+#include "config/common.h"
+
+static bool test_simple_cases(void)
+{
+  log_line(__func__);
+
+  struct Buffer *buf = mutt_buffer_new();
+  { /* handle edge cases */
+    struct Regex *rx = regex_new("hello bob", 0, buf);
+
+    const bool failed =
+        !TEST_CHECK(mutt_regex_match(NULL, NULL) == false) ||
+        !TEST_CHECK(mutt_regex_match(NULL, "bob the string") == false) ||
+        !TEST_CHECK(mutt_regex_match(rx, NULL) == false);
+    regex_free(&rx);
+
+    if (failed)
+      return false;
+  }
+
+  { /* handle normal cases */
+    struct Regex *rx = regex_new("hell", 0, buf);
+
+    const bool failed =
+        !TEST_CHECK(mutt_regex_match(rx, "hello there")) ||
+        !TEST_CHECK(mutt_regex_match(rx, "hell is not a greeting")) ||
+        !TEST_CHECK(mutt_regex_match(rx, "a demonic elavator is a hellevator"));
+    regex_free(&rx);
+
+    if (failed)
+      return false;
+  }
+
+  { /* test more elaborate regex */
+    const char *input = "bob bob bob mary bob jonny bob jon jon joe bob";
+    struct Regex *rx = regex_new("bob", 0, buf);
+
+    const bool result = mutt_regex_capture(rx, input, 0, NULL);
+    const bool failed = !TEST_CHECK(result);
+    regex_free(&rx);
+
+    if (failed)
+      return false;
+  }
+
+  { /* test passing simple flags */
+    const char *input = "BOB";
+    struct Regex *rx = regex_new("bob", 0, buf);
+    const bool failed = !TEST_CHECK(mutt_regex_capture(rx, input, 0, 0));
+    regex_free(&rx);
+
+    if (failed)
+      return false;
+  }
+
+  mutt_buffer_free(&buf);
+  return true;
+}
+
+static bool test_old_implementation(void)
+{
+  log_line(__func__);
+
+  // These tests check that the wrapper has the same behavior as
+  // prior, similar implementations
+
+  const char *bob_line = "definitely bob haha";
+  const char *not_bob_line = "john dave marty nothing else here";
+
+  struct Buffer *buf = mutt_buffer_new();
+  {
+    // from: if (regexec(C_PgpGoodSign->regex, bob_line, 0, NULL, 0) == 0)
+    //   to: if (mutt_regex_match(C_PgpGoodSign, bob_line))
+
+    struct Regex *rx = regex_new("bob", 0, buf);
+    const bool old = regexec(rx->regex, bob_line, 0, NULL, 0) == 0;
+    const bool new = mutt_regex_match(rx, bob_line);
+    const bool failed = !TEST_CHECK(old == new);
+
+    regex_free(&rx);
+    if (failed)
+      return false;
+  }
+
+  {
+    // from: if (regexec(C_QuoteRegex->regex, bob_line, 1, pmatch, 0) == 0)
+    //   to: if (mutt_regex_capture(QuoteRegex, bob_line, 1, pmatch))
+
+    const int nmatch = 1;
+    regmatch_t pmatch_1[nmatch], pmatch_2[nmatch];
+    struct Regex *rx = regex_new("bob", 0, buf);
+    const bool old = regexec(rx->regex, bob_line, nmatch, pmatch_1, 0) == 0;
+    const bool new = mutt_regex_capture(rx, bob_line, 1, pmatch_2);
+    const bool failed_common_behavior = !TEST_CHECK(old == new);
+
+    regex_free(&rx);
+    if (failed_common_behavior)
+      return false;
+  }
+
+  {
+    // from: if (C_QuoteRegex && C_QuoteRegex->regex &&
+    //          (regexec(C_QuoteRegex->regex, bob_line, 1, pmatch, 0) == 0))
+    //   to: if (mutt_regex_capture(QuoteRegex, bob_line, 1, pmatch))
+
+    const int nmatch = 1;
+    regmatch_t pmatch_1[nmatch], pmatch_2[nmatch];
+
+    struct Regex *rx = regex_new("bob", 0, buf);
+    const bool old = rx && rx->regex &&
+                     (regexec(rx->regex, bob_line, nmatch, pmatch_1, 0) == 0);
+    const bool new = mutt_regex_capture(rx, bob_line, 1, pmatch_2);
+    regex_free(&rx);
+
+    const bool failed_common_behavior = !TEST_CHECK(old == new);
+    if (failed_common_behavior)
+      return false;
+
+    const bool failed_pmatch = !TEST_CHECK(pmatch_1->rm_so == pmatch_2->rm_so &&
+                                           pmatch_2->rm_eo == pmatch_2->rm_eo);
+    if (failed_pmatch)
+      return false;
+  }
+
+  {
+    // from: if ((tmp->type & hook) &&
+    //         ((match && (regexec(tmp->regex.regex, match, 0, NULL, 0) == 0)) ^
+    //          tmp->regex.pat_not))
+    //   to: if ((tmp->type & hook) && mutt_regex_match(&tmp->regex, match))
+
+    struct Regex *rx = regex_new("!bob", DT_REGEX_ALLOW_NOT, buf);
+    const bool old =
+        (regexec(rx->regex, not_bob_line, 0, NULL, DT_REGEX_ALLOW_NOT) == 0) ^ rx->pat_not;
+    const bool new = mutt_regex_match(rx, not_bob_line);
+    regex_free(&rx);
+
+    const bool failed_common_behavior = !TEST_CHECK(old == new);
+    if (failed_common_behavior)
+      return false;
+
+    // make sure that bob is *NOT* found
+    const bool bob_found = !TEST_CHECK(new == true);
+    if (bob_found)
+      return false;
+  }
+
+  {
+    // from: if (C_Mask && C_Mask->regex &&
+    //       !((regexec(C_Mask->regex, mdata->group, 0, NULL, 0) == 0)
+    //       ^ C_Mask->pat_not))
+    // to: if(mutt_regex_match(C_Mask, de->d_name))
+
+    struct Regex *rx = regex_new("!bob", DT_REGEX_ALLOW_NOT, buf);
+    const bool old = rx && rx->regex &&
+                     !((regexec(rx->regex, not_bob_line, 0, NULL, 0) == 0) ^ rx->pat_not);
+    const bool new = mutt_regex_match(rx, bob_line);
+    regex_free(&rx);
+
+    const bool failed_common_behavior = !TEST_CHECK(old == new);
+    if (failed_common_behavior)
+      return false;
+  }
+
+  {
+    // from: if (C_Mask && C_Mask->regex &&
+    //          !((regexec(C_Mask->regex, mdata->group, 0, NULL, 0) == 0) ^ C_Mask->pat_not))
+    //   to: if (!mutt_regex_match(C_Mask, mdata->group))
+    struct Regex *rx = regex_new("!bob", DT_REGEX_ALLOW_NOT, buf);
+    const bool old =
+      (rx && rx->regex) && !((regexec(rx->regex, line, 0, NULL, 0) == 0) ^ rx->pat_not);
+    const bool new = !mutt_regex_match(rx, line);
+    regex_free(&rx);
+
+    const bool failed_common_behavior = !TEST_CHECK(old == new);
+    if (failed_common_behavior)
+      return false;
+  }
+
+  {
+    // if ((regexec(hook->regex.regex, url, 0, NULL, 0) == 0) ^ hook->regex.pat_not)
+    // if (mutt_regex_match(&hook->regex, url))
+
+    struct Regex *rx = regex_new("bob", 0, buf);
+    const bool old = (regexec(rx->regex, bob_line, 0, NULL, 0) == 0) ^ rx->pat_not;
+    const bool new = mutt_regex_match(rx, bob_line);
+    regex_free(&rx);
+
+    const bool failed_common_behavior = !TEST_CHECK(old == new);
+    if (failed_common_behavior)
+      return false;
+  }
+
+  mutt_buffer_free(&buf);
+  return true;
+}
+
+void test_mutt_regex_match(void)
+{
+  bool (*tests[])(void) = { test_simple_cases, test_old_implementation };
+
+  const size_t num_tests = sizeof(tests) / sizeof(tests[0]);
+
+  for (size_t i = 0; i < num_tests; ++i)
+    TEST_CHECK(tests[i]());
+}