Compare commits

..

23 commits

Author SHA1 Message Date
CismonX
f1451d206e
chore: bump version to 0.1.2 2025-06-12 11:31:39 +08:00
CismonX
303c934894
test: misc refactor
- If the utility library is not built, link to the existing library
  when building helper programs for filesystem tests.
- Refactor PRNG seeding in tests.
2025-06-11 20:45:16 +08:00
CismonX
14b5a79147
build: fix builds with existing utility library
Regression in commit 81db7a786c.
2025-06-11 17:11:19 +08:00
CismonX
1e5149cdc0
doc: misc corrections and improvements 2025-06-10 21:17:37 +08:00
CismonX
6f2a00dd6f
fs_ops: refactor setattr
- Only perform one operation per call.
- Always fail with EPERM if given unsupported flags.
2025-06-09 19:51:25 +08:00
CismonX
c23623b5a3
test: add tests for file atime/mtime 2025-06-08 12:34:10 +08:00
CismonX
fdfd6fe069
backend_firefox: fix directory attr update 2025-06-08 07:24:39 +08:00
CismonX
5082e7c67c
fs_ops: fix mtime update of regular files
Explicit mtime update (e.g., via futimens(2)) should override
file handle mtime.
2025-06-07 19:55:27 +08:00
CismonX
72b9e200d9
fs_ops: misc refactor
- Use nodeid for fh_map keys as well as bm_*() and intfs_*() args.
- bm_readdir(), fs_op_rename(): check subsys type more gracefully.
- ...
2025-06-07 08:57:48 +08:00
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
27 changed files with 1183 additions and 481 deletions

View file

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

View file

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

View file

