Fix literal caret handling in Windows path normalization and wildcard matching

This commit is contained in:
Andrea Mazzoleni 2026-08-26 15:09:30 +02:00
parent 69a459ee43
commit 4fdfc8a923
4 changed files with 130 additions and 23 deletions

View file

@ -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();

View file

@ -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;

View file

@ -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
*/

View file

@ -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