Compare commits

...

14 commits

Author SHA1 Message Date
CismonX
29338ca02b
backend_firefox: purge dangling refs on delete
When a bookmark is deleted, if there are no other bookmarks
referencing the corresponding `moz_place` entry, tag and keyword
references to that entry are considered "dangling" references,
and shall be automatically removed.

Also reverts commit b5fa6960ef,
since the NULL title check is no longer necessary.
2025-06-06 22:46:44 +08:00
CismonX
eb426f9fc4
bookmarkctl: xattr-get: rename -m option to -a 2025-06-06 19:41:09 +08:00
CismonX
b5fa6960ef
backend_firefox: hide dangling keywords
When a bookmark associated with a keyword is deleted,
there may still be dangling references (e.g., tags) to the
corresponding `moz_places` entry.

By filtering out NULL titles, `bookmark_lookup()` and
`bookmark_list()` now only give keywords associated with
valid bookmarks.
2025-06-05 08:27:59 +08:00
CismonX
6fb9438d3c
fs_ops: fix opendir for keyword directory 2025-06-05 07:39:53 +08:00
CismonX
c1cf9db2a1
test: add tests for tags and keywords 2025-06-02 09:20:19 +08:00
CismonX
495f8592e6
fs_ops: fix deletion of tag directories 2025-06-02 08:41:33 +08:00
CismonX
d5eae85774
test: refactor filesystem tests 2025-06-01 21:35:07 +08:00
CismonX
ab88e0e839
test: add tests for directory entries 2025-05-28 18:51:02 +08:00
CismonX
e6809b7e84
xstd: add xgetdents() 2025-05-28 18:27:55 +08:00
CismonX
5a28b069c4
fs_ops: fix file size opened with O_CREAT|O_TRUNC
Also make sure that new regular files always have a size of zero.
2025-04-29 14:48:39 +08:00
CismonX
83f201435f
backend_firefox: add keyword xattr
This allows users to quickly discover which keyword is
associated with a given bookmark.

Updating keywords via xattr is not implemented,
since it can be done trivially using existing API.
2025-04-29 12:46:29 +08:00
CismonX
0e20604c73
backend_firefox: fix stmt id for keyword delete 2025-04-28 20:04:40 +08:00
CismonX
9c0d5fb337
test: improve filesystem tests
- Add a final check to see whether the fs daemon is still there.
- Other misc updates.
2025-04-07 19:32:44 +08:00
CismonX
565063ee9b
chore: bump version to 0.1.1 2025-04-07 12:23:50 +08:00
19 changed files with 605 additions and 106 deletions

View file

@ -8,7 +8,7 @@ dnl This file is offered as-is, without any warranty.
dnl
AC_PREREQ([2.70])
AC_INIT([bookmarkfs], [0.1.0], [bug-bookmarkfs@nongnu.org])
AC_INIT([bookmarkfs], [0.1.1], [bug-bookmarkfs@nongnu.org])
AC_CONFIG_SRCDIR([bookmarkfs_util.pc.in])
AC_CONFIG_HEADERS([config.h])
AC_CONFIG_MACRO_DIR([m4])

View file

@ -32,7 +32,7 @@ bookmarkctl - manage a mounted BookmarkFS filesystem
.B bookmarkctl
.B xattr\-get
.RI [ options ]
.B \-m
.B \-a
.IR attrname "... " pathname
.PP
.B bookmarkctl
@ -145,7 +145,7 @@ Treat the value as binary, and print it verbatim.
.IP
If this option is not provided, non-printable characters are replaced with '?'.
.TP
.B \-m
.B \-a
Switch to multi-attrname mode, where multiple extended attribute names
can be specified instead of multiple files.
.IP

View file

