diff --git a/tests/Makefile.am b/tests/Makefile.am index 036a136..7d92e9f 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -9,7 +9,7 @@ EXTRA_DIST = package.m4 testsuite.at $(TESTSUITE) $(TESTS_) TESTS_ = lib_hash.at lib_prng.at lib_watcher.at lib_sandbox.at \ - lib_hashmap.at fs_basic.at fs_regrw.at + lib_hashmap.at fs_basic.at fs_regrw.at fs_dents.at # Helper programs for testing @@ -35,7 +35,7 @@ if BOOKMARKFS_MOUNT if BOOKMARKFS_UTIL check_fs_CPPFLAGS += -DHAVE_BOOKMARKFS_UTIL check_fs_LDADD += $(top_builddir)/src/libbookmarkfs_util.la - check_fs_SOURCES += check_fs_regrw.c check_util.c + check_fs_SOURCES += check_fs_regrw.c check_fs_dents.c check_util.c endif # BOOKMARKFS_UTIL endif # BOOKMARKFS_MOUNT diff --git a/tests/check_fs.c b/tests/check_fs.c index 47fd85e..2fa9717 100644 --- a/tests/check_fs.c +++ b/tests/check_fs.c @@ -57,6 +57,8 @@ dispatch_subcmds ( #ifdef HAVE_BOOKMARKFS_UTIL } else if (0 == strcmp("regrw", cmd)) { status = check_fs_regrw(argc, argv); + } else if (0 == strcmp("dents", cmd)) { + status = check_fs_dents(argc, argv); #endif } else { log_printf("bad subcmd '%s'", cmd); diff --git a/tests/check_fs_dents.c b/tests/check_fs_dents.c new file mode 100644 index 0000000..5774f7b --- /dev/null +++ b/tests/check_fs_dents.c @@ -0,0 +1,282 @@ +/** + * bookmarkfs/tests/check_fs_dents.c + * ---- + * + * Copyright (C) 2025 CismonX + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include +#include +#include + +#include +#include +#include + +#include "check_util.h" +#include "frontend_util.h" +#include "ioctl.h" +#include "prng.h" + +#define ITEM_DELETED ( 1u << 0 ) +#define ITEM_DIRTY ( 1u << 1 ) +#define ITEM_MARKED ( 1u << 2 ) + +struct check_item { + int id; + unsigned flags; +}; + +// Forward declaration start +static int dent_check (int, struct check_item *, int const *, int, int); +static int dent_delete (int, struct check_item *); +static int dent_new (int, struct check_item *); +static int dent_permute (int, struct check_item *, struct check_item *); +static int do_check_fs_dents (int, int); +// Forward declaration end + +static int +dent_check ( + int dirfd, + struct check_item *item, + int const *map, + int n, + int ignore_dirty +) { + char buf[4096]; + struct check_item const *last_found = NULL; + for (ssize_t off = 0, len = 0; ; ) { + if (off == len) { + len = xgetdents(dirfd, buf, sizeof(buf)); + if (len < 0) { + log_printf("getdents(): %s", strerror(errno)); + return -1; + } + if (len == 0) { + break; + } + off = 0; + } + struct dirent *dent = (struct dirent *)(buf + off); + off += dent->d_reclen; + + int id = atoi(dent->d_name); + if (id < 0 || id >= n) { + return -1; + } + struct check_item *found = item + map[id]; + if (found->flags & (ITEM_DELETED | ITEM_MARKED)) { + return -1; + } + if (ignore_dirty && found->flags & ITEM_DIRTY) { + continue; + } + if (last_found != NULL && found <= last_found) { + return -1; + } + last_found = found; + found->flags |= ITEM_MARKED; + } + + for (last_found = item + n; item < last_found; ++item) { + if (ignore_dirty && item->flags & ITEM_DIRTY) { + continue; + } + if (!(item->flags & (ITEM_DELETED | ITEM_MARKED))) { + return -1; + } + item->flags &= ~ITEM_MARKED; + } + return 0; +} + +static int +dent_delete ( + int dirfd, + struct check_item *item +) { + if (item->flags & ITEM_DELETED) { + return 0; + } + + char name[16]; + sprintf(name, "%d", item->id); + if (0 != unlinkat(dirfd, name, 0)) { + log_printf("unlinkat(): %s", strerror(errno)); + return -1; + } + item->flags |= ITEM_DELETED; + return 0; +} + +static int +dent_new ( + int dirfd, + struct check_item *item +) { + char name[16]; + sprintf(name, "%d", item->id); + int fd = openat(dirfd, name, O_CREAT | O_EXCL, 0600); + if (fd < 0) { + log_printf("openat(): %s", strerror(errno)); + return -1; + } + close(fd); + item->flags &= ~ITEM_DELETED; + return 0; +} + +static int +dent_permute ( + int dirfd, + struct check_item *item1, + struct check_item *item2 +) { + int op = BOOKMARKFS_PERMD_OP_SWAP; + struct check_item *itemx = item2; + if ((item1->flags & ITEM_DELETED) || (item2->flags & ITEM_DELETED)) { + if (item1->flags & ITEM_DELETED) { + struct check_item *item_tmp = item1; + item1 = item2; + item2 = item_tmp; + } + if (item1 > item2) { + op = BOOKMARKFS_PERMD_OP_MOVE_BEFORE; + for (itemx = item2 + 1; itemx->flags & ITEM_DELETED; ++itemx); + } else { + op = BOOKMARKFS_PERMD_OP_MOVE_AFTER; + for (itemx = item2 - 1; itemx->flags & ITEM_DELETED; --itemx); + } + } + + struct bookmarkfs_permd_data permd_data; + permd_data.op = op; + sprintf(permd_data.name1, "%d", item1->id); + sprintf(permd_data.name2, "%d", itemx->id); + if (0 != ioctl(dirfd, BOOKMARKFS_IOC_PERMD, &permd_data)) { + log_printf("ioctl(): %s", strerror(errno)); + return -1; + } + + struct check_item item_tmp = *item1; + *item1 = *item2; + *item2 = item_tmp; + item1->flags |= ITEM_DIRTY; + item2->flags |= ITEM_DIRTY; + return 0; +} + +static int +do_check_fs_dents ( + int dirfd, + int n +) { +#define ASSERT_EQ(val, expr) ASSERT_EXPR_INT(expr, r_, (val) == r_, goto end;) +#define ASSERT_NE(val, expr) ASSERT_EXPR_INT(expr, r_, (val) != r_, goto end;) + + struct check_item *items = calloc(n, sizeof(struct check_item)); + int *map = malloc(sizeof(int) * n); + if (items == NULL || map == NULL) { + return -1; + } + int status = -1; + + for (int i = 0; i < n; ++i) { + struct check_item *item = items + i; + map[i] = item->id = i; + ASSERT_EQ(0, dent_new(dirfd, item)); + } + + ASSERT_EQ(0, lseek(dirfd, 0, SEEK_SET)); + for (int i = 0; i < n / 4; ++i) { + uint64_t bits = prng_rand(); + struct check_item *i1 = &items[bits % n]; + struct check_item *i2 = &items[(bits >> 32) % n]; + + if (i1 == i2 || bits >> 63) { + ASSERT_EQ(0, dent_delete(dirfd, i1)); + ASSERT_EQ(0, dent_delete(dirfd, i2)); +#if !defined(__FreeBSD__) + } else { + if (i1->flags & ITEM_DELETED && i2->flags & ITEM_DELETED) { + continue; + } + ASSERT_EQ(0, dent_permute(dirfd, i1, i2)); + map[i1->id] = i1 - items; + map[i2->id] = i2 - items; +#endif + } + } + + ASSERT_EQ(0, dent_check(dirfd, items, map, n, 1)); + ASSERT_EQ(0, lseek(dirfd, 0, SEEK_SET)); + ASSERT_EQ(0, dent_check(dirfd, items, map, n, 0)); + status = 0; + + end: + free(items); + free(map); + return status; +} + +int +check_fs_dents ( + int argc, + char *argv[] +) { + uint64_t seed_buf[4], *seed = NULL; + int n = -1; + + OPT_START(argc, argv, "n:s:") + OPT_OPT('n') { + n = atoi(optarg); + break; + } + OPT_OPT('s') { + if (0 != prng_seed_from_hex(seed_buf, optarg)) { + return -1; + } + seed = seed_buf; + break; + } + OPT_END + + if (n <= 0) { + log_printf("bad size %d", n); + return -1; + } + if (argc < 1) { + log_puts("path not given"); + return -1; + } + char const *path = argv[0]; + + if (0 != prng_seed(seed)) { + return -1; + } + int dirfd = open(path, O_RDONLY | O_DIRECTORY); + if (dirfd < 0) { + log_printf("open: %s: %s", path, strerror(errno)); + return -1; + } + int status = do_check_fs_dents(dirfd, n); + close(dirfd); + return status; +} diff --git a/tests/check_util.h b/tests/check_util.h index c9dc35c..0b5a0fd 100644 --- a/tests/check_util.h +++ b/tests/check_util.h @@ -35,6 +35,12 @@ action_if_false \ } while (0) +int +check_fs_dents ( + int argc, + char *argv[] +); + int check_fs_regrw ( int argc, diff --git a/tests/fs_dents.at b/tests/fs_dents.at new file mode 100644 index 0000000..77a4fa6 --- /dev/null +++ b/tests/fs_dents.at @@ -0,0 +1,29 @@ +dnl +dnl Copyright (C) 2025 CismonX +dnl +dnl Copying and distribution of this file, with or without modification, +dnl are permitted in any medium without royalty, +dnl provided the copyright notice and this notice are preserved. +dnl This file is offered as-is, without any warranty. +dnl + +AT_SETUP([fs: directory entries]) +AT_KEYWORDS([fs dents]) + +ATX_CHECK_FS_NEW_ANY(, [ + # requires PRNG + ATX_FEAT_PREREQ([bookmarkfs-util]) +], [ + ATX_RUN_REPEAT([8], [ + name=$(ath_fn_rand_u64_hex) + seed=$(ath_fn_prng_seed) + echo "prng seed: $seed" + + mkdir $name + ATX_RUN([ + check-fs dents -n 1024 -s "$seed" $name + ]) + ]) +]) + +AT_CLEANUP diff --git a/tests/testsuite.at b/tests/testsuite.at index 5feb02f..87af7d6 100644 --- a/tests/testsuite.at +++ b/tests/testsuite.at @@ -207,3 +207,4 @@ m4_include([lib_hashmap.at]) AT_BANNER([The Filesystem]) m4_include([fs_basic.at]) m4_include([fs_regrw.at]) +m4_include([fs_dents.at])