diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index c449166d..d5c0bb9d 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -19,6 +19,7 @@ add_subdirectory(crc) add_subdirectory(crypto) add_subdirectory(fifo) add_subdirectory(fsm) +add_subdirectory(glob) add_subdirectory(guards) add_subdirectory(hashmap) add_subdirectory(hashtbl) diff --git a/examples/glob/.gitignore b/examples/glob/.gitignore new file mode 100644 index 00000000..86113e2d --- /dev/null +++ b/examples/glob/.gitignore @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +/glob diff --git a/examples/glob/CMakeLists.txt b/examples/glob/CMakeLists.txt new file mode 100644 index 00000000..9d9e5d3b --- /dev/null +++ b/examples/glob/CMakeLists.txt @@ -0,0 +1,21 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# +# Copyright(c) 2023 John Sanpe +# + +add_executable(glob utils.c) +target_link_libraries(glob bfdev) + +if(${CMAKE_PROJECT_NAME} STREQUAL "bfdev") + install(FILES + utils.c + DESTINATION + ${CMAKE_INSTALL_DOCDIR}/examples/glob + ) + + install(TARGETS + glob + DESTINATION + ${CMAKE_INSTALL_DOCDIR}/bin + ) +endif() diff --git a/examples/glob/utils.c b/examples/glob/utils.c new file mode 100644 index 00000000..f0a84ce1 --- /dev/null +++ b/examples/glob/utils.c @@ -0,0 +1,31 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright(c) 2025 John Sanpe + */ + +#define MODULE_NAME "bfdev-glob" +#define bfdev_log_fmt(fmt) MODULE_NAME ": " fmt + +#include +#include + +int +main(int argc, const char *argv[]) +{ + unsigned int count; + const char *patten; + bool result; + + if (argc < 2) { + bfdev_log_alert("Usage: %s patten string ...\n", argv[0]); + return 1; + } + + patten = argv[1]; + for (count = 2; count < argc; ++count) { + result = bfdev_glob(patten, argv[count]); + bfdev_log_info("matching %s: %s\n", argv[count], result ? "yes" : "no"); + } + + return 0; +} diff --git a/include/bfdev/glob.h b/include/bfdev/glob.h new file mode 100644 index 00000000..f3cd0bd0 --- /dev/null +++ b/include/bfdev/glob.h @@ -0,0 +1,20 @@ +/* SPDX-License-Identifier: LGPL-3.0-or-later */ +/* + * Copyright(c) 2025 John Sanpe + */ + +#ifndef _BFDEV_GLOB_H_ +#define _BFDEV_GLOB_H_ + +#include +#include +#include + +BFDEV_BEGIN_DECLS + +extern bool +bfdev_glob(const char *patten, const char *string); + +BFDEV_END_DECLS + +#endif /* _BFDEV_GLOB_H_ */ diff --git a/src/build.cmake b/src/build.cmake index 9199e15d..f2367d8a 100644 --- a/src/build.cmake +++ b/src/build.cmake @@ -22,6 +22,7 @@ set(BFDEV_SOURCE ${CMAKE_CURRENT_LIST_DIR}/errname.c ${CMAKE_CURRENT_LIST_DIR}/fifo.c ${CMAKE_CURRENT_LIST_DIR}/fsm.c + ${CMAKE_CURRENT_LIST_DIR}/glob.c ${CMAKE_CURRENT_LIST_DIR}/hashmap.c ${CMAKE_CURRENT_LIST_DIR}/heap.c ${CMAKE_CURRENT_LIST_DIR}/ilist.c diff --git a/src/glob.c b/src/glob.c new file mode 100644 index 00000000..8d763d4c --- /dev/null +++ b/src/glob.c @@ -0,0 +1,83 @@ +/* SPDX-License-Identifier: LGPL-3.0-or-later */ +/* + * Copyright(c) 2025 John Sanpe + */ + +#include +#include +#include + +export bool +bfdev_glob(const char *patten, const char *string) +{ + const char *class, *bpatten, *bstring; + char ptch, stch, tcha, tchb; + bool match, inverted; + + bpatten = NULL; + bstring = NULL; + + for (;;) { + ptch = *patten++; + stch = *string++; + + switch (ptch) { + case '?': + if (stch == '\0') + return false; + break; + + case '*': + if (*patten == '\0') + return true; + bpatten = patten; + bstring = --string; + break; + + case '[': + match = false; + inverted = *patten == '!'; + class = patten + inverted; + tcha = *class++; + + do { + tchb = tcha; + if (tcha == '\0') + goto literal; + + if (class[0] == '-' && class[1] != ']') { + tchb = class[1]; + if (tchb == '\0') + goto literal; + class += 2; + } + match |= tcha <= stch && stch <= tchb; + } while ((tcha = *class++) != ']'); + + if (match == inverted) + goto backtrack; + + patten = class; + break; + + case '\\': + ptch = *patten++; + bfdev_fallthrough; + + default: literal: + if (ptch == stch) { + if (ptch == '\0') + return true; + break; + } + + backtrack: + if (stch == '\0' || !bpatten) + return false; + + patten = bpatten; + string = ++bstring; + break; + } + } +} diff --git a/testsuite/CMakeLists.txt b/testsuite/CMakeLists.txt index fa0f4a41..5bba07cf 100644 --- a/testsuite/CMakeLists.txt +++ b/testsuite/CMakeLists.txt @@ -9,6 +9,7 @@ include(testsuite.cmake) add_subdirectory(array) add_subdirectory(bitwalk) add_subdirectory(fifo) +add_subdirectory(glob) add_subdirectory(hlist) add_subdirectory(list) add_subdirectory(memalloc) diff --git a/testsuite/glob/.gitignore b/testsuite/glob/.gitignore new file mode 100644 index 00000000..ffc5fcc1 --- /dev/null +++ b/testsuite/glob/.gitignore @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +/fifo-selftest diff --git a/testsuite/glob/CMakeLists.txt b/testsuite/glob/CMakeLists.txt new file mode 100644 index 00000000..b2a9b23c --- /dev/null +++ b/testsuite/glob/CMakeLists.txt @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# +# Copyright(c) 2024 John Sanpe +# + +add_executable(glob-selftest selftest.c) +target_link_libraries(glob-selftest bfdev testsuite) +add_test(glob-selftest glob-selftest) + +if(${CMAKE_PROJECT_NAME} STREQUAL "bfdev") + install(TARGETS + glob-selftest + DESTINATION + ${CMAKE_INSTALL_DOCDIR}/testsuite + ) +endif() diff --git a/testsuite/glob/selftest.c b/testsuite/glob/selftest.c new file mode 100644 index 00000000..95a492b2 --- /dev/null +++ b/testsuite/glob/selftest.c @@ -0,0 +1,120 @@ +/* SPDX-License-Identifier GPL-2.0-or-later */ +/* + * Copyright(c) 2022-2024 John Sanpe + */ + +#define MODULE_NAME "bitwalk-selftest" +#define bfdev_log_fmt(fmt) MODULE_NAME ":" fmt + +#include +#include +#include +#include + +struct glob_test { + const char *glob; + const char *string; + bool result; +}; + +static const struct glob_test +test_glob[] = { + /* Some basic tests */ + {"a", "a", true}, + {"a", "b", false}, + {"a", "aa", false}, + {"a", "", false}, + {"", "", true}, + {"", "a", false}, + + /* Simple character class tests */ + {"[a]", "a", true}, + {"[a]", "b", false}, + {"[!a]", "a", false}, + {"[!a]", "b", true}, + {"[ab]", "a", true}, + {"[ab]", "b", true}, + {"[ab]", "c", false}, + {"[!ab]", "c", true}, + {"[a-c]", "b", true}, + {"[a-c]", "d", false}, + + /* Corner cases in character class parsing */ + {"[a-c-e-g]", "-", true}, + {"[a-c-e-g]", "d", false}, + {"[a-c-e-g]", "f", true}, + {"[]a-ceg-ik[]", "a", true}, + {"[]a-ceg-ik[]", "]", true}, + {"[]a-ceg-ik[]", "[", true}, + {"[]a-ceg-ik[]", "h", true}, + {"[]a-ceg-ik[]", "f", false}, + {"[!]a-ceg-ik[]", "h", false}, + {"[!]a-ceg-ik[]", "]", false}, + {"[!]a-ceg-ik[]", "f", true}, + + /* Simple wild cards */ + {"?", "a", true}, + {"?", "aa", false}, + {"??", "a", false}, + {"?x?", "axb", true}, + {"?x?", "abx", false}, + {"?x?", "xab", false}, + + /* Asterisk wild cards (backtracking) */ + {"*??", "a", false}, + {"*??", "ab", true}, + {"*??", "abc", true}, + {"*??", "abcd", true}, + {"??*", "a", false}, + {"??*", "ab", true}, + {"??*", "abc", true}, + {"??*", "abcd", true}, + {"?*?", "a", false}, + {"?*?", "ab", true}, + {"?*?", "abc", true}, + {"?*?", "abcd", true}, + {"*b", "b", true}, + {"*b", "ab", true}, + {"*b", "ba", false}, + {"*b", "bb", true}, + {"*b", "abb", true}, + {"*b", "bab", true}, + {"*bc", "abbc", true}, + {"*bc", "bc", true}, + {"*bc", "bbc", true}, + {"*bc", "bcbc", true}, + + /* Multiple asterisks (complex backtracking) */ + {"*ac*", "abacadaeafag", true}, + {"*ac*ae*ag*", "abacadaeafag", true}, + {"*a*b*[bc]*[ef]*g*", "abacadaeafag", true}, + {"*a*b*[ef]*[cd]*g*", "abacadaeafag", false}, + {"*abcd*", "abcabcabcabcdefg", true}, + {"*ab*cd*", "abcabcabcabcdefg", true}, + {"*abcd*abcdef*", "abcabcdabcdeabcdefg", true}, + {"*abcd*", "abcabcabcabcefg", false}, + {"*ab*cd*", "abcabcabcabcefg", false}, +}; + +TESTSUITE( + "glob:selftest", NULL, NULL, + "glob selftest" +) { + unsigned int count; + + for (count = 0; count < BFDEV_ARRAY_SIZE(test_glob); ++count) { + const struct glob_test *test = test_glob + count; + bool matched; + + matched = bfdev_glob(test->glob, test->string); + bfdev_log_info("match '%s' with '%s': result %d\n", + test->string, test->glob, matched); + + if (matched != test->result) { + bfdev_log_err("result should be %d\n", test->result); + return -BFDEV_EFAULT; + } + } + + return -BFDEV_ENOERR; +}