@ -630,7 +630,7 @@ Displays extended attribute values.
@example
bookmarkctl xattr-get [@var{options}] @var{attrname} @var{pathname}...
bookmarkctl xattr-get [@var{options}] -m @var{attrname}... @var{pathname}
bookmarkctl xattr-get [@var{options}] -a @var{attrname}... @var{pathname}
@end example
@table @var
@ -650,7 +650,7 @@ Treat the value as binary, and print it verbatim.
If this option is not provided, non-printable characters are replaced with
@samp{?}.
@item -m
@item -a
Switch to multi-attrname mode, where multiple extended attribute names
can be specified instead of multiple files.
@ -1358,6 +1358,12 @@ The bookmark creation time.
Value is a decimal integer representing number of microseconds since
the Unix epoch.
@item keyword
The keyword associated with the bookmark.
@xref{Keywords}.
This attribute is read-only.
@end table
Notable limitations:

View file

@ -59,6 +59,7 @@
#define BM_XATTR_TITLE 2
#define BM_XATTR_GUID 3
#define BM_XATTR_DATE_ADDED 4
#define BM_XATTR_KEYWORD 5
#define MOZBM_XATTR_START BM_XATTR_TITLE
#define BACKEND_EXCLUSIVE_LOCK ( 1u << 16 )
@ -132,11 +133,14 @@ enum {
STMT_MOZBM_MTIME_UPDATE,
STMT_MOZBM_POS_SHIFT,
STMT_MOZBM_POS_UPDATE,
STMT_MOZBM_PURGE,
STMT_MOZBM_PURGE_CHECK,
STMT_MOZBM_UPDATE,
STMT_MOZBMDEL_INSERT,
STMT_MOZKW_DELETE,
STMT_MOZKW_INSERT,
STMT_MOZKW_LOOKUP,
STMT_MOZKW_PURGE,
STMT_MOZKW_RENAME,
STMT_TAG_ENTRY_LOOKUP,
#endif /* defined(BOOKMARKFS_BACKEND_FIREFOX_WRITE) */
@ -287,7 +291,7 @@ static bool is_valid_guid (char const *, size_t);
static int keyword_create (struct backend_ctx *, char const *, size_t,
struct bookmarkfs_bookmark_stat *);
static int mozbm_check_cb (void *, sqlite3_stmt *);
static int mozbm_delete (struct backend_ctx *, int64_t, bool);
static int mozbm_delete (struct backend_ctx *, int64_t, bool, bool);
static int mozbm_delete_cb (void *, sqlite3_stmt *);
static int mozbm_get_title (struct backend_ctx *, int64_t, int64_t,
db_query_row_func *, void *);
@ -301,11 +305,14 @@ static int mozbm_mtime_update (struct backend_ctx *, int64_t, int64_t *);
static int mozbm_pos_shift (struct backend_ctx *, int64_t, int64_t,
int64_t *, enum bookmarkfs_permd_op);
static int mozbm_pos_update (struct backend_ctx *, int64_t, int64_t);
static int mozbm_purge (struct backend_ctx *, int64_t);
static int mozbm_purge_check (struct backend_ctx *, int64_t);
static int mozbm_update (struct backend_ctx *, struct mozbm *);
static int mozbmdel_insert (struct backend_ctx *, char const *);
static int mozkw_delete (struct backend_ctx *, char const *, size_t);
static int mozkw_insert (struct backend_ctx *, struct mozkw *);
static int mozkw_lookup (struct backend_ctx *, struct mozkw *);
static int mozkw_purge (struct backend_ctx *, int64_t);
static int mozkw_rename (struct backend_ctx *, char const *,
char const *, uint32_t);
static int mozorigin_delete (struct backend_ctx *, int64_t);
@ -317,8 +324,9 @@ static int mozplace_addref (struct backend_ctx *, char const *, size_t,
static int mozplace_addref_cb (void *, sqlite3_stmt *);
static int mozplace_addref_id (struct backend_ctx *, int64_t);
static int mozplace_delete (struct backend_ctx *, int64_t, int64_t);
static int mozplace_delref (struct backend_ctx *, int64_t);
static int mozplace_delref (struct backend_ctx *, int64_t, int);
static int mozplace_insert (struct backend_ctx *, struct mozplace *);
static int mozplace_purge (struct backend_ctx *, int64_t);
static int mozplace_update (struct backend_ctx *, struct mozplace *);
static int64_t mozplace_url_hash (char const *, size_t);
static int parse_mkfsopts (struct bookmarkfs_conf_opt const *,
@ -397,9 +405,8 @@ bookmark_do_create (
stat_buf->value_len = -1;
int64_t place_id = 0;
if (!is_dir) {
char const *url = "about:blank";
stat_buf->value_len = strlen(url);
status = mozplace_addref(ctx, url, stat_buf->value_len, &place_id,
stat_buf->value_len = 0;
status = mozplace_addref(ctx, STR_WITHLEN("about:blank"), &place_id,
&stat_buf->atime);
if (status < 0) {
return status;
@ -457,7 +464,7 @@ bookmark_do_delete (
return is_dir ? -ENOTDIR : -EISDIR;
}
status = mozbm_delete(ctx, cols.id, is_dir);
status = mozbm_delete(ctx, cols.id, is_dir, true);
if (status < 0) {
return status;
}
@ -675,7 +682,8 @@ static int
mozbm_delete (
struct backend_ctx *ctx,
int64_t id,
bool is_dir
bool is_dir,
bool purge
) {
#define MOZBM_DELETE_(cond) \
"DELETE FROM `moz_bookmarks` WHERE `id` = ? " cond \
@ -707,7 +715,7 @@ mozbm_delete (
// The ID is alwayed obtained from a previous query in
// the same transaction. This shall not happen.
xassert(nrows > 0);
int status = mozplace_delref(ctx, qctx.place_id);
int status = mozplace_delref(ctx, qctx.place_id, purge ? 0 : 1);
if (status < 0) {
return status;
}
@ -1020,6 +1028,45 @@ mozbm_pos_update (
return 1;
}
static int
mozbm_purge (
struct backend_ctx *ctx,
int64_t place_id
) {
sqlite3_stmt **stmt_ptr = &ctx->stmts[STMT_MOZBM_PURGE];
char const *sql = "DELETE FROM `moz_bookmarks` WHERE `fk` = ?";
int status;
DO_QUERY(ctx, stmt_ptr, sql, NULL, NULL, status, , ,
DB_QUERY_BIND_INT64(place_id),
);
if (status < 0) {
return status;
}
return sqlite3_changes(ctx->db);
}
static int
mozbm_purge_check (
struct backend_ctx *ctx,
int64_t place_id
) {
sqlite3_stmt **stmt_ptr = &ctx->stmts[STMT_MOZBM_PURGE_CHECK];
char const *sql = "SELECT COUNT(*) FROM `moz_bookmarks` "
"WHERE `fk` = ? AND `title` IS NOT NULL";
int64_t result;
ssize_t nrows;
DO_QUERY(ctx, stmt_ptr, sql, db_query_i64_cb, &result, nrows, , ,
DB_QUERY_BIND_INT64(place_id),
);
if (nrows < 0) {
return nrows;
}
debug_assert(nrows == 1);
return result == 0;
}
static int
mozbm_update (
struct backend_ctx *ctx,
@ -1086,7 +1133,7 @@ mozkw_delete (
char const *name,
size_t name_len
) {
sqlite3_stmt **stmt_ptr = &ctx->stmts[STMT_MOZKW_RENAME];
sqlite3_stmt **stmt_ptr = &ctx->stmts[STMT_MOZKW_DELETE];
char const *sql = "DELETE FROM `moz_keywords` "
"WHERE `keyword` = ? RETURNING `place_id`";
@ -1098,7 +1145,7 @@ mozkw_delete (
if (status < 0) {
return status;
}
status = mozplace_delref(ctx, place_id);
status = mozplace_delref(ctx, place_id, 1);
if (status < 0) {
return status;
}
@ -1153,6 +1200,24 @@ mozkw_lookup (
return 0;
}
static int
mozkw_purge (
struct backend_ctx *ctx,
int64_t place_id
) {
sqlite3_stmt **stmt_ptr = &ctx->stmts[STMT_MOZKW_PURGE];
char const *sql = "DELETE FROM `moz_keywords` WHERE `place_id` = ?";
int status;
DO_QUERY(ctx, stmt_ptr, sql, NULL, NULL, status, , ,
DB_QUERY_BIND_INT64(place_id),
);
if (status < 0) {
return status;
}
return sqlite3_changes(ctx->db);
}
static int
mozkw_rename (
struct backend_ctx *ctx,
@ -1173,7 +1238,7 @@ mozkw_rename (
if (flags & BOOKMARKFS_BOOKMARK_RENAME_NOREPLACE) {
return -EEXIST;
}
status = mozplace_delref(ctx, old_cols.place_id);
status = mozplace_delref(ctx, old_cols.place_id, 1);
if (status < 0) {
return status;
}
@ -1412,16 +1477,18 @@ mozplace_delete (
static int
mozplace_delref (
struct backend_ctx *ctx,
int64_t id
int64_t id,
int purge
) {
sqlite3_stmt **stmt_ptr = &ctx->stmts[STMT_MOZPLACE_DELREF];
char const *sql =
"UPDATE `moz_places` SET `foreign_count` = `foreign_count` - 1 "
"UPDATE `moz_places` SET `foreign_count` = `foreign_count` - ? "
"WHERE `id` = ? RETURNING `foreign_count`, `origin_id`";
ssize_t nrows;
int64_t result[2]; // `foreign_count`, `origin_id`
DO_QUERY(ctx, stmt_ptr, sql, db_query_i64_cb, result, nrows, , ,
DB_QUERY_BIND_INT64(purge == 0 ? 1 : purge),
DB_QUERY_BIND_INT64(id),
);
if (nrows < 0) {
@ -1430,11 +1497,14 @@ mozplace_delref (
if (unlikely(nrows == 0)) {
return -EIO;
}
if (result[0] > 0) {
if (result[0] == 0) {
// `foreign_count` reaches 0, delete row.
return mozplace_delete(ctx, id, result[1]);
}
if (purge > 0) {
return 0;
}
// `foreign_count` reaches 0, delete row.
return mozplace_delete(ctx, id, result[1]);
return mozplace_purge(ctx, id);
}
static int
@ -1469,6 +1539,33 @@ mozplace_insert (
return 0;
}
static int
mozplace_purge (
struct backend_ctx *ctx,
int64_t id
) {
int status = mozbm_purge_check(ctx, id);
if (status <= 0) {
return status;
}
status = mozbm_purge(ctx, id);
if (status < 0) {
return status;
}
int changes = status;
status = mozkw_purge(ctx, id);
if (status < 0) {
return status;
}
changes += status;
if (changes == 0) {
return 0;
}
return mozplace_delref(ctx, id, changes);
}
static int
mozplace_update (
struct backend_ctx *ctx,
@ -1482,7 +1579,7 @@ mozplace_update (
if (status < 0) {
return status;
}
status = mozplace_delref(ctx, cols->id);
status = mozplace_delref(ctx, cols->id, 1);
if (status < 0) {
return status;
}
@ -2000,7 +2097,7 @@ tag_entry_delete (
return status;
}
status = mozbm_delete(ctx, cols.id, false);
status = mozbm_delete(ctx, cols.id, false, false);
if (status < 0) {
return status;
}
@ -2092,7 +2189,10 @@ bookmark_do_get (
#define BOOKMARK_GET_(cols, join) "SELECT CASE ? " cols "END " \
"FROM `moz_bookmarks` `b` " join "WHERE `b`.`id` = ?"
#define BOOKMARK_GET(cols) BOOKMARK_GET_(cols \
"WHEN " STRINGIFY(BM_XATTR_DATE_ADDED) " THEN `b`.`dateAdded` ", )
"WHEN " STRINGIFY(BM_XATTR_DATE_ADDED) " THEN `b`.`dateAdded` " \
"WHEN " STRINGIFY(BM_XATTR_KEYWORD) " THEN " \
"(SELECT `keyword` FROM `moz_keywords` `k` " \
"WHERE `k`.`place_id` = `b`.`fk`) ", )
#define BOOKMARK_GET_EX BOOKMARK_GET_( \
"WHEN " STRINGIFY(BM_XATTR_NULL) " THEN `p`.`url` " \
"WHEN " STRINGIFY(BM_XATTR_DESC) " THEN `p`.`description` " \
@ -2637,6 +2737,9 @@ get_xattr_id (
return BM_XATTR_GUID;
}
}
if (0 == strcmp("keyword", name)) {
return BM_XATTR_KEYWORD;
}
return -1;
}
@ -2942,9 +3045,9 @@ backend_create (
resp_flags |= BOOKMARKFS_BACKEND_EXCLUSIVE;
}
char const *xattr_names = "guid\0date_added\0description\0";
char const *xattr_names = "guid\0date_added\0description\0keyword\0";
if (opts.flags & BACKEND_FILENAME_GUID) {
xattr_names = "title\0date_added\0description\0";
xattr_names = "title\0date_added\0description\0keyword\0";
}
resp->name = "firefox";
@ -3611,7 +3714,7 @@ bookmark_rename (
status = (old_cols.place_id == 0) ? -ENOTDIR : -EISDIR;
goto fail;
}
status = mozbm_delete(ctx, new_cols.id, old_cols.place_id == 0);
status = mozbm_delete(ctx, new_cols.id, old_cols.place_id == 0, true);
if (status < 0) {
goto fail;
}
@ -3724,6 +3827,9 @@ bookmark_set (
}
break;
case BM_XATTR_KEYWORD:
return -EPERM;
default:
return -ENOATTR;
}

View file

@ -48,7 +48,7 @@ struct xattr_get_ctx {
char eol;
unsigned binary : 1;
unsigned multi_name : 1;
unsigned multi_attr : 1;
};
// Forward declaration start
@ -249,8 +249,8 @@ subcmd_xattr_get (
ctx.binary = 1;
break;
}
OPT_OPT('m') {
ctx.multi_name = 1;
OPT_OPT('a') {
ctx.multi_attr = 1;
break;
}
OPT_OPT('q') {
@ -269,7 +269,7 @@ subcmd_xattr_get (
return -1;
}
if (ctx.multi_name) {
if (ctx.multi_attr) {
return xattr_get_one(argv[argc - 1], argv, argc - 1, &ctx);
}
for (int i = 1; i < argc; ++i) {
@ -416,7 +416,7 @@ xattr_get_one (
for (int i = 0; i < names_cnt; ++i) {
char const *name = names[i];
ctx->prefix = ctx->multi_name ? name : path;
ctx->prefix = ctx->multi_attr ? name : path;
status = bookmarkfs_xattr_get(fd, name, xattr_get_cb, ctx);
if (status < 0) {
goto end;

View file

@ -409,6 +409,12 @@ bm_create (
}
}
if (flags & O_TRUNC) {
// See bm_open() for comments on O_RDONLY|O_TRUNC.
if ((flags & O_ACCMODE) != O_RDONLY) {
stat.value_len = 0;
}
}
bm_fillstat(&stat, SUBSYS_TYPE_BOOKMARK, false, stat_buf);
return 0;
}
@ -1346,12 +1352,14 @@ intfs_delete (
) {
int status = -EPERM;
int64_t bm_parent_id = BOOKMARKS_ROOT_ID;
switch (parent_id) {
case INTFS_ID_TAGS:
flags |= BOOKMARKFS_BOOKMARK_TYPE(TAG);
bm_parent_id = TAGS_ROOT_ID;
// fallthrough
case INTFS_ID_BOOKMARKS:
status = bm_delete(BOOKMARKS_ROOT_ID, name, flags);
status = bm_delete(bm_parent_id, name, flags);
break;
case INTFS_ID_KEYWORDS:
@ -1524,25 +1532,25 @@ intfs_opendir (
fuse_ino_t ino,
struct fuse_file_info *fi
) {
int status = 0;
uint64_t bm_id = BOOKMARKS_ROOT_ID;
uint32_t flags = 0;
switch (id) {
case INTFS_ID_ROOT:
fi->cache_readdir = 1;
fi->keep_cache = 1;
break;
case INTFS_ID_BOOKMARKS:
status = bm_opendir(BOOKMARKS_ROOT_ID, ino, flags, fi);
break;
return 0;
case INTFS_ID_TAGS:
flags |= BOOKMARKFS_BOOKMARK_TYPE(TAG);
status = bm_opendir(TAGS_ROOT_ID, ino, flags, fi);
bm_id = TAGS_ROOT_ID;
break;
case INTFS_ID_KEYWORDS:
flags |= BOOKMARKFS_BOOKMARK_TYPE(KEYWORD);
bm_id = 0;
break;
}
return status;
return bm_opendir(bm_id, ino, flags, fi);
}
static int

View file

@ -28,10 +28,8 @@
#include <stdio.h>
#include <stdlib.h>
#include <dirent.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <unistd.h>
#include "backend.h"
@ -67,10 +65,6 @@ struct fsck_dir {
#define FSCK_DIR_DONE ( 1u << 0 )
// Forward declaration start
#ifdef __linux__
static ssize_t getdents_ (int, void *, size_t);
#endif
static int next_subdir (struct fsck_ctx *, struct fsck_dir *,
struct dirent const **);
static int open_subdir (int, char const *, uint64_t *);
@ -79,22 +73,6 @@ static void print_help (void);
static void print_version (void);
// Forward declaration end
#ifdef __linux__
// Some libc (e.g., musl) may declare a getdents() function
// in dirent.h with conflicting types.
#define getdents getdents_
static ssize_t
getdents_ (
int dirfd,
void *buf,
size_t bufsize
) {
return syscall(SYS_getdents64, dirfd, buf, bufsize);
}
#endif /* defined(__linux__) */
static int
next_subdir (
struct fsck_ctx *ctx,
@ -112,7 +90,7 @@ next_subdir (
ctx->dent_buf = xrealloc(ctx->dent_buf, ctx->dent_buf_size);
}
ssize_t nbytes
= getdents(dir->fd, ctx->dent_buf + start, DIRENT_BUFSIZE);
= xgetdents(dir->fd, ctx->dent_buf + start, DIRENT_BUFSIZE);
if (nbytes < 0) {
log_printf("getdents(): %s", xstrerror(errno));
return -1;

View file

@ -27,7 +27,7 @@
#define BOOKMARKFS_VER_MAJOR 0
#define BOOKMARKFS_VER_MINOR 1
#define BOOKMARKFS_VER_PATCH 0
#define BOOKMARKFS_VER_PATCH 1
#define bookmarkfs_make_vernum(major, minor, patch) \
( ((major) << 16) | ((minor) << 8) | ((patch) << 0) )

View file

@ -26,6 +26,9 @@
#include <stdio.h>
#include <time.h>
#include <dirent.h>
#include <sys/syscall.h>
#include "defs.h"
#ifdef HAVE___BUILTIN_EXPECT
@ -84,6 +87,12 @@
} while (0)
#endif
#if defined(__linux__)
# define xgetdents(fd, buf, bufsz) syscall(SYS_getdents64, fd, buf, bufsz)
#elif defined(__FreeBSD__)
# define xgetdents(fd, buf, bufsz) getdents(fd, buf, bufsz)
#endif
/**
* Prints a message to standard error, and then aborts.
*/

View file

@ -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 fs_assoc.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

View file

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

282
tests/check_fs_dents.c Normal file
View file

@ -0,0 +1,282 @@
/**
* bookmarkfs/tests/check_fs_dents.c
* ----
*
* Copyright (C) 2025 CismonX <admin@cismon.net>
*
* 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 <https://www.gnu.org/licenses/>.
*/
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#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;
}

View file

@ -122,7 +122,7 @@ do_check_fs_regrw (
#ifndef O_DIRECT
# define O_DIRECT 0
#endif
int fd = open(path, O_RDWR | O_TRUNC | O_DIRECT);
int fd = open(path, O_RDWR | O_CREAT | O_TRUNC | O_DIRECT);
ASSERT_NE(-1, fd);
struct stat stat_buf;

View file

@ -35,6 +35,12 @@
action_if_false \
} while (0)
int
check_fs_dents (
int argc,
char *argv[]
);
int
check_fs_regrw (
int argc,

65
tests/fs_assoc.at Normal file
View file

@ -0,0 +1,65 @@
dnl
dnl Copyright (C) 2025 CismonX <admin@cismon.net>
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: tags and keywords])
AT_KEYWORDS([fs assoc tag keyword])
ATX_CHECK_FS_NEW_ASSOC([eol], , [
ATX_RUN_REPEAT([8], [
name=$(ath_fn_rand_u64_hex)
tag=$(ath_fn_rand_u64_hex)
keyword=$(ath_fn_rand_u64_hex)
content=foo:$(ath_fn_rand_u64_hex)
ATX_RUN([
echo "$content/1" > $name-1
echo "$content/2" > $name-2
echo "$content/3" > $name-3
mkdir "$atx_tags/$tag-1" "$atx_tags/$tag-2" "$atx_tags/$tag-3"
ln $name-1 $name-2 "$atx_tags/$tag-1"
ln $name-2 $name-3 "$atx_tags/$tag-2"
ln $name-3 $name-1 "$atx_tags/$tag-3"
test $name-1 -ef "$atx_tags/$tag-1/$name-1"
test $name-1 -ef "$atx_tags/$tag-3/$name-1"
test $name-2 -ef "$atx_tags/$tag-1/$name-2"
test $name-2 -ef "$atx_tags/$tag-2/$name-2"
test $name-3 -ef "$atx_tags/$tag-3/$name-3"
test $name-3 -ef "$atx_tags/$tag-2/$name-3"
ln $name-1 "$atx_keywords/$keyword-2"
ln $name-2 "$atx_keywords/$keyword-3"
ln $name-3 "$atx_keywords/$keyword-1"
test $name-1 -ef "$atx_keywords/$keyword-2"
test $name-2 -ef "$atx_keywords/$keyword-3"
test $name-3 -ef "$atx_keywords/$keyword-1"
rm "$atx_tags/$tag-1/$name-1"
rm "$atx_tags/$tag-2/$name-2"
rm "$atx_tags/$tag-3/$name-3"
test ! -e "$atx_tags/$tag-1/$name-1"
test ! -e "$atx_tags/$tag-2/$name-2"
test ! -e "$atx_tags/$tag-3/$name-3"
rm "$atx_keywords/$keyword-1"
rm "$atx_keywords/$keyword-2"
test ! -e "$atx_keywords/$keyword-1"
test ! -e "$atx_keywords/$keyword-2"
rm $name-1 $name-2 $name-3
test ! -e "$atx_tags/$tag-1/$name-2"
test ! -e "$atx_tags/$tag-2/$name-3"
test ! -e "$atx_tags/$tag-3/$name-1"
test ! -e "$atx_keywords/$keyword-3"
rmdir "$atx_tags/$tag-1" "$atx_tags/$tag-2" "$atx_tags/$tag-3"
])
])
])
AT_CLEANUP

View file

@ -15,53 +15,48 @@ AT_KEYWORDS([fs basic])
ATX_CHECK_FS_NEW_ANY([eol], , [
ATX_RUN_REPEAT([8], [
name=$(ath_fn_rand_u64_hex)
name_1=${name}_1
name_2=${name}_2
content=foo:$(ath_fn_rand_u64_hex)
content_1=${content}/1
content_2=${content}/2
ATX_RUN([
echo "$content_1" > $name_1
test "$(cat $name_1)" = "$content_1"
echo "$content_2" > $name_2
test "$(cat $name_2)" = "$content_2"
echo "$content/1" > $name-1
test "$(cat $name-1)" = "$content/1"
echo "$content/2" > $name-2
test "$(cat $name-2)" = "$content/2"
mv $name_1 $name_2
test ! -e $name_1
test "$(cat $name_2)" = "$content_1"
mv $name-1 $name-2
test ! -e $name-1
test "$(cat $name-2)" = "$content/1"
mv $name_2 $name_1
test ! -e $name_2
test "$(cat $name_1)" = "$content_1"
mv $name-2 $name-1
test ! -e $name-2
test "$(cat $name-1)" = "$content/1"
mkdir $name_2
mv $name_1 $name_2/$name_2
test ! -e $name_1
test "$(cat $name_2/$name_2)" = "$content_1"
mkdir $name-2
mv $name-1 $name-2/$name-2
test ! -e $name-1
test "$(cat $name-2/$name-2)" = "$content/1"
! mkdir $name_2/$name_2
mkdir $name_2/$name_1
mv $name_2/$name_2 $name_2/$name_1/$name_1
test "$(cat $name_2/$name_1/$name_1)" = "$content_1"
! mkdir $name-2/$name-2
mkdir $name-2/$name-1
mv $name-2/$name-2 $name-2/$name-1/$name-1
test "$(cat $name-2/$name-1/$name-1)" = "$content/1"
mkdir $name_1
! mv $name_1 $name_2/$name_1/$name_1
! mv $name_1 $name_2
mkdir $name-1
! mv $name-1 $name-2/$name-1/$name-1
! mv $name-1 $name-2
! mv $name_2/$name_1/$name_1 $name_2
rm $name_2/$name_1/$name_1
test ! -e $name_2/$name_1/$name_1
! mv $name-2/$name-1/$name-1 $name-2
rm $name-2/$name-1/$name-1
test ! -e $name-2/$name-1/$name-1
mv $name_1 $name_2
test ! -e $name_1
test -d $name_2/$name_1
mv $name-1 $name-2
test ! -e $name-1
test -d $name-2/$name-1
! rmdir $name_2
rmdir $name_2/$name_1
rmdir $name_2
test ! -e $name_2
! rmdir $name-2
rmdir $name-2/$name-1
rmdir $name-2
test ! -e $name-2
])
])
])

29
tests/fs_dents.at Normal file
View file

@ -0,0 +1,29 @@
dnl
dnl Copyright (C) 2025 CismonX <admin@cismon.net>
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

View file

@ -19,7 +19,6 @@ ATX_CHECK_FS_NEW_ANY([file_max=524288], [
echo "prng seed: $seed"
ATX_RUN([
touch $name
check-fs regrw -n 524288 -s "$seed" $name
])
])

View file

@ -139,6 +139,7 @@ m4_define([ATX_CHECK_FS], [
fi
done
$6
ATX_RUN_ONE([check-fs ismount "$4"])
], [
umount "$4"
])
@ -173,6 +174,17 @@ m4_define([ATX_CHECK_FS_NEW_ANY], [
])
])
dnl
dnl ATX_CHECK_FS_NEW_ASSOC([options], [prepare], [check])
dnl
m4_define([ATX_CHECK_FS_NEW_ASSOC], [
ATX_CHECK_FS_NEW([firefox], [$1], [mnt.tmp], [$2], [
atx_tags=../../tags
atx_keywords=../../keywords
ATX_RUN_IN_DIR([mnt.tmp/bookmarks/unfiled], [$3])
])
])
dnl -- Helper functions --
AT_TEST_HELPER_FN([rand_u64_hex], , , [
@ -206,3 +218,5 @@ m4_include([lib_hashmap.at])
AT_BANNER([The Filesystem])
m4_include([fs_basic.at])
m4_include([fs_regrw.at])
m4_include([fs_dents.at])
m4_include([fs_assoc.at])