diff --git a/meson.build b/meson.build index b105c593..98e500b2 100644 --- a/meson.build +++ b/meson.build @@ -307,6 +307,7 @@ foreach h: util_headers endforeach src_libinput_util = [ + 'src/util-files.c', 'src/util-list.c', 'src/util-ratelimit.c', 'src/util-strings.c', diff --git a/src/util-files.c b/src/util-files.c new file mode 100644 index 00000000..c9bde03e --- /dev/null +++ b/src/util-files.c @@ -0,0 +1,139 @@ +/* + * Copyright © 2024 Red Hat, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include "config.h" + +#include "libinput-versionsort.h" +#include "util-macros.h" +#include "util-files.h" +#include "util-strings.h" +#include "util-list.h" + +struct file { + struct list link; + char *name; + char *directory; +}; + +static void +file_destroy(struct file *f) +{ + list_remove(&f->link); + free(f->name); + free(f->directory); + free(f); +} + +DEFINE_DESTROY_CLEANUP_FUNC(file); + +/** + * Appends to the given list all files files in the given directory that end + * with the given with the given suffix. + */ +static void +filenames(const char *directory, const char *suffix, struct list *list) +{ + _autofree_ struct dirent **namelist = NULL; + + int ndev = scandir(directory, &namelist, NULL, versionsort); + if (ndev <= 0) + return; + + for (int i = 0; i < ndev; i++) { + _autofree_ struct dirent *entry = namelist[i]; + if (!strendswith(entry->d_name, suffix)) + continue; + + struct file *f = zalloc(sizeof(*f)); + f->name = safe_strdup(entry->d_name); + f->directory = safe_strdup(directory); + list_append(list, &f->link); + } +} + +static int +filenamesort(const void *a, const void *b) +{ + const struct file *f1 = *(const struct file **)a; + const struct file *f2 = *(const struct file **)b; + + return strverscmp(f1->name, f2->name); +} + +char ** +list_files(const char **directories, + const char *suffix, + size_t *nfiles_out) +{ + struct list files = LIST_INIT(files); + + const char **d = directories; + while (*d) { + struct list new_files = LIST_INIT(new_files); + filenames(*d, suffix, &new_files); + + struct file *old_file; + list_for_each_safe(old_file, &files, link) { + struct file *new_file; + list_for_each_safe(new_file, &new_files, link) { + if (streq(old_file->name, new_file->name)) { + file_destroy(new_file); + break; + } + } + } + struct file *new_file; + list_for_each_safe(new_file, &new_files, link) { + list_remove(&new_file->link); + list_append(&files, &new_file->link); + } + d++; + } + + size_t nfiles = 0; + struct file *f; + list_for_each(f, &files, link) { + nfiles++; + } + /* Allocating +1 conveniently handles the directories[0] = NULL case */ + _autofree_ struct file **fs = zalloc((nfiles + 1) * sizeof(*fs)); + size_t idx = 0; + list_for_each_safe(f, &files, link) { + fs[idx++] = f; + list_remove(&f->link); + list_init(&f->link); // So we can file_destroy it later + } + + qsort(fs, nfiles, sizeof(*fs), filenamesort); + + char **paths = zalloc((nfiles + 1) * sizeof(*paths)); + for (size_t i = 0; i < nfiles; i++) { + _destroy_(file) *f = fs[i]; + paths[i] = strdup_printf("%s/%s", f->directory, f->name); + } + + if (nfiles_out) + *nfiles_out = nfiles; + + return steal(&paths); +} diff --git a/src/util-files.h b/src/util-files.h index 1c1639d4..0cb51b15 100644 --- a/src/util-files.h +++ b/src/util-files.h @@ -62,3 +62,21 @@ xclose(int *fd) *fd = -1; } } + +/** + * In the NULL-terminated list of directories + * search for files with the given suffix and return + * a filename-ordered NULL-terminated list of those + * full paths. + * + * The directories are given in descending priority order. + * Any file with a given filename shadows the same file + * in another directory of lower sorting order. + * + * If nfiles is not NULL, it is set to the number of + * files returned (not including the NULL terminator). + */ +char ** +list_files(const char **directories, + const char *suffix, + size_t *nfiles); diff --git a/test/test-utils.c b/test/test-utils.c index 2c5e0d42..91775268 100644 --- a/test/test-utils.c +++ b/test/test-utils.c @@ -75,6 +75,99 @@ START_TEST(mkdir_p_test) } END_TEST +START_TEST(find_files_test) +{ + _autofree_ char *dirname = strdup("/tmp/litest_find_files_test.XXXXXX"); + mkdtemp(dirname); + + _autofree_ char *d1 = strdup_printf("%s/d1", dirname); + _autofree_ char *d2 = strdup_printf("%s/d2", dirname); + _autofree_ char *d3 = strdup_printf("%s/d3", dirname); + + litest_assert_neg_errno_success(mkdir_p(d1)); + litest_assert_neg_errno_success(mkdir_p(d2)); + litest_assert_neg_errno_success(mkdir_p(d3)); + + struct f { + const char *name; + const char *dir1; + const char *dir2; + const char *dir3; + char *expected; + } files[] = { + { "10-abc.suf", d1, d2, d3 }, + { "20-def.suf", d1, NULL, d3 }, + { "30-ghi.suf", d1, d2, NULL }, + { "40-jkl.suf", NULL, d2, NULL }, + { "50-mno.suf", NULL, d2, d3 }, + { "60-pgr.suf", NULL, NULL, d3 }, + { "70-abc.suf", NULL, NULL, d3 }, + { "21-xyz.fix", NULL, NULL, d3 }, + { "35-uvw.fix", NULL, d2, d3 }, + { "70-rst.fix", d1, NULL, d3 }, + { NULL }, + }; + for (struct f *f = files; f->name; f++) { + if (f->dir1) { + _autofree_ char *path = strdup_printf("%s/%s", f->dir1, f->name); + close(open(path, O_WRONLY | O_CREAT, 0644)); + f->expected = steal(&path); + } + if (f->dir2) { + _autofree_ char *path = strdup_printf("%s/%s", f->dir2, f->name); + close(open(path, O_WRONLY | O_CREAT, 0644)); + if (!f->expected) + f->expected = steal(&path); + } + if (f->dir3) { + _autofree_ char *path = strdup_printf("%s/%s", f->dir3, f->name); + close(open(path, O_WRONLY | O_CREAT, 0644)); + if (!f->expected) + f->expected = steal(&path); + } + } + + const char *dirs[] = {d1, d2, d3, NULL}; + size_t nfiles; + _autostrvfree_ char **paths = list_files(dirs, "suf", &nfiles); + litest_assert_int_eq(nfiles, (size_t)7); + litest_assert_str_eq(paths[0], files[0].expected); + litest_assert_str_eq(paths[1], files[1].expected); + litest_assert_str_eq(paths[2], files[2].expected); + litest_assert_str_eq(paths[3], files[3].expected); + litest_assert_str_eq(paths[4], files[4].expected); + litest_assert_str_eq(paths[5], files[5].expected); + litest_assert_str_eq(paths[6], files[6].expected); + litest_assert_str_eq(paths[7], NULL); + + for (struct f *f = files; f->name; f++) { + if (f->dir1) { + _autofree_ char *path = strdup_printf("%s/%s", f->dir1, f->name); + unlink(path); + } + if (f->dir2) { + _autofree_ char *path = strdup_printf("%s/%s", f->dir2, f->name); + unlink(path); + } + if (f->dir3) { + _autofree_ char *path = strdup_printf("%s/%s", f->dir3, f->name); + unlink(path); + } + free(f->expected); + } + rmdir(d1); + rmdir(d2); + rmdir(d3); + rmdir(dirname); + + const char *empty[] = {NULL}; + _autostrvfree_ char** empty_path = list_files(empty, "suf", &nfiles); + litest_assert_int_eq(nfiles, (size_t)0); + litest_assert_ptr_notnull(empty_path); + litest_assert_ptr_null(empty_path[0]); +} +END_TEST + START_TEST(array_for_each) { int ai[6]; @@ -2313,6 +2406,7 @@ int main(void) ADD_TEST(auto_test); ADD_TEST(mkdir_p_test); + ADD_TEST(find_files_test); ADD_TEST(array_for_each);