util: add a helper to find files in a set of directories

Returns a number of paths for files with a given suffix in a
priority-sorted list of directories.

Part-of: <https://gitlab.freedesktop.org/libinput/libinput/-/merge_requests/1204>
This commit is contained in:
Peter Hutterer 2025-04-22 11:50:12 +10:00 committed by Marge Bot
parent 95a417cefa
commit efea8463fe
4 changed files with 252 additions and 0 deletions

View file

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

139
src/util-files.c Normal file
View file

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

View file

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

View file

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