From: Timo Sirainen Date: Tue, 2 May 2023 11:47:23 +0000 (+0300) Subject: lib: Add wildcard_match_escaped*() X-Git-Tag: 2.4.0~2750 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=17f34b174950faeed45106400115d5ea7b031937;p=thirdparty%2Fdovecot%2Fcore.git lib: Add wildcard_match_escaped*() This allows using \* and \? to match wildcard characters as-is instead of being wildcards. --- diff --git a/src/lib/test-wildcard-match.c b/src/lib/test-wildcard-match.c index 7a459da774..40b10b0014 100644 --- a/src/lib/test-wildcard-match.c +++ b/src/lib/test-wildcard-match.c @@ -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(); } diff --git a/src/lib/wildcard-match.c b/src/lib/wildcard-match.c index c1e2b2e968..a635d1febc 100644 --- a/src/lib/wildcard-match.c +++ b/src/lib/wildcard-match.c @@ -13,17 +13,30 @@ */ #include "lib.h" +#include "str.h" #include "wildcard-match.h" #include #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); } diff --git a/src/lib/wildcard-match.h b/src/lib/wildcard-match.h index bfe6076f1d..fa33d705c5 100644 --- a/src/lib/wildcard-match.h +++ b/src/lib/wildcard-match.h @@ -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