]> git.ipfire.org Git - thirdparty/dovecot/core.git/commitdiff
lib: Add wildcard_match_escaped*()
authorTimo Sirainen <timo.sirainen@open-xchange.com>
Tue, 2 May 2023 11:47:23 +0000 (14:47 +0300)
committeraki.tuomi <aki.tuomi@open-xchange.com>
Sun, 21 May 2023 17:41:38 +0000 (17:41 +0000)
This allows using \* and \? to match wildcard characters as-is instead of
being wildcards.

src/lib/test-wildcard-match.c
src/lib/wildcard-match.c
src/lib/wildcard-match.h

index 7a459da7745c5a270837517ac443d0c798f08492..40b10b0014b34900aef82eab613319528b6d894c 100644 (file)
@@ -7,7 +7,7 @@ static const struct {
        const char *data;
        const char *mask;
        bool result;
-} tests[] = {
+} tests_common[] = {
        { "foo", "*", TRUE },
        { "foo", "*foo*", TRUE },
        { "foo", "foo", TRUE },
@@ -36,13 +36,74 @@ static const struct {
        { "", "?", FALSE }
 };
 
+static const struct {
+       const char *data;
+       const char *mask;
+       bool result;
+} tests_unescaped[] = {
+       { "f*o", "f*o", TRUE },
+       { "f*o", "f\\*o", FALSE },
+};
+
+static const struct {
+       const char *data;
+       const char *mask;
+       bool result;
+} tests_escaped[] = {
+       { "f*o", "f*o", TRUE },
+       { "f*o", "f\\*o", TRUE },
+       { "f?o", "f\\*o", FALSE },
+       { "f?o", "f\\?o", TRUE },
+       { "f*o", "f\\*", FALSE },
+       { "f*", "f\\", FALSE },
+       { "f\\*o", "f\\*o", FALSE },
+       { "f\\*o", "f\\\\*", TRUE },
+       { "f\\*o", "f\\\\*o", TRUE },
+       { "f\\*o", "f\\\\\\*o", TRUE },
+       { "f\\*o", "f\\?o", FALSE },
+       { "f\\*o", "f\\\\?o", TRUE },
+       { "f\\*o", "f\\\\\\?o", FALSE },
+       { "f\\", "f\\", TRUE },
+       { "\\\\?", "?\\\\?", TRUE },
+};
+
 void test_wildcard_match(void)
 {
        unsigned int i;
 
        test_begin("wildcard_match()");
-       for (i = 0; i < N_ELEMENTS(tests); i++) {
-               test_assert_idx(wildcard_match(tests[i].data, tests[i].mask) == tests[i].result, i);
+       for (i = 0; i < N_ELEMENTS(tests_common); i++) {
+               test_assert_idx(wildcard_match(tests_common[i].data,
+                                              tests_common[i].mask) ==
+                               tests_common[i].result, i);
+               test_assert_idx(wildcard_match_escaped(tests_common[i].data,
+                                                      tests_common[i].mask) ==
+                               tests_common[i].result, i);
+       }
+
+       for (i = 0; i < N_ELEMENTS(tests_unescaped); i++) {
+               test_assert_idx(wildcard_match(tests_unescaped[i].data,
+                                              tests_unescaped[i].mask) ==
+                               tests_unescaped[i].result, i);
        }
+
+       for (i = 0; i < N_ELEMENTS(tests_escaped); i++) {
+               test_assert_idx(wildcard_match_escaped(tests_escaped[i].data,
+                                                      tests_escaped[i].mask) ==
+                               tests_escaped[i].result, i);
+       }
+       test_assert(!wildcard_is_literal("\\*foo\\?bar\\?"));
+       test_assert(wildcard_is_escaped_literal("\\*foo\\?bar\\?"));
+       test_assert(!wildcard_is_escaped_literal("*foo"));
+       test_end();
+
+       test_begin("wildcard_str_escape()");
+       const char *foo = "foo";
+       test_assert(wildcard_str_escape(foo) == foo);
+       test_assert_strcmp(wildcard_str_escape("foo?"), "foo\\?");
+       test_assert_strcmp(wildcard_str_escape("foo*"), "foo\\*");
+       test_assert_strcmp(wildcard_str_escape("foo\\"), "foo\\\\");
+       test_assert_strcmp(wildcard_str_escape("foo\\"), "foo\\\\");
+       test_assert_strcmp(wildcard_str_escape("\\f*o?o'\"x"), "\\\\f\\*o\\?o\\'\\\"x");
        test_end();
 }
index c1e2b2e968b823659a8daab4741f36d4574ee5fd..a635d1febc8959296ac31ccfb0bb1d4046cf4796 100644 (file)
  */
 
 #include "lib.h"
+#include "str.h"
 #include "wildcard-match.h"
 
 #include <ctype.h>
 
 #define WILDS '*'  /* matches 0 or more characters (including spaces) */
 #define WILDQ '?'  /* matches exactly one character */
+#define WILDE '\\' /* escapes one wildcard */
 
 #define NOMATCH 0
 #define MATCH (match+sofar)
 
-static int wildcard_match_int(const char *data, const char *mask, bool icase)
+static bool is_escaped(const char *p, const char *start)
+{
+  bool is_escaped = FALSE;
+  while (p > start && p[-1] == WILDE) {
+    is_escaped = !is_escaped;
+    p--;
+  }
+  return is_escaped;
+}
+
+static int
+wildcard_match_int(const char *data, const char *mask, bool icase, bool escaped)
 {
   const char *ma = mask, *na = data, *lsm = NULL, *lsn = NULL;
   int match = 1;
@@ -56,18 +69,45 @@ static int wildcard_match_int(const char *data, const char *mask, bool icase)
     }
 
     switch (*mask) {
+    case WILDE:
+      if (escaped && is_escaped(mask, ma)) {
+       if (*mask != *data)
+         goto nomatch;
+       mask -= 2;
+       data--;
+       sofar++;
+       continue;
+      }
+      break;
     case WILDS:                /* Matches anything */
+      if (escaped && is_escaped(mask, ma)) {
+       if (*mask != *data)
+         goto nomatch;
+       mask -= 2;
+       data--;
+       sofar++;
+       continue;
+      }
       do
-        mask--;                    /* Zap redundant wilds */
-      while ((mask >= ma) && (*mask == WILDS));
+       mask--;                    /* Zap redundant wilds */
+      while ((mask >= ma) && (*mask == WILDS) &&
+            (!escaped || !is_escaped(mask, ma)));
       lsm = mask;
       lsn = data;
       match += sofar;
       sofar = 0;                /* Update fallback pos */
       if (mask < ma)
-        return MATCH;
+       return MATCH;
       continue;                 /* Next char, please */
     case WILDQ:
+      if (escaped && is_escaped(mask, ma)) {
+       if (*mask != *data)
+         goto nomatch;
+       mask -= 2;
+       data--;
+       sofar++;
+       continue;
+      }
       mask--;
       data--;
       continue;                 /* '?' always matches */
@@ -79,6 +119,7 @@ static int wildcard_match_int(const char *data, const char *mask, bool icase)
       sofar++;                  /* Tally the match */
       continue;                 /* Next char, please */
     }
+nomatch:
     if (lsm != NULL) {          /* To to fallback on '*' */
       data = --lsn;
       mask = lsm;
@@ -89,17 +130,61 @@ static int wildcard_match_int(const char *data, const char *mask, bool icase)
     }
     return NOMATCH;             /* No fallback=No match */
   }
-  while ((mask >= ma) && (*mask == WILDS))
+  while ((mask >= ma) && (*mask == WILDS) &&
+        (!escaped || !is_escaped(mask, ma)))
     mask--;                        /* Zap leftover %s & *s */
   return (mask >= ma) ? NOMATCH : MATCH;   /* Start of both = match */
 }
 
 bool wildcard_match(const char *data, const char *mask)
 {
-       return wildcard_match_int(data, mask, FALSE) != 0;
+       return wildcard_match_int(data, mask, FALSE, FALSE) != 0;
 }
 
 bool wildcard_match_icase(const char *data, const char *mask)
 {
-       return wildcard_match_int(data, mask, TRUE) != 0;
+       return wildcard_match_int(data, mask, TRUE, FALSE) != 0;
+}
+
+bool wildcard_match_escaped(const char *data, const char *mask)
+{
+       return wildcard_match_int(data, mask, FALSE, TRUE) != 0;
+}
+
+bool wildcard_match_escaped_icase(const char *data, const char *mask)
+{
+       return wildcard_match_int(data, mask, TRUE, TRUE) != 0;
+}
+
+bool wildcard_is_escaped_literal(const char *mask)
+{
+       const char *p = mask;
+
+       while ((p = strpbrk(p, "*?\\")) != NULL) {
+               if (*p != '\\')
+                       return FALSE;
+               if (p[1] == '\0')
+                       break;
+               p += 2;
+       }
+       return TRUE;
+}
+
+const char *wildcard_str_escape(const char *str)
+{
+       const char *p = strpbrk(str, "*?\\\"'");
+       if (p == NULL)
+               return str;
+
+       string_t *esc = t_str_new((p - str) + strlen(p) + 8);
+       do {
+               str_append_data(esc, str, p - str);
+               str_append_c(esc, '\\');
+               str_append_c(esc, *p);
+
+               str = p + 1;
+               p = strpbrk(str, "*?\\\"'");
+       } while (p != NULL);
+       str_append(esc, str);
+       return str_c(esc);
 }
index bfe6076f1dd0d77dd0e7d1e98fc548d8e1694670..fa33d705c5354b6b1cb06028a433d1132b1fdb68 100644 (file)
@@ -12,4 +12,13 @@ static inline bool wildcard_is_literal(const char *mask)
        return strpbrk(mask, "*?") == NULL;
 }
 
+bool wildcard_match_escaped(const char *data, const char *mask);
+bool wildcard_match_escaped_icase(const char *data, const char *mask);
+/* Returns TRUE if mask does *not* contain any '*' or '?' wildcards, except
+   preceded by '\' escape character. */
+bool wildcard_is_escaped_literal(const char *mask);
+
+/* Same as str_escape(), but also escape '*' and '?' characters. */
+const char *wildcard_str_escape(const char *str);
+
 #endif