@ -38,7 +38,7 @@
@end macro @end macro
@macro tcldoc {name, path} @macro tcldoc {name, path}
@uref{https://www.tcl.tk/man/tcl8.6.13/\path\, \name\} @uref{https://www.tcl.tk/man/tcl8.6.16/\path\, \name\}
@end macro @end macro
@copying @copying
@ -106,22 +106,22 @@ for CI build logs.
Currently, BookmarkFS only runs on GNU/Linux and FreeBSD. Currently, BookmarkFS only runs on GNU/Linux and FreeBSD.
Although BookmarkFS sticks hard to POSIX and avoids using platform-specific Although BookmarkFS sticks hard to POSIX and avoids platform-specific
features, porting it to other operating systems is not trivial. features, porting it to other operating systems is not trivial.
The major pitfall is the @linuxdoc{FUSE, filesystems/fuse.html} dependency. The major pitfall is the @linuxdoc{FUSE, filesystems/fuse.html} dependency.
Generally speaking, FUSE is Linux-only. FUSE was originally Linux-only.
FreeBSD partially implements the FUSE protocol in its kernel, Recent versions of the FreeBSD kernel partially implements the FUSE protocol,
to the extent that BookmarkFS is mostly usable. however, that's not the case for other operating systems.
However, that's not the case for other operating systems.
For example, OpenBSD implements its own FUSE protocol, For example, OpenBSD implements its own FUSE protocol,
which is incompatible with the Linux one. which is incompatible with the Linux one.
OpenBSD does provide a libfuse-compatible library, however, OpenBSD does provide a libfuse-compatible library, however,
it only covers the high-level API, while BookmarkFS uses the it only covers the high-level API, while BookmarkFS uses the
@uref{https://libfuse.github.io/doxygen/fuse__lowlevel_8h.html, low-level API}. @uref{https://libfuse.github.io/doxygen/fuse__lowlevel_8h.html, low-level API}.
For a similar reason, @uref{https://github.com/winfsp/winfsp, WinFsp} For a similar reason, @uref{https://github.com/winfsp/winfsp, WinFsp}
won't work if you're trying to port BookmarkFS to Microsoft Windows. won't help much if you're trying to port BookmarkFS to Microsoft Windows.
Other notable portability issues: Other notable portability issues:
@ -129,13 +129,10 @@ Other notable portability issues:
@item Sandboxing @item Sandboxing
Not all operating system kernels provide sandboxing mechanisms similar to Not all operating system kernels provide sandboxing mechanisms similar to
Linux and FreeBSD. Linux and FreeBSD.
@xref{Sandboxing}.
If not supported, operations that require sandboxing should fail. If unsupported, operations that require sandboxing should fail,
Users should not be provided with a false sense of security. unless sandboxing is explicitly disabled by the caller.
If they wish, they could pass a @option{-o no_sandbox} option to
explicitly disable sandboxing.
Also @pxref{Sandboxing}.
@end table @end table
@ -255,32 +252,32 @@ access the files.
@item -o ctime @item -o ctime
Maintain last status change time instead of last modification time. Maintain last status change time instead of last modification time.
Usually, a bookmark's ``modification time'' attribute behaves differently From a browser's perspective, the ``modification time'' attribute
from both mtime and ctime. of a bookmark behaves differently from both mtime and ctime.
In Chromium, for instance, when a bookmark is renamed, neither itself In Chromium, for instance, when a bookmark is renamed, neither itself
nor the parent directory changes timestamp accordingly. nor the parent directory changes timestamp accordingly.
BookmarkFS do not follow browser behavior here, and instead try to stay In BookmarkFS, mtime behavior mostly stays compatible with POSIX,
compatible with POSIX. with a few caveats.
Since a bookmark has only one ``modification time'' attribute instead of two, Since a bookmark has only one ``modification time'' attribute instead of two,
the user has to choose which one to maintain: the user has to choose which one to maintain:
@table @asis @table @asis
@item Last modification time @item Last modification time (default)
ctime only updates when mtime does. mtime updates normally; ctime value always follow mtime.
The kernel may automatically update and cache ctime,
making it appear more ``correct'' than what we expect.
However, this behavior should not be relied upon.
@item Last status change time @item Last status change time
ctime updates normally; mtime always updates when ctime does, ctime updates normally; mtime value always follow ctime,
even if the file content is not modified. even if the file content is not modified.
This behavior may be inefficient, but makes applications that depend on ctime This behavior may be inefficient, but makes applications that depend on ctime
less fragile. less fragile.
@end table @end table
The kernel may automatically update and cache ctime,
making it more ``correct'' than what we expect.
However, this behavior should not be relied upon.
@item -o eol @item -o eol
Add a newline (ASCII LF character) to the end of each file. Add a newline (ASCII LF character) to the end of each file.
@ -302,8 +299,8 @@ Do not use Landlock for sandboxing.
This option is ignored on non-Linux platforms. This option is ignored on non-Linux platforms.
Without Landlock, sandboxing offers less security. Without Landlock, sandboxing offers less security.
However, Landlock is a rather new feature (requires kernel version 5.13 Nonetheless, we provide an option to disable it separately,
or later), thus we provide an option to disable it separately. since Landlock is a rather new feature (requires kernel version 5.13 or later).
@item -F @item -F
Stay in the foreground, do not daemonize. Stay in the foreground, do not daemonize.
@ -364,7 +361,7 @@ so that the user don't have to manually dismount the inactive filesystem.
See @linuxmanpage{mount.fuse3, 8} for details. See @linuxmanpage{mount.fuse3, 8} for details.
This option is helpful when sandboxing is enabled, especially when This option is helpful when sandboxing is enabled, especially when
the @option{-F} option is given, since a sandboxed process itself can neither given the @option{-F} option, since a sandboxed process itself can neither
@linuxmanpage{umount, 2} nor fork-exec. @linuxmanpage{umount, 2} nor fork-exec.
Currently, this option is not available on FreeBSD. Currently, this option is not available on FreeBSD.
@ -630,7 +627,7 @@ Displays extended attribute values.
@example @example
bookmarkctl xattr-get [@var{options}] @var{attrname} @var{pathname}... 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 @end example
@table @var @table @var
@ -650,7 +647,7 @@ Treat the value as binary, and print it verbatim.
If this option is not provided, non-printable characters are replaced with If this option is not provided, non-printable characters are replaced with
@samp{?}. @samp{?}.
@item -m @item -a
Switch to multi-attrname mode, where multiple extended attribute names Switch to multi-attrname mode, where multiple extended attribute names
can be specified instead of multiple files. can be specified instead of multiple files.
@ -822,8 +819,8 @@ However, consecutive lookups and @code{readdir()}s should produce consistent
results for that file, provided that it is not renamed or deleted. results for that file, provided that it is not renamed or deleted.
Tag files behave differently from traditional hard links. Tag files behave differently from traditional hard links.
If the original bookmark file is renamed or deleted, If the associated bookmark file of a tag is renamed or deleted,
it may also change accordingly. it may change accordingly.
It may even link to another file that was previously shadowed. It may even link to another file that was previously shadowed.
Applications should tread lightly if they wish to cache tag directory entries. Applications should tread lightly if they wish to cache tag directory entries.
@ -887,14 +884,13 @@ for web browser bookmarks.
An unexpected internal error occurred, likely due to a bug in BookmarkFS, An unexpected internal error occurred, likely due to a bug in BookmarkFS,
or a corruption in the bookmark storage. or a corruption in the bookmark storage.
When this error occurs, a log message describing the situation may be printed
to the standard error of the filesystem daemon.
Sometimes this error comes from the kernel-side FUSE implementation,
and there's no error message.
Once this error occurs, behavior of any further operations on the filesystem Once this error occurs, behavior of any further operations on the filesystem
is undefined. is undefined.
A log message describing the situation may be printed to the standard error
of the filesystem daemon.
Sometimes the error comes from the kernel, and there's no error message.
@item ESTALE @item ESTALE
The file associated with the file descriptor no longer exists. The file associated with the file descriptor no longer exists.
@ -1358,6 +1354,12 @@ The bookmark creation time.
Value is a decimal integer representing number of microseconds since Value is a decimal integer representing number of microseconds since
the Unix epoch. the Unix epoch.
@item keyword
The keyword associated with the bookmark.
@xref{Keywords}.
This attribute is read-only.
@end table @end table
Notable limitations: Notable limitations:
@ -1367,6 +1369,9 @@ Notable limitations:
of a bookmark may affect that of other bookmarks, of a bookmark may affect that of other bookmarks,
due to the way bookmarks are associated with the URLs. due to the way bookmarks are associated with the URLs.
@item For a similar reason, directory atime is not supported, since the atime
attribute only exists on URLs, and directories are not associated with URLs.
@item The bookmark storage (SQLite database file) should be writable even if @item The bookmark storage (SQLite database file) should be writable even if
the BookmarkFS filesystem is mounted read-only, due to the limitations the BookmarkFS filesystem is mounted read-only, due to the limitations
of WAL-mode. of WAL-mode.
@ -1492,7 +1497,7 @@ Notable limitations:
@itemize @bullet{} @itemize @bullet{}
@item Does not scale well with large bookmark storage, @item Does not scale well with large bookmark storage,
since everything is kept in memory, and the entire JSON file has to be parsed since everything is kept in memory, and the entire JSON file has to be parsed
whenever a reload is required. (with a parser known not to be fast) whenever a reload is required.
@item No support for tags (@pxref{Tags}) and keywords (@pxref{Keywords}), @item No support for tags (@pxref{Tags}) and keywords (@pxref{Keywords}),
since Chromium does not have such concepts. since Chromium does not have such concepts.
@ -1723,7 +1728,7 @@ as well as a list of backend-specific options.
Indicates that the function should print a version message. Indicates that the function should print a version message.
Mutually exclusive with @code{BOOKMARKFS_BACKEND_INFO_HELP}. Mutually exclusive with @code{BOOKMARKFS_BACKEND_INFO_HELP}.
In addition to the version number, the version message may also contain In addition to the version number, the version message may contain
dependency versions, compile-time options, and other information dependency versions, compile-time options, and other information
that can be used to determine a specific version of the backend. that can be used to determine a specific version of the backend.
@ -1964,7 +1969,7 @@ The pointer referring to the backend context.
@subsection Enter Sandbox @subsection Enter Sandbox
The @code{backend_sandbox} function is called to instruct the backend to The @code{backend_sandbox} function is called to instruct the backend to
enter a sandboxed state where it has limited access to system resources, enter a sandboxed state, where it has limited access to system resources,
thereby improving security. thereby improving security.
@xref{Sandboxing}. @xref{Sandboxing}.
@ -1973,10 +1978,10 @@ is @emph{not} set for this context.
Considering how sandboxing is usually implemented, Considering how sandboxing is usually implemented,
it may be complicated or even impractical for multiple backend contexts it may be complicated or even impractical for multiple backend contexts
to enter sandbox separately without affecting other ones. to enter sandbox separately without affecting one another.
Thus it is guaranteed that, when launched from a frontend program like Thus it is guaranteed that, when launched from a frontend program like
@command{mount.bookmarkfs}, only one backend context will be created @command{mount.bookmarkfs} which requires sandboxing, only one
throughout the lifetime of the process if sandboxing is ever needed. backend context will be created throughout the lifetime of the process.
Type of the @code{backend_sandbox} function is defined as: Type of the @code{backend_sandbox} function is defined as:
@ -4315,8 +4320,7 @@ it is incompatible with our sandboxing design.
Implemented with the @code{EVFILT_VNODE} filter of @freebsdmanpage{kevent, 2}. Implemented with the @code{EVFILT_VNODE} filter of @freebsdmanpage{kevent, 2}.
@item Fallback @item Fallback
When no platform-specific filesystem watching mechanism is available, Periodically checks the @code{st_ino} and @code{st_mtim} attributes
periodically checks the @code{st_ino} and @code{st_mtim} attributes
of the watched file with @posixfuncmanpage{fstatat}. of the watched file with @posixfuncmanpage{fstatat}.
This approach is less efficient than ``native'' implementations, This approach is less efficient than ``native'' implementations,

View file

@ -28,7 +28,7 @@ UTIL_LIBS_ =
if BOOKMARKFS_UTIL if BOOKMARKFS_UTIL
UTIL_LIBS_ += libbookmarkfs_util.la UTIL_LIBS_ += libbookmarkfs_util.la
else else
UTIL_LIBS_ += $(BOOKMARKFS_LIBS) UTIL_LIBS_ += $(BOOKMARKFS_UTIL_LIBS)
endif # BOOKMARKFS_UTIL endif # BOOKMARKFS_UTIL
BACKEND_SOURCES_ = backend_util.c lib.c xstd.c BACKEND_SOURCES_ = backend_util.c lib.c xstd.c

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

@ -28,10 +28,8 @@
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <dirent.h>
#include <fcntl.h> #include <fcntl.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <sys/syscall.h>
#include <unistd.h> #include <unistd.h>
#include "backend.h" #include "backend.h"
@ -67,10 +65,6 @@ struct fsck_dir {
#define FSCK_DIR_DONE ( 1u << 0 ) #define FSCK_DIR_DONE ( 1u << 0 )
// Forward declaration start // Forward declaration start
#ifdef __linux__
static ssize_t getdents_ (int, void *, size_t);
#endif
static int next_subdir (struct fsck_ctx *, struct fsck_dir *, static int next_subdir (struct fsck_ctx *, struct fsck_dir *,
struct dirent const **); struct dirent const **);
static int open_subdir (int, char const *, uint64_t *); static int open_subdir (int, char const *, uint64_t *);
@ -79,22 +73,6 @@ static void print_help (void);
static void print_version (void); static void print_version (void);
// Forward declaration end // 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 static int
next_subdir ( next_subdir (
struct fsck_ctx *ctx, struct fsck_ctx *ctx,
@ -112,7 +90,7 @@ next_subdir (
ctx->dent_buf = xrealloc(ctx->dent_buf, ctx->dent_buf_size); ctx->dent_buf = xrealloc(ctx->dent_buf, ctx->dent_buf_size);
} }
ssize_t nbytes ssize_t nbytes
= getdents(dir->fd, ctx->dent_buf + start, DIRENT_BUFSIZE); = xgetdents(dir->fd, ctx->dent_buf + start, DIRENT_BUFSIZE);
if (nbytes < 0) { if (nbytes < 0) {
log_printf("getdents(): %s", xstrerror(errno)); log_printf("getdents(): %s", xstrerror(errno));
return -1; return -1;

View file

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

View file

@ -26,6 +26,9 @@
#include <stdio.h> #include <stdio.h>
#include <time.h> #include <time.h>
#include <dirent.h>
#include <sys/syscall.h>
#include "defs.h" #include "defs.h"
#ifdef HAVE___BUILTIN_EXPECT #ifdef HAVE___BUILTIN_EXPECT
@ -84,6 +87,12 @@
} while (0) } while (0)
#endif #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. * Prints a message to standard error, and then aborts.
*/ */

View file

@ -9,7 +9,8 @@
EXTRA_DIST = package.m4 testsuite.at $(TESTSUITE) $(TESTS_) EXTRA_DIST = package.m4 testsuite.at $(TESTSUITE) $(TESTS_)
TESTS_ = lib_hash.at lib_prng.at lib_watcher.at lib_sandbox.at \ 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 \
fs_times.at
# Helper programs for testing # Helper programs for testing
@ -30,12 +31,13 @@ if BOOKMARKFS_MOUNT
check_fs_CPPFLAGS = -I$(top_srcdir)/src check_fs_CPPFLAGS = -I$(top_srcdir)/src
check_fs_LDADD = check_fs_LDADD =
check_fs_SOURCES = check_fs.c check_fs_SOURCES = check_fs.c check_fs_dents.c check_fs_regrw.c \
check_fs_times.c check_util.c
if BOOKMARKFS_UTIL if BOOKMARKFS_UTIL
check_fs_CPPFLAGS += -DHAVE_BOOKMARKFS_UTIL
check_fs_LDADD += $(top_builddir)/src/libbookmarkfs_util.la check_fs_LDADD += $(top_builddir)/src/libbookmarkfs_util.la
check_fs_SOURCES += check_fs_regrw.c check_util.c else
check_fs_LDADD += $(BOOKMARKFS_UTIL_LIBS)
endif # BOOKMARKFS_UTIL endif # BOOKMARKFS_UTIL
endif # BOOKMARKFS_MOUNT endif # BOOKMARKFS_MOUNT

View file

@ -54,10 +54,12 @@ dispatch_subcmds (
status = subcmd_ismount(argc, argv); status = subcmd_ismount(argc, argv);
} else if (0 == strcmp("sleep", cmd)) { } else if (0 == strcmp("sleep", cmd)) {
status = subcmd_sleep(argc, argv); status = subcmd_sleep(argc, argv);
#ifdef HAVE_BOOKMARKFS_UTIL
} else if (0 == strcmp("regrw", cmd)) { } else if (0 == strcmp("regrw", cmd)) {
status = check_fs_regrw(argc, argv); status = check_fs_regrw(argc, argv);
#endif } else if (0 == strcmp("dents", cmd)) {
status = check_fs_dents(argc, argv);
} else if (0 == strcmp("times", cmd)) {
status = check_fs_times(argc, argv);
} else { } else {
log_printf("bad subcmd '%s'", cmd); log_printf("bad subcmd '%s'", cmd);
} }

279
tests/check_fs_dents.c Normal file
View file

@ -0,0 +1,279 @@
/**
* 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[]
) {
char const *seed = NULL;
int n = -1;
OPT_START(argc, argv, "n:s:")
OPT_OPT('n') {
n = atoi(optarg);
break;
}
OPT_OPT('s') {
seed = optarg;
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_from_hex(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 #ifndef O_DIRECT
# define O_DIRECT 0 # define O_DIRECT 0
#endif #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); ASSERT_NE(-1, fd);
struct stat stat_buf; struct stat stat_buf;
@ -167,7 +167,7 @@ check_fs_regrw (
int argc, int argc,
char *argv[] char *argv[]
) { ) {
uint64_t seed_buf[4], *seed = NULL; char const *seed = NULL;
int file_max = -1; int file_max = -1;
OPT_START(argc, argv, "n:s:") OPT_START(argc, argv, "n:s:")
@ -176,10 +176,7 @@ check_fs_regrw (
break; break;
} }
OPT_OPT('s') { OPT_OPT('s') {
if (0 != prng_seed_from_hex(seed_buf, optarg)) { seed = optarg;
return -1;
}
seed = seed_buf;
break; break;
} }
OPT_END OPT_END
@ -194,7 +191,7 @@ check_fs_regrw (
} }
char const *path = argv[0]; char const *path = argv[0];
if (0 != prng_seed(seed)) { if (0 != prng_seed_from_hex(seed)) {
return -1; return -1;
} }
return do_check_fs_regrw(path, file_max); return do_check_fs_regrw(path, file_max);

191
tests/check_fs_times.c Normal file
View file

@ -0,0 +1,191 @@
/**
* bookmarkfs/tests/check_fs_times.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 <string.h>
#include <time.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include "check_util.h"
#include "frontend_util.h"
#include "prng.h"
// Forward declaration start
static int compare_timespec (struct timespec, struct timespec);
static int do_check_fs_times (int, int);
static void usecs_to_timespec (struct timespec *, uint32_t);
// Forward declaration end
static int
compare_timespec (
struct timespec ts1,
struct timespec ts2
) {
#define timespec_to_usecs(ts) ( (ts).tv_sec * 1000000 + (ts).tv_nsec / 1000 )
int64_t usecs1 = timespec_to_usecs(ts1);
int64_t usecs2 = timespec_to_usecs(ts2);
if (usecs1 == usecs2) {
return 0;
}
return usecs2 > usecs1 ? 1 : -1;
}
static int
do_check_fs_times (
int dirfd,
int rounds
) {
#define FILE1_NAME "foo.tmp"
#define FILE2_NAME "bar.tmp"
#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;)
int status = -1;
struct timespec now;
ASSERT_EQ(0, clock_gettime(CLOCK_REALTIME, &now));
int fd = openat(dirfd, FILE1_NAME, O_WRONLY | O_CREAT | O_EXCL);
ASSERT_NE(-1, fd);
struct stat stat_buf;
ASSERT_EQ(0, fstat(fd, &stat_buf));
ASSERT_NE(-1, compare_timespec(now, stat_buf.st_mtim));
ASSERT_EQ(0, fstat(dirfd, &stat_buf));
ASSERT_NE(-1, compare_timespec(now, stat_buf.st_mtim));
now = stat_buf.st_mtim;
ASSERT_EQ(11, write(fd, "foo:bar/baz", 11));
ASSERT_EQ(0, fstat(fd, &stat_buf));
ASSERT_NE(-1, compare_timespec(now, stat_buf.st_mtim));
ASSERT_EQ(0, renameat(dirfd, FILE1_NAME, dirfd, FILE2_NAME));
ASSERT_EQ(0, fstat(dirfd, &stat_buf));
ASSERT_NE(-1, compare_timespec(now, stat_buf.st_mtim));
now = stat_buf.st_mtim;
ASSERT_EQ(0, ftruncate(fd, 10));
ASSERT_EQ(0, fstat(fd, &stat_buf));
ASSERT_NE(-1, compare_timespec(now, stat_buf.st_mtim));
struct timespec times[2];
times[0].tv_nsec = UTIME_OMIT;
times[1].tv_nsec = UTIME_NOW;
ASSERT_EQ(0, futimens(dirfd, times));
ASSERT_EQ(0, fstat(dirfd, &stat_buf));
ASSERT_NE(-1, compare_timespec(now, stat_buf.st_mtim));
now = stat_buf.st_mtim;
times[0].tv_nsec = UTIME_NOW;
times[1].tv_nsec = UTIME_OMIT;
ASSERT_EQ(0, futimens(fd, times));
ASSERT_EQ(0, fstat(fd, &stat_buf));
ASSERT_NE(-1, compare_timespec(now, stat_buf.st_atim));
ASSERT_EQ(0, clock_gettime(CLOCK_REALTIME, &now));
ASSERT_NE(-1, compare_timespec(stat_buf.st_atim, now));
for (int i = 0; i < rounds; ++i) {
uint64_t bits = prng_rand();
usecs_to_timespec(&times[0], bits & 0xffffffff);
usecs_to_timespec(&times[1], bits >> 32);
ASSERT_EQ(0, futimens(fd, times));
ASSERT_EQ(0, fstat(fd, &stat_buf));
ASSERT_EQ(0, compare_timespec(times[0], stat_buf.st_atim));
ASSERT_EQ(0, compare_timespec(times[1], stat_buf.st_mtim));
}
ASSERT_EQ(0, unlinkat(dirfd, FILE2_NAME, 0));
ASSERT_EQ(0, fstat(dirfd, &stat_buf));
ASSERT_NE(-1, compare_timespec(now, stat_buf.st_mtim));
ASSERT_EQ(0, clock_gettime(CLOCK_REALTIME, &now));
ASSERT_NE(-1, compare_timespec(stat_buf.st_mtim, now));
status = 0;
end:
if (fd >= 0) {
close(fd);
}
return status;
}
static void
usecs_to_timespec (
struct timespec *ts_buf,
uint32_t usecs
) {
ts_buf->tv_sec = usecs / 1000000;
ts_buf->tv_nsec = (usecs % 1000000) * 1000;
}
int
check_fs_times (
int argc,
char *argv[]
) {
char const *seed = NULL;
int rounds = -1;
OPT_START(argc, argv, "r:s:")
OPT_OPT('r') {
rounds = atoi(optarg);
break;
}
OPT_OPT('s') {
seed = optarg;
break;
}
OPT_END
if (rounds < 0) {
log_printf("bad rounds cnt %d", rounds);
return -1;
}
if (argc < 1) {
log_puts("path not given");
return -1;
}
char const *path = argv[0];
if (0 != prng_seed_from_hex(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_times(dirfd, rounds);
close(dirfd);
return status;
}

View file

@ -168,16 +168,13 @@ check_hashmap (
int argc, int argc,
char *argv[] char *argv[]
) { ) {
uint64_t seed_buf[4], *seed = NULL; char const *seed = NULL;
int size_exp = -1; int size_exp = -1;
int rounds = -1; int rounds = -1;
OPT_START(argc, argv, "s:n:r:") OPT_START(argc, argv, "s:n:r:")
OPT_OPT('s') { OPT_OPT('s') {
if (0 != prng_seed_from_hex(seed_buf, optarg)) { seed = optarg;
return -1;
}
seed = seed_buf;
break; break;
} }
OPT_OPT('n') { OPT_OPT('n') {
@ -199,7 +196,7 @@ check_hashmap (
return -1; return -1;
} }
if (0 != prng_seed(seed)) { if (0 != prng_seed_from_hex(seed)) {
return -1; return -1;
} }
return do_check_hashmap(1u << size_exp, rounds); return do_check_hashmap(1u << size_exp, rounds);

View file

@ -104,15 +104,12 @@ subcmd_prng (
int argc, int argc,
char *argv[] char *argv[]
) { ) {
uint64_t seed_buf[4], *seed = NULL; char const *seed = NULL;
int n = 0; int n = 0;
OPT_START(argc, argv, "s:n:") OPT_START(argc, argv, "s:n:")
OPT_OPT('s') { OPT_OPT('s') {
if (0 != prng_seed_from_hex(seed_buf, optarg)) { seed = optarg;
return -1;
}
seed = seed_buf;
break; break;
} }
OPT_OPT('n') { OPT_OPT('n') {
@ -121,7 +118,7 @@ subcmd_prng (
} }
OPT_END OPT_END
if (0 != prng_seed(seed)) { if (0 != prng_seed_from_hex(seed)) {
return -1; return -1;
} }
for (; n > 0; --n) { for (; n > 0; --n) {

View file

@ -27,17 +27,24 @@
#include <inttypes.h> #include <inttypes.h>
#include <stdio.h> #include <stdio.h>
#include "prng.h"
int int
prng_seed_from_hex ( prng_seed_from_hex (
uint64_t *buf,
char const *str char const *str
) { ) {
uint64_t buf[4], *seed = NULL;
if (str != NULL) {
int cnt = sscanf(str, int cnt = sscanf(str,
"%16" SCNx64 "%16" SCNx64 "%16" SCNx64 "%16" SCNx64, "%16" SCNx64 "%16" SCNx64 "%16" SCNx64 "%16" SCNx64,
&buf[0], &buf[1], &buf[2], &buf[3]); &buf[0], &buf[1], &buf[2], &buf[3]);
if (cnt != 4) { if (cnt != 4) {
log_printf("bad seed '%s'", str); log_puts("bad prng seed");
return -1; return -1;
} }
return 0; seed = buf;
log_printf("prng seed: '%s'", str);
}
return prng_seed(seed);
} }

View file

@ -35,6 +35,18 @@
action_if_false \ action_if_false \
} while (0) } while (0)
int
check_fs_dents (
int argc,
char *argv[]
);
int
check_fs_times (
int argc,
char *argv[]
);
int int
check_fs_regrw ( check_fs_regrw (
int argc, int argc,
@ -61,7 +73,6 @@ check_watcher (
int int
prng_seed_from_hex ( prng_seed_from_hex (
uint64_t *buf,
char const *str char const *str
); );

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_CHECK_FS_NEW_ANY([eol], , [
ATX_RUN_REPEAT([8], [ ATX_RUN_REPEAT([8], [
name=$(ath_fn_rand_u64_hex) name=$(ath_fn_rand_u64_hex)
name_1=${name}_1
name_2=${name}_2
content=foo:$(ath_fn_rand_u64_hex) content=foo:$(ath_fn_rand_u64_hex)
content_1=${content}/1
content_2=${content}/2
ATX_RUN([ ATX_RUN([
echo "$content_1" > $name_1 echo "$content/1" > $name-1
test "$(cat $name_1)" = "$content_1" test "$(cat $name-1)" = "$content/1"
echo "$content_2" > $name_2 echo "$content/2" > $name-2
test "$(cat $name_2)" = "$content_2" test "$(cat $name-2)" = "$content/2"
mv $name_1 $name_2 mv $name-1 $name-2
test ! -e $name_1 test ! -e $name-1
test "$(cat $name_2)" = "$content_1" test "$(cat $name-2)" = "$content/1"
mv $name_2 $name_1 mv $name-2 $name-1
test ! -e $name_2 test ! -e $name-2
test "$(cat $name_1)" = "$content_1" test "$(cat $name-1)" = "$content/1"
mkdir $name_2 mkdir $name-2
mv $name_1 $name_2/$name_2 mv $name-1 $name-2/$name-2
test ! -e $name_1 test ! -e $name-1
test "$(cat $name_2/$name_2)" = "$content_1" test "$(cat $name-2/$name-2)" = "$content/1"
! mkdir $name_2/$name_2 ! mkdir $name-2/$name-2
mkdir $name_2/$name_1 mkdir $name-2/$name-1
mv $name_2/$name_2 $name_2/$name_1/$name_1 mv $name-2/$name-2 $name-2/$name-1/$name-1
test "$(cat $name_2/$name_1/$name_1)" = "$content_1" test "$(cat $name-2/$name-1/$name-1)" = "$content/1"
mkdir $name_1 mkdir $name-1
! mv $name_1 $name_2/$name_1/$name_1 ! mv $name-1 $name-2/$name-1/$name-1
! mv $name_1 $name_2 ! mv $name-1 $name-2
! mv $name_2/$name_1/$name_1 $name_2 ! mv $name-2/$name-1/$name-1 $name-2
rm $name_2/$name_1/$name_1 rm $name-2/$name-1/$name-1
test ! -e $name_2/$name_1/$name_1 test ! -e $name-2/$name-1/$name-1
mv $name_1 $name_2 mv $name-1 $name-2
test ! -e $name_1 test ! -e $name-1
test -d $name_2/$name_1 test -d $name-2/$name-1
! rmdir $name_2 ! rmdir $name-2
rmdir $name_2/$name_1 rmdir $name-2/$name-1
rmdir $name_2 rmdir $name-2
test ! -e $name_2 test ! -e $name-2
]) ])
]) ])
]) ])

25
tests/fs_dents.at Normal file
View file

@ -0,0 +1,25 @@
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(, , [
ATX_RUN_REPEAT([8], [
name=$(ath_fn_rand_u64_hex)
seed=$(ath_fn_prng_seed)
mkdir $name
ATX_RUN([
check-fs dents -n 1024 -s "$seed" $name
])
])
])
AT_CLEANUP

View file

@ -10,16 +10,11 @@ dnl
AT_SETUP([fs: regular file read/write]) AT_SETUP([fs: regular file read/write])
AT_KEYWORDS([fs regrw]) AT_KEYWORDS([fs regrw])
ATX_CHECK_FS_NEW_ANY([file_max=524288], [ ATX_CHECK_FS_NEW_ANY([file_max=524288], , [
# requires PRNG
ATX_FEAT_PREREQ([bookmarkfs-util])
], [
name=$(ath_fn_rand_u64_hex) name=$(ath_fn_rand_u64_hex)
seed=$(ath_fn_prng_seed) seed=$(ath_fn_prng_seed)
echo "prng seed: $seed"
ATX_RUN([ ATX_RUN([
touch $name
check-fs regrw -n 524288 -s "$seed" $name check-fs regrw -n 524288 -s "$seed" $name
]) ])
]) ])

23
tests/fs_times.at Normal file
View file

@ -0,0 +1,23 @@
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: last access/modification times])
AT_KEYWORDS([fs times])
ATX_CHECK_FS_NEW_ANY(, , [
name=$(ath_fn_rand_u64_hex)
seed=$(ath_fn_prng_seed)
mkdir $name
ATX_RUN([
check-fs times -s "$seed" -r 512 $name
])
])
AT_CLEANUP

View file

@ -15,7 +15,6 @@ ATX_CHECK_LIB([
if test -z "$seed"; then if test -z "$seed"; then
seed=$(ath_fn_prng_seed) seed=$(ath_fn_prng_seed)
fi fi
echo "prng seed: $seed"
size="${CHECK_HASHMAP_DATA_SIZE}" size="${CHECK_HASHMAP_DATA_SIZE}"
if test -z "$size"; then if test -z "$size"; then

View file

@ -21,7 +21,6 @@ ATX_CHECK_LIB([
ATX_RUN_REPEAT([16], [ ATX_RUN_REPEAT([16], [
seed=$(ath_fn_prng_seed) seed=$(ath_fn_prng_seed)
count=32 count=32
echo "prng seed: $seed"
num_1=$(gen_num $seed $count) num_1=$(gen_num $seed $count)
num_2=$(gen_num $seed $count) num_2=$(gen_num $seed $count)

View file

@ -139,6 +139,7 @@ m4_define([ATX_CHECK_FS], [
fi fi
done done
$6 $6
ATX_RUN_ONE([check-fs ismount "$4"])
], [ ], [
umount "$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 -- dnl -- Helper functions --
AT_TEST_HELPER_FN([rand_u64_hex], , , [ AT_TEST_HELPER_FN([rand_u64_hex], , , [
@ -206,3 +218,6 @@ m4_include([lib_hashmap.at])
AT_BANNER([The Filesystem]) AT_BANNER([The Filesystem])
m4_include([fs_basic.at]) m4_include([fs_basic.at])
m4_include([fs_regrw.at]) m4_include([fs_regrw.at])
m4_include([fs_dents.at])
m4_include([fs_assoc.at])
m4_include([fs_times.at])