Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 20 additions & 9 deletions include/zephyr/posix/fnmatch.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 /<tail> 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
}
Expand Down
194 changes: 193 additions & 1 deletion lib/posix/c_lib_ext/fnmatch.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{

Expand All @@ -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;
Expand Down Expand Up @@ -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 != ']') {
Expand Down Expand Up @@ -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;;) {
Expand Down
66 changes: 63 additions & 3 deletions tests/posix/c_lib_ext/src/fnmatch.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,29 @@

#include <zephyr/posix/fnmatch.h>
#include <zephyr/ztest.h>
#include <ctype.h>

/*
* 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
* https://git.musl-libc.org/cgit/libc-testsuite/tree/fnmatch.c
*/
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);
Expand Down Expand Up @@ -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);
}