Compare commits

...

7 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
9 changed files with 242 additions and 71 deletions

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.

View file

@ -133,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) */
@ -288,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 *);
@ -302,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);
@ -318,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 *,
@ -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,
@ -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;
}
@ -3617,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;
}

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

@ -1352,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:
@ -1530,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

@ -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 fs_dents.at
lib_hashmap.at fs_basic.at fs_regrw.at fs_dents.at fs_assoc.at
# Helper programs for testing

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
])
])
])

View file

@ -174,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], , , [
@ -208,3 +219,4 @@ AT_BANNER([The Filesystem])
m4_include([fs_basic.at])
m4_include([fs_regrw.at])
m4_include([fs_dents.at])
m4_include([fs_assoc.at])