From 4fdfc8a923ce542975051805d8409ee924b96c99 Mon Sep 17 00:00:00 2001 From: Andrea Mazzoleni Date: Wed, 26 Aug 2026 15:09:30 +0200 Subject: [PATCH] Fix literal caret handling in Windows path normalization and wildcard matching --- cmdline/selftest.c | 115 +++++++++++++++++++++++++++++++++++++++++++++ cmdline/str.c | 13 +++-- cmdline/str.h | 2 +- cmdline/support.c | 23 ++------- 4 files changed, 130 insertions(+), 23 deletions(-) diff --git a/cmdline/selftest.c b/cmdline/selftest.c index 5ac9e44d..ead8484f 100644 --- a/cmdline/selftest.c +++ b/cmdline/selftest.c @@ -864,6 +864,25 @@ struct { { "Movies*", "Movies4/file.mkv", 0, 1 }, /* escaping of wildcard metacharacters */ +#ifdef _WIN32 + { "foo^*bar", "foo*bar", 0, 0 }, + { "foo^?bar", "foo?bar", 0, 0 }, + { "foo^[bar", "foo[bar", 0, 0 }, + { "foo^]bar", "foo]bar", 0, 0 }, + { "foo^^bar", "foo^bar", 0, 0 }, + { "foo^*bar", "fooXXXbar", 0, 1 }, + { "foo^?bar", "fooXbar", 0, 1 }, + { "foo^[bar", "fooXbar", 0, 1 }, + { "foo^]bar", "fooXbar", 0, 1 }, + { "foo*bar", "fooXXXbar", 0, 0 }, + { "foo?bar", "fooXbar", 0, 0 }, + { "foo[abc]bar", "fooabar", 0, 0 }, + { "foo^abar", "foo^abar", 0, 0 }, + { "foo^abar", "fooabar", 0, 1 }, + { "foo^", "foo^", 0, 0 }, + { "foo^", "foo", 0, 1 }, + { "foo^bar", "foo^bar", 0, 0 }, +#else { "foo\\*bar", "foo*bar", 0, 0 }, { "foo\\?bar", "foo?bar", 0, 0 }, { "foo\\[bar", "foo[bar", 0, 0 }, @@ -880,6 +899,8 @@ struct { { "foo\\abar", "fooabar", 0, 1 }, { "foo\\", "foo\\", 0, 0 }, { "foo\\", "foo", 0, 1 }, + { "foo^bar", "foo^bar", 0, 0 }, +#endif { 0 } }; @@ -896,6 +917,82 @@ static void test_wnmatch(void) } } +static void test_path(void) +{ + char dst[PATH_MAX]; + + /* + * Use strcmp instead of pathcmp because pathcmp on Windows treats + * '/' and '\' as equivalent, which would prevent detecting bugs in + * separator conversion by pathimport/pathexport. + */ +#ifdef _WIN32 + pathimport(dst, sizeof(dst), "foo\\bar"); + if (strcmp(dst, "foo/bar") != 0) { + /* LCOV_EXCL_START */ + log_fatal(EINTERNAL, "Failed pathimport Windows separator test\n"); + exit(EXIT_FAILURE); + /* LCOV_EXCL_STOP */ + } + + pathimport(dst, sizeof(dst), "foo^bar"); + if (strcmp(dst, "foo^bar") != 0) { + /* LCOV_EXCL_START */ + log_fatal(EINTERNAL, "Failed pathimport Windows caret test\n"); + exit(EXIT_FAILURE); + /* LCOV_EXCL_STOP */ + } + + pathexport(dst, sizeof(dst), "foo/bar"); + if (strcmp(dst, "foo\\bar") != 0) { + /* LCOV_EXCL_START */ + log_fatal(EINTERNAL, "Failed pathexport Windows separator test\n"); + exit(EXIT_FAILURE); + /* LCOV_EXCL_STOP */ + } + + pathexport(dst, sizeof(dst), "foo^bar"); + if (strcmp(dst, "foo^bar") != 0) { + /* LCOV_EXCL_START */ + log_fatal(EINTERNAL, "Failed pathexport Windows caret test\n"); + exit(EXIT_FAILURE); + /* LCOV_EXCL_STOP */ + } +#else + pathimport(dst, sizeof(dst), "foo/bar"); + if (strcmp(dst, "foo/bar") != 0) { + /* LCOV_EXCL_START */ + log_fatal(EINTERNAL, "Failed pathimport Unix test\n"); + exit(EXIT_FAILURE); + /* LCOV_EXCL_STOP */ + } + + pathimport(dst, sizeof(dst), "foo^bar"); + if (strcmp(dst, "foo^bar") != 0) { + /* LCOV_EXCL_START */ + log_fatal(EINTERNAL, "Failed pathimport Unix caret test\n"); + exit(EXIT_FAILURE); + /* LCOV_EXCL_STOP */ + } + + pathexport(dst, sizeof(dst), "foo/bar"); + if (strcmp(dst, "foo/bar") != 0) { + /* LCOV_EXCL_START */ + log_fatal(EINTERNAL, "Failed pathexport Unix test\n"); + exit(EXIT_FAILURE); + /* LCOV_EXCL_STOP */ + } + + pathexport(dst, sizeof(dst), "foo^bar"); + if (strcmp(dst, "foo^bar") != 0) { + /* LCOV_EXCL_START */ + log_fatal(EINTERNAL, "Failed pathexport Unix caret test\n"); + exit(EXIT_FAILURE); + /* LCOV_EXCL_STOP */ + } +#endif +} + struct { const char* pattern; int valid; @@ -1215,6 +1312,21 @@ static const struct filter_scenario FILTER_SCENARIOS[] = { { "d1", "src/app.css", -1, 0, -1 }, { 0 } } + }, + { + "filter with literal caret in pattern and root", + { + { 1, 0, "", "/foo^bar" }, + { -1, 0, "proj^ect/sub/", "*.tmp" }, + { 0 } + }, + { + { "d1", "foo^bar", 0, 0, 0 }, + { "d1", "foo_bar", 0, 0, 0 }, + { "d1", "proj^ect/sub/file.tmp", -1, 0, 0 }, + { "d1", "proj^ect/sub/file.txt", 0, 0, 0 }, + { 0 } + } } }; @@ -1564,6 +1676,9 @@ void selftest(void) /* filter pattern parsing */ test_filter(); + /* pathname import and export */ + test_path(); + /* smartctl output parsing */ test_parse_smartctl(); diff --git a/cmdline/str.c b/cmdline/str.c index cbaafe5d..f2bd43b0 100644 --- a/cmdline/str.c +++ b/cmdline/str.c @@ -366,7 +366,11 @@ static inline int is_wild_escape(char c) || c == '?' || c == '[' || c == ']' +#ifdef _WIN32 + || c == '^'; +#else || c == '\\'; +#endif } int wnmatch_sub(const char* p, const char* t, int match_sub) @@ -375,13 +379,16 @@ int wnmatch_sub(const char* p, const char* t, int match_sub) while (*p) { char p0 = *p; switch (p0) { +#ifdef _WIN32 + case '^' : +#else case '\\' : +#endif /* * Escape wildcard metacharacters. * - * '\' is the canonical internal escape character. - * On Windows, pathimport() converts the external '^' - * escape syntax to this form before matching. + * If the following character is not escapable, the + * escape character itself is matched literally. */ if (is_wild_escape(p[1])) ++p; diff --git a/cmdline/str.h b/cmdline/str.h index 9f508629..1b03bf19 100644 --- a/cmdline/str.h +++ b/cmdline/str.h @@ -127,7 +127,7 @@ unsigned strtou(const char* nptr, char** endptr, int base); * - ** (not near a /) like * * - ##/ reduces to nothing in addition to the normal matching of ** (using # instead of * to not mess the C comment) * - [...] matches character classes with support for ranges and negation lile [!...] or [^...], except / - * - \ escapes *, ?, [, ], and \ + * - \ (Unix) or ^ (Windows) escapes *, ?, [, ], and the escape character itself * * \return 0 if it matches */ diff --git a/cmdline/support.c b/cmdline/support.c index cb22c7e9..b728d615 100644 --- a/cmdline/support.c +++ b/cmdline/support.c @@ -1094,19 +1094,10 @@ void pathimport(char* dst, size_t size, const char* src) pathcpy(dst, size, src); #ifdef _WIN32 - /* - * Convert the Windows dir separator '\' to C '/', - * and the Windows escaping char '^' to the fnmatch '\' - */ + /* Convert the Windows dir separator '\' to C '/' */ while (*dst) { - switch (*dst) { - case '\\' : + if (*dst == '\\') *dst = '/'; - break; - case '^' : - *dst = '\\'; - break; - } ++dst; } #endif @@ -1117,16 +1108,10 @@ void pathexport(char* dst, size_t size, const char* src) pathcpy(dst, size, src); #ifdef _WIN32 - /* invert the import */ + /* Convert C '/' to Windows dir separator '\' */ while (*dst) { - switch (*dst) { - case '/' : + if (*dst == '/') *dst = '\\'; - break; - case '\\' : - *dst = '^'; - break; - } ++dst; } #endif