diff --git a/include/zephyr/posix/fnmatch.h b/include/zephyr/posix/fnmatch.h index 41e77aa70c149..3171898264f28 100644 --- a/include/zephyr/posix/fnmatch.h +++ b/include/zephyr/posix/fnmatch.h @@ -36,21 +36,32 @@ #ifndef _FNMATCH_H_ #define _FNMATCH_H_ -#define FNM_NOMATCH 1 /* Match failed. */ -#define FNM_NOSYS 2 /* Function not implemented. */ -#define FNM_NORES 3 /* Out of resources */ +#define FNM_NOMATCH 1 /**< Match failed */ -#define FNM_NOESCAPE 0x01 /* Disable backslash escaping. */ -#define FNM_PATHNAME 0x02 /* Slash must be matched by slash. */ -#define FNM_PERIOD 0x04 /* Period must be matched by period. */ -#define FNM_CASEFOLD 0x08 /* Pattern is matched case-insensitive */ -#define FNM_LEADING_DIR 0x10 /* Ignore / after Imatch. */ +#define FNM_NOESCAPE 0x01 /**< Disable backslash escaping */ +#define FNM_PATHNAME 0x02 /**< Slash must be matched by slash */ +#define FNM_PERIOD 0x04 /**< Period must be matched by period */ +#define FNM_CASEFOLD 0x08 /**< Pattern is matched case-insensitive */ +#define FNM_LEADING_DIR \ + 0x10 /**< Only match the initial segment of a string up to the first '/' \ + */ #ifdef __cplusplus extern "C" { #endif -int fnmatch(const char *, const char *, int); +/** + * @brief Check if a filename or input string matches a shell-style matching pattern. + * + * @param pattern pattern that is matched against @param string + * @param string input string to match against @param pattern + * @param flags flags used to signal special matching conditions such as @ref FNM_NOESCAPE + * + * + * @retval 0 pattern found in string + * @retval FNM_NOMATCH pattern not found in string + */ +int fnmatch(const char *pattern, const char *string, int flags); #ifdef __cplusplus } diff --git a/lib/posix/c_lib_ext/fnmatch.c b/lib/posix/c_lib_ext/fnmatch.c index 911b6f41afa70..09e0bda4e9074 100644 --- a/lib/posix/c_lib_ext/fnmatch.c +++ b/lib/posix/c_lib_ext/fnmatch.c @@ -48,6 +48,160 @@ #define EOS '\0' +#define MATCH_CLASS6(p, a, b, c, d, e, f, g) \ + ((p)[0] == (a) && (p)[1] == (b) && (p)[2] == (c) && (p)[3] == (d) && (p)[4] == (e) && \ + (p)[5] == (f) && (p)[6] == (g)) + +#define MATCH_CLASS7(p, a, b, c, d, e, f, g, h) \ + ((p)[0] == (a) && (p)[1] == (b) && (p)[2] == (c) && (p)[3] == (d) && (p)[4] == (e) && \ + (p)[5] == (f) && (p)[6] == (g) && (p)[7] == (h)) + +enum fnm_char_class { + FNM_CC_ALNUM, + FNM_CC_ALPHA, + FNM_CC_BLANK, + FNM_CC_CNTRL, + FNM_CC_DIGIT, + FNM_CC_GRAPH, + FNM_CC_LOWER, + FNM_CC_PRINT, + FNM_CC_PUNCT, + FNM_CC_SPACE, + FNM_CC_UPPER, + FNM_CC_XDIGIT, + FNM_CC_INVALID, +}; + +static bool fnm_cc_is_valid(const char *pattern, size_t psize, enum fnm_char_class *cc) +{ + if (psize < 4 || *pattern != ':') { + return false; + } + + pattern++; /* skip ':' */ + psize--; + + /* Each class name ends with ":]" */ + switch (pattern[0]) { + case 'a': + if (MATCH_CLASS6(pattern, 'a', 'l', 'n', 'u', 'm', ':', ']')) { + *cc = FNM_CC_ALNUM; + return true; + } + if (MATCH_CLASS6(pattern, 'a', 'l', 'p', 'h', 'a', ':', ']')) { + *cc = FNM_CC_ALPHA; + return true; + } + break; + + case 'b': + if (MATCH_CLASS6(pattern, 'b', 'l', 'a', 'n', 'k', ':', ']')) { + *cc = FNM_CC_BLANK; + return true; + } + break; + + case 'c': + if (MATCH_CLASS6(pattern, 'c', 'n', 't', 'r', 'l', ':', ']')) { + *cc = FNM_CC_CNTRL; + return true; + } + break; + + case 'd': + if (MATCH_CLASS6(pattern, 'd', 'i', 'g', 'i', 't', ':', ']')) { + *cc = FNM_CC_DIGIT; + return true; + } + break; + + case 'g': + if (MATCH_CLASS6(pattern, 'g', 'r', 'a', 'p', 'h', ':', ']')) { + *cc = FNM_CC_GRAPH; + return true; + } + break; + + case 'l': + if (MATCH_CLASS6(pattern, 'l', 'o', 'w', 'e', 'r', ':', ']')) { + *cc = FNM_CC_LOWER; + return true; + } + break; + + case 'p': + if (MATCH_CLASS6(pattern, 'p', 'r', 'i', 'n', 't', ':', ']')) { + *cc = FNM_CC_PRINT; + return true; + } + if (MATCH_CLASS6(pattern, 'p', 'u', 'n', 'c', 't', ':', ']')) { + *cc = FNM_CC_PUNCT; + return true; + } + break; + + case 's': + if (MATCH_CLASS6(pattern, 's', 'p', 'a', 'c', 'e', ':', ']')) { + *cc = FNM_CC_SPACE; + return true; + } + break; + + case 'u': + if (MATCH_CLASS6(pattern, 'u', 'p', 'p', 'e', 'r', ':', ']')) { + *cc = FNM_CC_UPPER; + return true; + } + break; + + case 'x': + if (MATCH_CLASS7(pattern, 'x', 'd', 'i', 'g', 'i', 't', ':', ']')) { + *cc = FNM_CC_XDIGIT; + return true; + } + break; + + default: + break; + } + + return false; +} + +static inline int fnm_cc_match(int c, enum fnm_char_class cc) +{ + switch (cc) { + case FNM_CC_ALNUM: + return isalnum(c); + case FNM_CC_ALPHA: + return isalpha(c); + case FNM_CC_BLANK: + return isblank(c); + case FNM_CC_CNTRL: + return iscntrl(c); + case FNM_CC_DIGIT: + return isdigit(c); + case FNM_CC_GRAPH: + return isgraph(c); + case FNM_CC_LOWER: + return islower(c); + case FNM_CC_PRINT: + return isprint(c); + case FNM_CC_PUNCT: + return ispunct(c); + case FNM_CC_SPACE: + return isspace(c); + case FNM_CC_UPPER: + return isupper(c); + case FNM_CC_XDIGIT: + return isxdigit(c); + default: + break; + } + + return 0; +} + static inline int foldcase(int ch, int flags) { @@ -60,6 +214,27 @@ static inline int foldcase(int ch, int flags) #define FOLDCASE(ch, flags) foldcase((unsigned char)(ch), (flags)) +static bool match_posix_class(const char **pattern, int test) +{ + enum fnm_char_class cc; + + const char *p = *pattern; + size_t remaining = strlen(p); + + if (!fnm_cc_is_valid(p, remaining, &cc)) { + return false; + } + + /* move pattern pointer past ":]" */ + const char *end = strstr(p, ":]"); + + if (end) { + *pattern = end + 2; + } + + return fnm_cc_match(test, cc); +} + static const char *rangematch(const char *pattern, int test, int flags) { bool negate, ok, need; @@ -99,6 +274,23 @@ static const char *rangematch(const char *pattern, int test, int flags) return NULL; } + if (c == '[' && *pattern == ':') { + if (match_posix_class(&pattern, test)) { + ok = true; + continue; + } else { + /* skip over class if unrecognized */ + while (*pattern && !(*pattern == ':' && *(pattern + 1) == ']')) { + pattern++; + } + + if (*pattern) { + pattern += 2; + } + continue; + } + } + if (*pattern == '-') { c2 = FOLDCASE(*(pattern + 1), flags); if (c2 != EOS && c2 != ']') { @@ -133,7 +325,7 @@ static int fnmatchx(const char *pattern, const char *string, int flags, size_t r } if (recursion-- == 0) { - return FNM_NORES; + return FNM_NOMATCH; } for (stringstart = string;;) { diff --git a/tests/posix/c_lib_ext/src/fnmatch.c b/tests/posix/c_lib_ext/src/fnmatch.c index 42c84d7ca1a0f..696feb66387cc 100644 --- a/tests/posix/c_lib_ext/src/fnmatch.c +++ b/tests/posix/c_lib_ext/src/fnmatch.c @@ -6,6 +6,22 @@ #include #include +#include + +/* + * Note: the \x00 control character is specifically excluded below, since testing for it is + * equivalent to reading past the end of a '\0'-terminated string (i.e. can fault). + */ +#define TEST_BLANK_CHARS " \t" +#define TEST_CNTRL_CHARS \ + "\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16" \ + "\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x7f" +#define TEST_DIGIT_CHARS "0123456789" +#define TEST_LOWER_CHARS "abcdefghijklmnopqrstuvwxyz" +#define TEST_PUNCT_CHARS "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~" +#define TEST_SPACE_CHARS " \f\n\r\t\v" +#define TEST_UPPER_CHARS "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +#define TEST_XDIGIT_CHARS TEST_DIGIT_CHARS "ABCDEFabcdef" /* * Adapted from @@ -13,8 +29,6 @@ */ ZTEST(posix_c_lib_ext, test_fnmatch) { - /* Note: commented out lines indicate known problems to be addressed in #55186 */ - zassert_ok(fnmatch("*.c", "foo.c", 0)); zassert_ok(fnmatch("*.c", ".c", 0)); zassert_equal(fnmatch("*.a", "foo.c", 0), FNM_NOMATCH); @@ -73,11 +87,57 @@ ZTEST(posix_c_lib_ext, test_fnmatch) zassert_equal(fnmatch("*/*", "a/.b", FNM_PATHNAME | FNM_PERIOD), FNM_NOMATCH); zassert_ok(fnmatch("*?*/*", "a/.b", FNM_PERIOD)); zassert_ok(fnmatch("*[.]/b", "a./b", FNM_PATHNAME | FNM_PERIOD)); - /* zassert_ok(fnmatch("*[[:alpha:]]/""*[[:alnum:]]", "a/b", FNM_PATHNAME)); */ + zassert_ok(fnmatch("*[[:alpha:]]/""*[[:alnum:]]", "a/b", FNM_PATHNAME)); zassert_not_equal(fnmatch("*[![:digit:]]*/[![:d-d]", "a/b", FNM_PATHNAME), 0); zassert_not_equal(fnmatch("*[![:digit:]]*/[[:d-d]", "a/[", FNM_PATHNAME), 0); zassert_not_equal(fnmatch("*[![:digit:]]*/[![:d-d]", "a/[", FNM_PATHNAME), 0); zassert_ok(fnmatch("a?b", "a.b", FNM_PATHNAME | FNM_PERIOD)); zassert_ok(fnmatch("a*b", "a.b", FNM_PATHNAME | FNM_PERIOD)); zassert_ok(fnmatch("a[.]b", "a.b", FNM_PATHNAME | FNM_PERIOD)); + + /* Additional test cases for POSIX character classes (C-locale only) */ + static const struct test_data_s { + const char *pattern; + const char *match; + const char *nomatch; + } test_data[] = { + {"[[:alnum:]]", TEST_DIGIT_CHARS TEST_UPPER_CHARS TEST_LOWER_CHARS, " "}, + {"[[:alpha:]]", TEST_UPPER_CHARS TEST_LOWER_CHARS, "0"}, + {"[[:blank:]]", TEST_BLANK_CHARS, "x"}, + {"[[:cntrl:]]", TEST_CNTRL_CHARS, "x"}, + {"[[:digit:]]", TEST_DIGIT_CHARS, "a"}, + {"[[:graph:]]", TEST_DIGIT_CHARS TEST_UPPER_CHARS TEST_LOWER_CHARS TEST_PUNCT_CHARS, + " "}, + {"[[:lower:]]", TEST_LOWER_CHARS, "X"}, + {"[[:print:]]", + TEST_DIGIT_CHARS TEST_UPPER_CHARS TEST_LOWER_CHARS TEST_PUNCT_CHARS " ", "\t"}, + {"[[:punct:]]", TEST_PUNCT_CHARS, "x"}, + {"[[:space:]]", TEST_SPACE_CHARS, "x"}, + {"[[:upper:]]", TEST_UPPER_CHARS, "x"}, + {"[[:xdigit:]]", TEST_XDIGIT_CHARS, "h"}, + }; + + ARRAY_FOR_EACH_PTR(test_data, data) { + /* ensure that characters in "nomatch" do not match "pattern" */ + for (size_t j = 0; j < strlen(data->nomatch); j++) { + char input[] = {data->nomatch[j], '\0'}; + + zexpect_equal(fnmatch(data->pattern, input, 0), FNM_NOMATCH, + "pattern \"%s\" unexpectedly matched char 0x%02x (%c)", + data->pattern, data->nomatch[j], + isprint(data->nomatch[j]) ? data->nomatch[j] : '.'); + } + + /* ensure that characters in "match" do match "pattern" */ + for (size_t j = 0; j < strlen(data->match); j++) { + char input[] = {data->match[j], '\0'}; + + zexpect_ok(fnmatch(data->pattern, input, 0), + "pattern \"%s\" did not match char 0x%02x (%c)", data->pattern, + data->match[j], isprint(data->match[j]) ? data->match[j] : '.'); + } + } + + /* ensure that an invalid character class generates an error */ + zassert_equal(fnmatch("[[:foobarbaz:]]", "Z", 0), FNM_NOMATCH); }