/** * bookmarkfs/src/fs_ops.c * ---- * * Copyright (C) 2024 CismonX * * This file is part of BookmarkFS. * * BookmarkFS 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. * * BookmarkFS 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 BookmarkFS. If not, see . */ #ifdef HAVE_CONFIG_H # include "config.h" #endif #include "fs_ops.h" #include #include #include #include #include #include #include "hash.h" #include "hashmap.h" #include "ioctl.h" #include "macros.h" #include "xstd.h" #define FS_FILEMODE_REG ( S_IFREG | (ctx.flags.accmode & ~0111u) ) #define FS_FILEMODE_DIR ( S_IFDIR | ctx.flags.accmode ) #define FS_FILEMODE_ROOT ( FS_FILEMODE_DIR & ~(S_IWUSR | S_IWGRP | S_IWOTH) ) // The 2.5s timeout value is chosen according to how frequent // Chromium saves its bookmarks (`bookmarks::BookmarkStorage::kSaveDelay`). // // See Chromium source code: /components/bookmarks/browser/bookmark_storage.h #define FS_ENTRY_TIMEOUT_MAX INT32_MAX #define FS_ENTRY_TIMEOUT_SECS ( ctx.flags.exclusive ? INT32_MAX : 2.5 ) #define BOOKMARKS_ROOT_ID ( ctx.bookmarks_root_id ) #define TAGS_ROOT_ID ( ctx.tags_root_id ) #define INTFS_NAME_BOOKMARKS "bookmarks" #define INTFS_NAME_TAGS "tags" #define INTFS_NAME_KEYWORDS "keywords" #define FH_BUF_BLOCKSIZE 512 #define FH_BUF_RESERVED(s) ( FH_BUF_BLOCKSIZE - (s) % FH_BUF_BLOCKSIZE ) #define FH_FLAG_DIRTY ( 1u << 0 ) #define FH_FLAG_FSCK ( 1u << 1 ) #define XATTR_PREFIX "user.bookmarkfs." #define SUBSYS_ID_BITS ( 64 - BOOKMARKFS_BOOKMARK_TYPE_BITS ) #define SUBSYS_ID_MASK ( ((fuse_ino_t)1 << SUBSYS_ID_BITS) - 1 ) #define INODE_SUBSYS_TYPE(ino) ( (ino) >> SUBSYS_ID_BITS ) #define INODE_SUBSYS_ID(ino) ( (ino) & SUBSYS_ID_MASK ) #define SUBSYS_NODEID(id, t) ( ((fuse_ino_t)(t) << SUBSYS_ID_BITS) | (id) ) #define STRUCT_FUSE_ENTRY_PARAM_INIT \ { \ .generation = 1, \ .attr = STRUCT_STAT_INIT, \ .attr_timeout = FS_ENTRY_TIMEOUT_SECS, \ .entry_timeout = FS_ENTRY_TIMEOUT_SECS, \ } #define STRUCT_STAT_INIT \ { .st_nlink = 1, .st_uid = ctx.uid, .st_gid = ctx.gid } #define BACKEND_CALL(name, ...) \ ctx.backend_impl->name(ctx.backend_ctx, __VA_ARGS__) #define backend_has_tags() ( TAGS_ROOT_ID != UINT64_MAX ) #define backend_has_keywords() ( ctx.flags.has_keyword ) #define send_reply(name, ...) \ reply_errcheck(fuse_reply_##name(__VA_ARGS__), #name, __LINE__) // Some platforms (e.g. Arm Morello) have 128-bit pointers, // where we cannot safely store a pointer to `fi->fh`. #if defined(SIZEOF_UINTPTR_T) && (SIZEOF_UINTPTR_T > 8) # error "sizeof(uintptr_t) > sizeof(uint64_t)" #endif #define ptr2uint(val) ( (uintptr_t)(void *)(val) ) #define uint2ptr(val) ( (void *)(uintptr_t)(val) ) enum { INTFS_ID_ROOT = FUSE_ROOT_ID, INTFS_ID_BOOKMARKS, INTFS_ID_TAGS, INTFS_ID_KEYWORDS, }; enum { SUBSYS_TYPE_INTERNAL, SUBSYS_TYPE_BOOKMARK, SUBSYS_TYPE_TAG, SUBSYS_TYPE_KEYWORD, }; struct fs_file_handle { uint64_t id; uint32_t refcount; uint32_t flags; char *buf; uint32_t buf_len; uint32_t data_len; void *cookie; struct timespec mtime; }; struct fs_ctx { struct bookmarkfs_backend const *backend_impl; void *backend_ctx; struct hashmap *fh_map; uid_t uid; gid_t gid; struct fs_flags flags; uint64_t bookmarks_root_id; uint64_t tags_root_id; size_t file_max; char const *bookmark_attrs; char *buf; size_t buf_len; struct fuse_session *session; }; struct bm_fsck_ctx { struct bookmarkfs_fsck_data out; int result; }; struct bm_getxattr_ctx { fuse_req_t req; size_t buf_max; }; struct bm_read_ctx { struct fs_file_handle *fh; }; struct bm_readdir_ctx { fuse_req_t req; uint64_t id; uint32_t flags; size_t buf_len; size_t reply_len; }; // Forward declaration start #ifndef __FreeBSD__ static int bm_syncdir (uint64_t, fuse_ino_t); static int intfs_syncdir (unsigned, fuse_ino_t); static int inval_dir (fuse_ino_t, struct fs_file_handle *); static int inval_inode (fuse_ino_t); #endif /* !defined(__FreeBSD__) */ static int bm_create (uint64_t, char const *, int, struct stat *); static int bm_delete (uint64_t, char const *, uint32_t); static int bm_do_write (uint64_t, struct fs_file_handle *); static void bm_fh_free (struct fs_file_handle *, long); static struct fs_file_handle * bm_fh_get (uint64_t, unsigned long *, unsigned long *); static struct fs_file_handle * bm_fh_new (uint64_t); static void bm_fillstat (struct bookmarkfs_bookmark_stat const *, int, bool, struct stat *); static int bm_free (uint64_t, struct fuse_file_info const *); static int bm_freedir (uint64_t, fuse_ino_t, void *); static int bm_fsck (uint64_t, struct bookmarkfs_fsck_data const *, uint32_t, struct bm_fsck_ctx *, void *); static int bm_fsck_cb (void *, int, uint64_t, uint64_t, char const *); static int bm_getxattr (fuse_req_t, uint64_t, char const *, size_t); static int bm_getxattr_cb (void *, void const *, size_t); static int bm_ioctl (fuse_req_t, uint64_t, fuse_ino_t, unsigned, uint32_t, void const *, size_t, size_t, void *); static int bm_lookup (uint64_t, char const *, uint32_t, struct stat *); static int bm_lsxattr (uint64_t, char *, size_t, size_t *); static int bm_mkdir (uint64_t, char const *, int, struct stat *); static int bm_open (uint64_t, struct fuse_file_info *); static int bm_opendir (uint64_t, fuse_ino_t, uint32_t, struct fuse_file_info *); static int bm_permute (uint64_t, struct bookmarkfs_permd_data const *, uint32_t); static int bm_read (fuse_req_t, uint64_t, size_t, off_t, struct fs_file_handle *); static int bm_read_cb (void *, void const *, size_t); static int bm_readdir (fuse_req_t, uint64_t, size_t, off_t, uint32_t, void *); static int bm_readdir_cb (void *, struct bookmarkfs_bookmark_entry const *); static int bm_rename (uint64_t, char const *, uint64_t, char const *, uint32_t); static int bm_rmxattr (uint64_t, char const *); static int bm_set_keyword (uint64_t, char const *, struct stat *); static int bm_set_tag (uint64_t, uint64_t, char const *, struct stat *); static int bm_setattr (uint64_t, int, bool, struct fuse_file_info const *, struct stat *); static int bm_setxattr (uint64_t, char const *, void const *, size_t); static int bm_write (fuse_req_t, int, char const *, size_t, off_t, struct fs_file_handle *); static int current_time (struct timespec *); static void do_delete (fuse_req_t, fuse_ino_t, char const *, uint32_t); static void do_readdir (fuse_req_t, fuse_ino_t, size_t, off_t, uint32_t, struct fuse_file_info const *); static unsigned long fh_entry_hash (void const *); static int fh_entry_comp (union hashmap_key, void const *); static bool has_access (fuse_req_t, int); static int intfs_create (unsigned, char const *, int, struct stat *, struct fuse_file_info *); static int intfs_delete (unsigned, char const *, uint32_t); static int intfs_freedir (unsigned, fuse_ino_t, void *); static int intfs_ioctl (fuse_req_t, unsigned, fuse_ino_t, unsigned, void const *, size_t, size_t, void *); static int intfs_link (uint64_t, unsigned, char const *, struct stat *); static int intfs_lookup (unsigned, char const *, struct stat *); static int intfs_lsxattr (unsigned, char *, size_t, size_t *); static int intfs_mkdir (unsigned, char const *, struct stat *); static int intfs_opendir (unsigned, fuse_ino_t, struct fuse_file_info *); static int intfs_readdir (fuse_req_t, unsigned, size_t, off_t, uint32_t, void *); static int reply_errcheck (int, char const *, int); static int subsys_type (fuse_ino_t, uint64_t *); static int timespec_cmp (struct timespec const *, struct timespec const *); static int xattr_name (char const *, char const **); // Forward declaration end static struct fs_ctx ctx = { .tags_root_id = UINT64_MAX, }; #ifndef __FreeBSD__ static int bm_syncdir ( uint64_t UNUSED_VAR(id), fuse_ino_t ino ) { struct fs_file_handle *fh = bm_fh_get(ino, NULL, NULL); return inval_dir(ino, fh); } static int intfs_syncdir ( unsigned id, fuse_ino_t ino ) { int64_t bm_id = 0; switch (id) { case INTFS_ID_BOOKMARKS: bm_id = BOOKMARKS_ROOT_ID; break; case INTFS_ID_TAGS: bm_id = TAGS_ROOT_ID; break; case INTFS_ID_KEYWORDS: break; default: return 0; } return bm_syncdir(bm_id, ino); } /** * An explicit flush is required, when the kernel holds dent cache * indefinitely, and dents are changed in a way that it could not perceive. * * Not needed on FreeBSD, since it does not support dent caching for fusefs. */ static int inval_dir ( fuse_ino_t ino, struct fs_file_handle *fh ) { if (ctx.flags.readonly || !ctx.flags.exclusive) { return 0; } if (fh->flags & FH_FLAG_DIRTY) { if (0 != inval_inode(ino)) { return -EIO; } fh->flags &= ~FH_FLAG_DIRTY; } return 0; } /** * XXX: We cannot do this on FreeBSD. * * The client holds an exclusive lock of a vnode whenever a VFS request is * being processed on it. Meanwhile, FUSE_NOTIFY_INVAL_INODE also requires * locking the vnode, which may block (on writing to the FUSE device). * * Once FUSE_NOTIFY_INVAL_INODE blocks, we're not able to precess other * requests until write(2) returns. * This results in a deadlock since we're using a single-threaded event loop. */ static int inval_inode ( fuse_ino_t ino ) { int status = fuse_lowlevel_notify_inval_inode(ctx.session, ino, 0, 0); if (status < 0) { log_printf("fuse_lowlevel_notify_inval_inode(): %s", xstrerror(-status)); } return status; } #endif /* !defined(__FreeBSD__) */ static int bm_create ( uint64_t parent_id, char const *name, int flags, struct stat *stat_buf ) { struct bookmarkfs_bookmark_stat stat; int status = BACKEND_CALL(bookmark_create, parent_id, name, 0, &stat); if (status < 0) { if (status != -EEXIST || (flags & O_EXCL)) { return status; } } bm_fillstat(&stat, SUBSYS_TYPE_BOOKMARK, false, stat_buf); return 0; } static int bm_delete ( uint64_t parent_id, char const *name, uint32_t flags ) { return BACKEND_CALL(bookmark_delete, parent_id, name, flags); } static int bm_do_write ( uint64_t id, struct fs_file_handle *fh ) { if (fh == NULL || !(fh->flags & FH_FLAG_DIRTY)) { return 0; } size_t data_len = fh->data_len; if (ctx.flags.eol && fh->buf[data_len - 1] == '\n') { --data_len; } int status = BACKEND_CALL(bookmark_set, id, NULL, 0, fh->buf, data_len); if (status < 0) { return status; } struct timespec const times[] = { { .tv_nsec = UTIME_OMIT }, fh->mtime, }; uint32_t set_flags = BOOKMARK_FLAG(SET_TIME); status = BACKEND_CALL(bookmark_set, id, NULL, set_flags, times, 2); if (status < 0) { return status; } fh->flags &= ~FH_FLAG_DIRTY; return 0; } static void bm_fh_free ( struct fs_file_handle *fh, long entry_id ) { hashmap_entry_delete(ctx.fh_map, fh, entry_id); free(fh); } static struct fs_file_handle * bm_fh_get ( uint64_t id, unsigned long *hashcode_ptr, unsigned long *entry_id_ptr ) { unsigned long hashcode = hash_digest(&id, sizeof(id)); if (hashcode_ptr != NULL) { *hashcode_ptr = hashcode; } union hashmap_key key = { .u64 = id }; return hashmap_search(ctx.fh_map, key, hashcode, entry_id_ptr); } static struct fs_file_handle * bm_fh_new ( uint64_t id ) { unsigned long hashcode; struct fs_file_handle *fh = bm_fh_get(id, &hashcode, NULL); if (fh != NULL) { ++fh->refcount; return fh; } fh = xmalloc(sizeof(*fh)); *fh = (struct fs_file_handle) { .id = id, .refcount = 1, }; union hashmap_key key = { .u64 = id }; *hashmap_insert(ctx.fh_map, key, hashcode) = fh; return fh; } static void bm_fillstat ( struct bookmarkfs_bookmark_stat const *src, int subsys_type, bool with_local, struct stat *dest ) { fuse_ino_t ino = SUBSYS_NODEID(src->id, subsys_type); dest->st_ino = ino; dest->st_atim = src->atime; dest->st_mtim = src->mtime; dest->st_ctim = src->mtime; if (src->value_len < 0) { dest->st_mode = FS_FILEMODE_DIR; return; } dest->st_mode = FS_FILEMODE_REG; dest->st_size = src->value_len + ctx.flags.eol; if (!with_local) { return; } struct fs_file_handle *fh = bm_fh_get(src->id, NULL, NULL); if (fh == NULL || !(fh->flags & FH_FLAG_DIRTY)) { return; } // If file content changes locally, remote mtime no longer matters, // since remote changes to the file will eventually be overwritten. if (!ctx.flags.ctime || 0 < timespec_cmp(&fh->mtime, &dest->st_mtim)) { dest->st_mtim = fh->mtime; dest->st_ctim = fh->mtime; } dest->st_size = fh->data_len; } static int bm_free ( uint64_t id, struct fuse_file_info const *fi ) { struct fs_file_handle *fh = uint2ptr(fi->fh); if (--fh->refcount > 0) { return 0; } int status = bm_do_write(id, fh); #ifndef __FreeBSD__ if (status != 0) { // When writeback fails, bookmark retains original data. // The kernel should re-fetch it from the server. if (0 != inval_inode(SUBSYS_NODEID(id, SUBSYS_TYPE_BOOKMARK))) { status = -EIO; } } #endif if (!ctx.flags.readonly) { free(fh->buf); } if (fh->cookie != NULL) { BACKEND_CALL(object_free, fh->cookie, BOOKMARKFS_OBJECT_TYPE_BGCOOKIE); } bm_fh_free(fh, -1); return status; } static int bm_freedir ( uint64_t UNUSED_VAR(id), fuse_ino_t ino, void *cookie ) { BACKEND_CALL(object_free, cookie, BOOKMARKFS_OBJECT_TYPE_BLCOOKIE); unsigned long entry_id; struct fs_file_handle *fh = bm_fh_get(ino, NULL, &entry_id); if ((fh->flags & FH_FLAG_FSCK) && fh->cookie == cookie) { fh->flags &= ~FH_FLAG_FSCK; } if (--fh->refcount > 0) { return 0; } int status = 0; #ifndef __FreeBSD__ status = inval_dir(ino, fh); #endif bm_fh_free(fh, entry_id); return status; } static int bm_fsck ( uint64_t parent_id, struct bookmarkfs_fsck_data const *data, uint32_t flags, struct bm_fsck_ctx *fctx, void *cookie ) { if (ctx.backend_impl->bookmark_fsck == NULL) { return -ENOTTY; } bookmarkfs_bookmark_fsck_cb *callback = NULL; if (fctx != NULL) { callback = bm_fsck_cb; fctx->result = BOOKMARKFS_FSCK_RESULT_END; } int status = BACKEND_CALL(bookmark_fsck, parent_id, data, flags, callback, fctx, &cookie); if (status < 0) { return status; } if (fctx != NULL) { return fctx->result; } return 0; } static int bm_fsck_cb ( void *user_data, int result, uint64_t id, uint64_t extra, char const *name ) { struct bm_fsck_ctx *fctx = user_data; fctx->out.id = id; fctx->out.extra = extra; if (name != fctx->out.name) { strncpy(fctx->out.name, name, sizeof(fctx->out.name)); } fctx->result = result; return 1; } static int bm_getxattr ( fuse_req_t req, uint64_t id, char const *name, size_t buf_max ) { if (0 != xattr_name(name, &name)) { return -ENOATTR; } struct bm_getxattr_ctx gctx = { .req = req, .buf_max = buf_max, }; return BACKEND_CALL(bookmark_get, id, name, bm_getxattr_cb, &gctx, NULL); } static int bm_getxattr_cb ( void *user_data, void const *xattr_val, size_t xattr_len ) { struct bm_getxattr_ctx *gctx = user_data; size_t buf_max = gctx->buf_max; if (buf_max == 0) { send_reply(xattr, gctx->req, xattr_len); return 0; } if (xattr_len > buf_max) { return -ERANGE; } return send_reply(buf, gctx->req, xattr_val, xattr_len); } static int bm_ioctl ( fuse_req_t req, uint64_t id, fuse_ino_t ino, unsigned cmd, uint32_t flags, void const *ibuf, size_t ibuf_len, size_t obuf_len, void *cookie ) { struct fs_file_handle *fh = bm_fh_get(ino, NULL, NULL); void *obuf = ctx.buf; int result = 0; switch (cmd) { case BOOKMARKFS_IOC_FSCK_REWIND: result = bm_fsck(id, NULL, flags, NULL, cookie); if (result == 0) { fh->flags &= ~FH_FLAG_FSCK; } break; case BOOKMARKFS_IOC_FSCK_NEXT: debug_assert(obuf_len == sizeof(struct bookmarkfs_fsck_data)); if (!has_access(req, R_OK)) { return -EACCES; } if ((fh->flags & FH_FLAG_FSCK) && fh->cookie != cookie) { return -EBUSY; } result = bm_fsck(id, NULL, flags, obuf, cookie); if (result < 0) { break; } if (result == BOOKMARKFS_FSCK_RESULT_END) { fh->flags &= ~FH_FLAG_FSCK; obuf_len = 0; } else { fh->cookie = cookie; fh->flags |= FH_FLAG_FSCK; } break; case BOOKMARKFS_IOC_FSCK_APPLY: debug_assert(ibuf_len == sizeof(struct bookmarkfs_fsck_data)); debug_assert(ibuf_len == obuf_len); if (ctx.flags.readonly) { return -EROFS; } if (!has_access(req, W_OK | X_OK)) { return -EACCES; } result = bm_fsck(id, ibuf, flags, obuf, cookie); if (result == BOOKMARKFS_FSCK_RESULT_END) { fh->flags |= FH_FLAG_DIRTY; obuf_len = 0; } break; case BOOKMARKFS_IOC_PERMD: debug_assert(ibuf_len == sizeof(struct bookmarkfs_permd_data)); if (ctx.flags.readonly) { return -EROFS; } if (!has_access(req, W_OK | X_OK)) { return -EACCES; } result = bm_permute(id, ibuf, flags); if (result < 0) { return result; } fh->flags |= FH_FLAG_DIRTY; break; default: return -ENOTTY; } return send_reply(ioctl, req, result, obuf, obuf_len); } static int bm_lookup ( uint64_t parent_id, char const *name, uint32_t flags, struct stat *stat_buf ) { struct bookmarkfs_bookmark_stat stat; int status = BACKEND_CALL(bookmark_lookup, parent_id, name, flags, &stat); if (status < 0) { return status; } int subsys_type = SUBSYS_TYPE_BOOKMARK; if (BOOKMARKFS_BOOKMARK_IS_TYPE(flags, TAG) && parent_id == ctx.tags_root_id ) { subsys_type = SUBSYS_TYPE_TAG; } bm_fillstat(&stat, subsys_type, true, stat_buf); return 0; } static int bm_lsxattr ( uint64_t UNUSED_VAR(id), char *buf, size_t buf_max, size_t *buf_len_ptr ) { size_t buf_len = 0; for (char const *xattrs = ctx.bookmark_attrs, *next; ; xattrs = next) { size_t xattr_len = strlen(xattrs) + 1; if (xattr_len == 1) { break; } next = xattrs + xattr_len; xattr_len += strlen(XATTR_PREFIX); buf_len += xattr_len; if (buf_max == 0) { continue; } if (unlikely(buf_len > buf_max)) { return -ERANGE; } char *dest = buf + buf_len - xattr_len; memcpy(dest, XATTR_PREFIX, strlen(XATTR_PREFIX)); dest += strlen(XATTR_PREFIX); memcpy(dest, xattrs, xattr_len - strlen(XATTR_PREFIX)); } *buf_len_ptr = buf_len; return 0; } static int bm_mkdir ( uint64_t parent_id, char const *name, int subsys_type, struct stat *stat_buf ) { uint32_t flags = BOOKMARK_FLAG(CREATE_DIR); if (subsys_type == SUBSYS_TYPE_TAG) { flags |= BOOKMARKFS_BOOKMARK_TYPE(TAG); } struct bookmarkfs_bookmark_stat stat; int status = BACKEND_CALL(bookmark_create, parent_id, name, flags, &stat); if (status < 0) { return status; } bm_fillstat(&stat, subsys_type, false, stat_buf); return 0; } static int bm_open ( uint64_t id, struct fuse_file_info *fi ) { struct fs_file_handle *fh = bm_fh_new(id); int flags = fi->flags; if (flags & O_TRUNC) { // POSIX does not specify the behavior for O_RDONLY|O_TRUNC. // We choose to silently ignore O_TRUNC. if ((flags & O_ACCMODE) != O_RDONLY) { fh->data_len = 0; current_time(&fh->mtime); fh->flags |= FH_FLAG_DIRTY; } } if (flags & O_DIRECT) { fi->direct_io = 1; } #ifndef __FreeBSD__ // Cannot reliably keep cache on FreeBSD, we're unable to explicitly // flush it. See comments for inval_inode(). fi->keep_cache = 1; #endif fi->noflush = 1; fi->fh = ptr2uint(fh); return 0; } static int bm_opendir ( uint64_t id, fuse_ino_t ino, uint32_t flags, struct fuse_file_info *fi ) { void *cookie = NULL; int status = BACKEND_CALL(bookmark_list, id, 0, flags, NULL, NULL, &cookie); if (status < 0) { return status; } bm_fh_new(ino); // Do not cache tag and keyword dents, since a bookmark // delete or rename may have a side effect of changing them. switch (flags & BOOKMARKFS_BOOKMARK_TYPE_MASK) { case BOOKMARKFS_BOOKMARK_TYPE(TAG): if (id != ctx.tags_root_id) { break; } // fallthrough case BOOKMARKFS_BOOKMARK_TYPE(BOOKMARK): fi->cache_readdir = 1; fi->keep_cache = 1; break; } fi->fh = ptr2uint(cookie); return 0; } static int bm_permute ( uint64_t id, struct bookmarkfs_permd_data const *permd_data, uint32_t flags ) { return BACKEND_CALL(bookmark_permute, id, permd_data->op, permd_data->name1, permd_data->name2, flags); } static int bm_read ( fuse_req_t req, uint64_t id, size_t buf_max, off_t off, struct fs_file_handle *fh ) { if (fh->flags & FH_FLAG_DIRTY) { goto do_read; } void **cookie_ptr = &fh->cookie; if (ctx.flags.exclusive) { // In exclusive mode, ignore server-side changes. if (fh->buf != NULL) { goto do_read; } cookie_ptr = NULL; } struct bm_read_ctx rctx = { .fh = fh }; int result = BACKEND_CALL(bookmark_get, id, NULL, bm_read_cb, &rctx, cookie_ptr); if (result < 0) { if (result != -EAGAIN && result != -ESTALE) { return result; } } do_read: ; char *buf = fh->buf; size_t buf_len = fh->data_len; if ((size_t)off > buf_len) { off = buf_len; } size_t bytes_read = buf_len - off; if (bytes_read > buf_max) { bytes_read = buf_max; } // buf may be NULL return send_reply(buf, req, off == 0 ? buf : buf + off, bytes_read); } static int bm_read_cb ( void *user_data, void const *val, size_t val_len ) { struct bm_read_ctx *rctx = user_data; size_t buf_len = val_len; if (ctx.flags.readonly) { buf_len += ctx.flags.eol; } else { buf_len += FH_BUF_RESERVED(buf_len); } struct fs_file_handle *fh = rctx->fh; if (fh->buf_len < buf_len) { free(fh->buf); fh->buf = xmalloc(buf_len); fh->buf_len = buf_len; } memcpy(fh->buf, val, val_len); if (ctx.flags.eol) { fh->buf[val_len++] = '\n'; } fh->data_len = val_len; return 0; } static int bm_readdir ( fuse_req_t req, uint64_t id, size_t buf_max, off_t off, uint32_t flags, void *cookie ) { struct bm_readdir_ctx rctx = { .req = req, .id = id, .flags = flags, .buf_len = buf_max, }; int status = BACKEND_CALL(bookmark_list, id, off, flags, bm_readdir_cb, &rctx, &cookie); if (status < 0) { return status; } return send_reply(buf, req, ctx.buf, rctx.reply_len); } static int bm_readdir_cb ( void *user_data, struct bookmarkfs_bookmark_entry const *entry ) { struct bm_readdir_ctx *rctx = user_data; bool is_readdirplus = rctx->flags & BOOKMARK_FLAG(LIST_WITHSTAT); bool is_tag = BOOKMARKFS_BOOKMARK_IS_TYPE(rctx->flags, TAG); char *buf = ctx.buf + rctx->reply_len; size_t buf_len = rctx->buf_len - rctx->reply_len; struct fuse_entry_param ep = STRUCT_FUSE_ENTRY_PARAM_INIT; int subsys_type = SUBSYS_TYPE_BOOKMARK; if (is_tag && rctx->id == ctx.tags_root_id) { subsys_type = SUBSYS_TYPE_TAG; } bm_fillstat(&entry->stat, subsys_type, is_readdirplus, &ep.attr); ep.ino = ep.attr.st_ino; size_t entry_size; if (is_readdirplus) { entry_size = fuse_add_direntry_plus(rctx->req, buf, buf_len, entry->name, &ep, entry->next); } else { entry_size = fuse_add_direntry(rctx->req, buf, buf_len, entry->name, &ep.attr, entry->next); } if (entry_size > buf_len) { return 1; } rctx->reply_len += entry_size; return 0; } static int bm_rename ( uint64_t parent_id, char const *name, uint64_t new_parent_id, char const *new_name, uint32_t flags ) { if (BOOKMARKFS_BOOKMARK_IS_TYPE(flags, TAG)) { // Only allow renaming tag directories. if (parent_id != TAGS_ROOT_ID || parent_id != new_parent_id) { return -EPERM; } } return BACKEND_CALL(bookmark_rename, parent_id, name, new_parent_id, new_name, flags); } static int bm_rmxattr ( uint64_t UNUSED_VAR(id), char const *name ) { if (0 != xattr_name(name, NULL)) { return -ENOATTR; } return -EPERM; } static int bm_set_keyword ( uint64_t id, char const *keyword, struct stat *stat_buf ) { struct bookmarkfs_bookmark_stat stat; stat.id = id; int status = BACKEND_CALL(bookmark_create, 0, keyword, BOOKMARKFS_BOOKMARK_TYPE(KEYWORD), &stat); if (status < 0) { return status; } bm_fillstat(&stat, SUBSYS_TYPE_BOOKMARK, true, stat_buf); return 0; } static int bm_set_tag ( uint64_t id, uint64_t new_parent_id, char const *new_name, struct stat *stat_buf ) { struct bookmarkfs_bookmark_stat stat; stat.id = id; int status = BACKEND_CALL(bookmark_create, new_parent_id, new_name, BOOKMARKFS_BOOKMARK_TYPE(TAG), &stat); if (status < 0) { return status; } bm_fillstat(&stat, SUBSYS_TYPE_BOOKMARK, true, stat_buf); return 0; } static int bm_setattr ( uint64_t id, int mask, bool is_tag, struct fuse_file_info const *fi, struct stat *stat_buf ) { #define FUSE_SET_ATTR_NAME_(name) FUSE_SET_ATTR_##name #define TO_SET(...) BITWISE_OR(FUSE_SET_ATTR_NAME_, __VA_ARGS__) int flags_unsupported = TO_SET(MODE, UID, GID); if (mask & flags_unsupported) { return -EPERM; } if (mask & TO_SET(SIZE)) { debug_assert(!is_tag); struct fs_file_handle *fh = uint2ptr(fi->fh); debug_assert(fh == bm_fh_get(id, NULL, NULL)); size_t new_len = stat_buf->st_size; if (new_len > ctx.file_max) { return -EFBIG; } if (fh->data_len < new_len) { if (fh->buf_len < new_len) { fh->buf_len = new_len + FH_BUF_RESERVED(new_len); fh->buf = xrealloc(fh->buf, fh->buf_len); } memset(fh->buf + fh->data_len, 0, new_len - fh->data_len); } fh->data_len = new_len; if (unlikely(0 != current_time(&fh->mtime))) { return -EIO; } fh->flags |= FH_FLAG_DIRTY; } if (mask & (TO_SET(ATIME, MTIME))) { if (ctx.flags.ctime) { mask |= TO_SET(MTIME, MTIME_NOW); } struct timespec ts = { .tv_nsec = UTIME_OMIT }; if (mask & TO_SET(ATIME_NOW, MTIME_NOW)) { if (unlikely(0 != current_time(&ts))) { return -EIO; } } struct timespec times[] = { ts, ts }; if (TO_SET(ATIME) == (mask & TO_SET(ATIME, ATIME_NOW))) { times[0] = stat_buf->st_atim; } if (TO_SET(MTIME) == (mask & TO_SET(MTIME, MTIME_NOW))) { times[1] = stat_buf->st_mtim; } uint32_t set_flags = BOOKMARK_FLAG(SET_TIME); if (is_tag) { set_flags |= BOOKMARKFS_BOOKMARK_TYPE(TAG); } int status = BACKEND_CALL(bookmark_set, id, NULL, set_flags, times, 2); if (status < 0) { return status; } } #undef TO_SET uint32_t lookup_flags = 0; if (is_tag) { lookup_flags |= BOOKMARKFS_BOOKMARK_TYPE(TAG); } *stat_buf = (struct stat) STRUCT_STAT_INIT; return bm_lookup(id, NULL, lookup_flags, stat_buf); } static int bm_setxattr ( uint64_t id, char const *name, void const *val, size_t val_len ) { if (0 != xattr_name(name, &name)) { return -ENOATTR; } return BACKEND_CALL(bookmark_set, id, name, 0, val, val_len); } static int bm_write ( fuse_req_t req, int flags, char const *req_buf, size_t req_buf_len, off_t off, struct fs_file_handle *fh ) { if (req_buf_len == 0) { return 0; } if (flags & O_APPEND) { off = fh->data_len; } size_t req_off_max = req_buf_len + off; if (req_off_max > ctx.file_max) { return -EFBIG; } char *buf = fh->buf; if (req_off_max > fh->buf_len) { fh->buf_len = req_off_max + FH_BUF_RESERVED(req_off_max); buf = xrealloc(buf, fh->buf_len); fh->buf = buf; } memcpy(buf + off, req_buf, req_buf_len); off_t hole_len = off - fh->data_len; if (hole_len > 0) { memset(buf + fh->data_len, 0, hole_len); } if (req_off_max > fh->data_len) { fh->data_len = req_off_max; } if (unlikely(0 != current_time(&fh->mtime))) { return -EIO; } // We're tempted to free the cookie here, however, // that would make a read following a writeback always realloc the buffer. fh->flags |= FH_FLAG_DIRTY; return send_reply(write, req, req_buf_len); } static int current_time ( struct timespec *ts ) { if (unlikely(0 != clock_gettime(CLOCK_REALTIME, ts))) { log_printf("clock_gettime(): %s", xstrerror(errno)); return -1; } return 0; } static void do_delete ( fuse_req_t req, fuse_ino_t parent, char const *name, uint32_t flags ) { int status = -EIO; switch (INODE_SUBSYS_TYPE(parent)) { case SUBSYS_TYPE_INTERNAL: status = intfs_delete(INODE_SUBSYS_ID(parent), name, flags); break; case SUBSYS_TYPE_TAG: flags |= BOOKMARKFS_BOOKMARK_TYPE(TAG); // fallthrough case SUBSYS_TYPE_BOOKMARK: status = bm_delete(INODE_SUBSYS_ID(parent), name, flags); break; } send_reply(err, req, -status); } static void do_readdir ( fuse_req_t req, fuse_ino_t ino, size_t buf_max, off_t off, uint32_t flags, struct fuse_file_info const *fi ) { debug_assert(off >= 0); int status = -EIO; if (unlikely(buf_max > ctx.buf_len)) { // Most likely that a process has just called getdirentries() // with a custom buffer size. Should not happen on Linux. debug_printf("bm_readdir(): reply_buf_max > page_size (%zu > %zu)", buf_max, ctx.buf_len); buf_max = ctx.buf_len; } void *cookie = uint2ptr(fi->fh); switch (INODE_SUBSYS_TYPE(ino)) { case SUBSYS_TYPE_INTERNAL: status = intfs_readdir(req, INODE_SUBSYS_ID(ino), buf_max, off, flags, cookie); break; case SUBSYS_TYPE_TAG: flags |= BOOKMARKFS_BOOKMARK_TYPE(TAG); // fallthrough case SUBSYS_TYPE_BOOKMARK: status = bm_readdir(req, INODE_SUBSYS_ID(ino), buf_max, off, flags, cookie); break; } if (status < 0) { send_reply(err, req, -status); return; } } static unsigned long fh_entry_hash ( void const *entry ) { struct fs_file_handle const *fh = entry; return hash_digest(&fh->id, sizeof(fuse_ino_t)); } static int fh_entry_comp ( union hashmap_key key, void const *entry ) { struct fs_file_handle const *fh = entry; if (key.u64 != fh->id) { return -1; } return 0; } static bool has_access ( fuse_req_t req, int to_check ) { struct fuse_ctx const *fctx = fuse_req_ctx(req); mode_t req_mode = 0; if (to_check & R_OK) { req_mode |= (S_IRUSR | S_IRGRP | S_IROTH); } if (to_check & W_OK) { req_mode |= (S_IWUSR | S_IWGRP | S_IWOTH); } if (to_check & X_OK) { req_mode |= (S_IXUSR | S_IXGRP | S_IXOTH); } if (fctx->uid != ctx.uid) { req_mode &= ~(S_IRUSR | S_IWUSR | S_IXUSR); } if (fctx->gid != ctx.gid) { req_mode &= ~(S_IRGRP | S_IWGRP | S_IXGRP); } return req_mode & ~ctx.flags.accmode; } static int intfs_create ( unsigned parent_id, char const *name, int flags, struct stat *stat_buf, struct fuse_file_info *fi ) { int status = -EPERM; switch (parent_id) { case INTFS_ID_BOOKMARKS: status = bm_create(BOOKMARKS_ROOT_ID, name, flags, stat_buf); if (status < 0) { break; } status = bm_open(INODE_SUBSYS_ID(stat_buf->st_ino), fi); break; } return status; } static int intfs_delete ( unsigned parent_id, char const *name, uint32_t flags ) { int status = -EPERM; switch (parent_id) { case INTFS_ID_TAGS: flags |= BOOKMARKFS_BOOKMARK_TYPE(TAG); // fallthrough case INTFS_ID_BOOKMARKS: status = bm_delete(BOOKMARKS_ROOT_ID, name, flags); break; case INTFS_ID_KEYWORDS: status = bm_delete(0, name, BOOKMARKFS_BOOKMARK_TYPE(KEYWORD)); break; } return status; } static int intfs_freedir ( unsigned id, fuse_ino_t ino, void *cookie ) { int64_t bm_id = 0; switch (id) { case INTFS_ID_BOOKMARKS: bm_id = BOOKMARKS_ROOT_ID; break; case INTFS_ID_TAGS: bm_id = TAGS_ROOT_ID; break; case INTFS_ID_KEYWORDS: break; default: return 0; } return bm_freedir(bm_id, ino, cookie); } static int intfs_ioctl ( fuse_req_t req, unsigned id, fuse_ino_t ino, unsigned cmd, void const *ibuf, size_t ibuf_len, size_t obuf_len, void *cookie ) { uint64_t bm_id = 0; uint32_t flags = 0; switch (id) { case INTFS_ID_BOOKMARKS: bm_id = BOOKMARKS_ROOT_ID; break; case INTFS_ID_TAGS: bm_id = TAGS_ROOT_ID; flags |= BOOKMARKFS_BOOKMARK_TYPE(TAG); break; case INTFS_ID_KEYWORDS: flags |= BOOKMARKFS_BOOKMARK_TYPE(KEYWORD); break; default: return -ENOTTY; } return bm_ioctl(req, bm_id, ino, cmd, flags, ibuf, ibuf_len, obuf_len, cookie); } static int intfs_link ( uint64_t id, unsigned new_parent_id, char const *new_name, struct stat *stat_buf ) { int status = -EPERM; switch (new_parent_id) { case INTFS_ID_KEYWORDS: status = bm_set_keyword(id, new_name, stat_buf); break; } return status; } static int intfs_lookup ( unsigned parent_id, char const *name, struct stat *stat_buf ) { int status = -ENOENT; switch (parent_id) { case INTFS_ID_ROOT: if (name == NULL) { stat_buf->st_mode = FS_FILEMODE_ROOT; status = 0; } #define INTFS_ENTRY(which, id, flags, cond) \ else if (0 == strcmp(name, INTFS_NAME_##which)) { \ parent_id = INTFS_ID_##which; \ name = NULL; \ /* fallthrough */ \ case INTFS_ID_##which: \ if (!(cond)) { \ break; \ } \ status = bm_lookup(id, name, flags, stat_buf); \ } INTFS_ENTRY(BOOKMARKS, BOOKMARKS_ROOT_ID, 0, true) INTFS_ENTRY(TAGS, TAGS_ROOT_ID, BOOKMARKFS_BOOKMARK_TYPE(TAG), backend_has_tags()) INTFS_ENTRY(KEYWORDS, 0, BOOKMARKFS_BOOKMARK_TYPE(KEYWORD), backend_has_keywords()) #undef INTFS_ENTRY break; } if (status < 0) { return status; } if (name == NULL) { stat_buf->st_ino = parent_id; } return 0; } static int intfs_lsxattr ( unsigned id, char *buf, size_t buf_max, size_t *buf_len_ptr ) { int status = 0; switch (id) { case INTFS_ID_BOOKMARKS: status = bm_lsxattr(BOOKMARKS_ROOT_ID, buf, buf_max, buf_len_ptr); break; } return status; } static int intfs_mkdir ( unsigned parent_id, char const *name, struct stat *stat_buf ) { int status = -EACCES; switch (parent_id) { case INTFS_ID_BOOKMARKS: status = bm_mkdir(BOOKMARKS_ROOT_ID, name, SUBSYS_TYPE_BOOKMARK, stat_buf); break; case INTFS_ID_TAGS: status = bm_mkdir(TAGS_ROOT_ID, name, SUBSYS_TYPE_TAG, stat_buf); break; } return status; } static int intfs_opendir ( unsigned id, fuse_ino_t ino, struct fuse_file_info *fi ) { int status = 0; 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; case INTFS_ID_TAGS: flags |= BOOKMARKFS_BOOKMARK_TYPE(TAG); status = bm_opendir(TAGS_ROOT_ID, ino, flags, fi); break; } return status; } static int intfs_readdir ( fuse_req_t req, unsigned id, size_t buf_max, off_t off, uint32_t flags, void *cookie ) { switch (id) { case INTFS_ID_BOOKMARKS: return bm_readdir(req, BOOKMARKS_ROOT_ID, buf_max, off, flags, cookie); case INTFS_ID_TAGS: flags |= BOOKMARKFS_BOOKMARK_TYPE(TAG); return bm_readdir(req, TAGS_ROOT_ID, buf_max, off, flags, cookie); case INTFS_ID_KEYWORDS: flags |= BOOKMARKFS_BOOKMARK_TYPE(KEYWORD); return bm_readdir(req, 0, buf_max, off, flags, cookie); } debug_assert(id == INTFS_ID_ROOT); bool is_readdirplus = flags & BOOKMARK_FLAG(LIST_WITHSTAT); char *reply_buf = ctx.buf; size_t reply_size = 0; #define INTFS_DIRENT(which, cond) \ { (cond) ? INTFS_NAME_##which : NULL, INTFS_ID_##which, false } struct intfs_dirent { char const *name; unsigned id; bool is_root; } const dents[] = { INTFS_DIRENT(BOOKMARKS, true), INTFS_DIRENT(TAGS, backend_has_tags()), INTFS_DIRENT(KEYWORDS, backend_has_keywords()), }; size_t dents_cnt = sizeof(dents) / sizeof(struct intfs_dirent); for (size_t idx = off; idx < dents_cnt; ++idx) { struct intfs_dirent const *dent = dents + idx; if (dent->name == NULL) { continue; } struct fuse_entry_param ep = STRUCT_FUSE_ENTRY_PARAM_INIT; if (dent->is_root) { ep.attr.st_mode = FS_FILEMODE_ROOT; } else { if (is_readdirplus) { int status = intfs_lookup(dent->id, NULL, &ep.attr); if (status < 0) { return status; } } else { ep.attr.st_mode = FS_FILEMODE_DIR; } } ep.ino = dent->id; ep.attr.st_ino = dent->id; size_t entry_size; if (is_readdirplus) { entry_size = fuse_add_direntry_plus(req, reply_buf, buf_max, dent->name, &ep, idx + 1); } else { entry_size = fuse_add_direntry(req, reply_buf, buf_max, dent->name, &ep.attr, idx + 1); } if (entry_size > buf_max) { break; } reply_buf += entry_size; buf_max -= entry_size; reply_size += entry_size; } return send_reply(buf, req, reply_buf - reply_size, reply_size); } static int reply_errcheck ( int err, char const *name, int line ) { if (err < 0) { log_printf("%d: fuse_reply_%s(): %s", line, name, xstrerror(-err)); } return 0; } static int subsys_type ( fuse_ino_t ino, uint64_t *subsys_id_ptr ) { int subsys_type = INODE_SUBSYS_TYPE(ino); uint64_t subsys_id = INODE_SUBSYS_ID(ino); if (subsys_type != SUBSYS_TYPE_INTERNAL) { *subsys_id_ptr = subsys_id; return subsys_type; } switch (subsys_id) { case INTFS_ID_BOOKMARKS: *subsys_id_ptr = BOOKMARKS_ROOT_ID; return SUBSYS_TYPE_BOOKMARK; case INTFS_ID_TAGS: *subsys_id_ptr = TAGS_ROOT_ID; return SUBSYS_TYPE_TAG; default: return -1; } } static int timespec_cmp ( struct timespec const *t1, struct timespec const *t2 ) { if (t1->tv_sec < t2->tv_sec) { return -1; } if (t1->tv_sec > t2->tv_sec) { return 1; } if (t1->tv_nsec < t2->tv_nsec) { return -1; } if (t1->tv_nsec > t2->tv_nsec) { return 1; } return 0; } static int xattr_name ( char const *name, char const **name_ptr ) { size_t name_len = strlen(name); size_t prefix_len = strlen(XATTR_PREFIX); if (name_len <= prefix_len) { return -1; } if (0 != memcmp(name, XATTR_PREFIX, prefix_len)) { return -1; } name += prefix_len; name_len -= prefix_len; for (char const *xattrs = ctx.bookmark_attrs, *next; ; xattrs = next) { size_t xattr_len = strlen(xattrs); if (xattr_len == 0) { break; } next = xattrs + xattr_len + 1; if (name_len != xattr_len) { continue; } if (0 != memcmp(name, xattrs, name_len)) { continue; } if (name_ptr != NULL) { *name_ptr = name; } return 0; } return -1; } void fs_init_backend ( struct bookmarkfs_backend const *backend_impl, void *backend_ctx ) { ctx.backend_impl = backend_impl; ctx.backend_ctx = backend_ctx; } void fs_init_fuse ( struct fuse_session *session ) { ctx.session = session; } void fs_init_metadata ( uint64_t bookmarks_root_id, uint64_t tags_root_id, char const *bookmark_attrs ) { if (bookmarks_root_id != UINT64_MAX) { ctx.bookmarks_root_id = bookmarks_root_id; } if (tags_root_id != UINT64_MAX) { ctx.tags_root_id = tags_root_id; } if (bookmark_attrs != NULL) { ctx.bookmark_attrs = bookmark_attrs; } } void fs_init_opts ( struct fs_flags flags, size_t file_max ) { ctx.flags = flags; ctx.file_max = file_max; } void fs_op_create ( fuse_req_t req, fuse_ino_t parent, char const *name, mode_t UNUSED_VAR(mode), struct fuse_file_info *fi ) { int status = -EPERM; struct fuse_entry_param ep = STRUCT_FUSE_ENTRY_PARAM_INIT; switch (INODE_SUBSYS_TYPE(parent)) { case SUBSYS_TYPE_INTERNAL: status = intfs_create(INODE_SUBSYS_ID(parent), name, fi->flags, &ep.attr, fi); break; case SUBSYS_TYPE_BOOKMARK: status = bm_create(INODE_SUBSYS_ID(parent), name, fi->flags, &ep.attr); if (status < 0) { break; } status = bm_open(INODE_SUBSYS_ID(ep.attr.st_ino), fi); break; } if (status < 0) { send_reply(err, req, -status); return; } ep.ino = ep.attr.st_ino; send_reply(create, req, &ep, fi); } void fs_op_destroy ( void *UNUSED_VAR(user_data) ) { hashmap_destroy(ctx.fh_map); free(ctx.buf); } void fs_op_fsync ( fuse_req_t req, fuse_ino_t ino, int UNUSED_VAR(data_sync), struct fuse_file_info *fi ) { int status = 0; if (ctx.flags.readonly) { goto end; } struct fs_file_handle *fh = uint2ptr(fi->fh); switch (INODE_SUBSYS_TYPE(ino)) { case SUBSYS_TYPE_BOOKMARK: status = bm_do_write(INODE_SUBSYS_ID(ino), fh); break; } if (status < 0) { goto end; } status = ctx.backend_impl->backend_sync(ctx.backend_ctx); end: send_reply(err, req, -status); } void fs_op_fsyncdir ( fuse_req_t req, fuse_ino_t ino, int UNUSED_VAR(data_sync), struct fuse_file_info *UNUSED_VAR(fi) ) { int status = 0; if (ctx.flags.readonly) { goto end; } switch (INODE_SUBSYS_TYPE(ino)) { #ifndef __FreeBSD__ case SUBSYS_TYPE_INTERNAL: status = intfs_syncdir(INODE_SUBSYS_ID(ino), ino); break; case SUBSYS_TYPE_BOOKMARK: status = bm_syncdir(INODE_SUBSYS_ID(ino), ino); break; #endif } if (status < 0) { goto end; } status = ctx.backend_impl->backend_sync(ctx.backend_ctx); end: send_reply(err, req, -status); } void fs_op_getattr ( fuse_req_t req, fuse_ino_t ino, struct fuse_file_info *UNUSED_VAR(fi) ) { int status = -EIO; uint32_t flags = 0; struct stat stat_buf = STRUCT_STAT_INIT; switch (INODE_SUBSYS_TYPE(ino)) { case SUBSYS_TYPE_INTERNAL: status = intfs_lookup(INODE_SUBSYS_ID(ino), NULL, &stat_buf); break; case SUBSYS_TYPE_TAG: flags |= BOOKMARKFS_BOOKMARK_TYPE(TAG); // fallthrough case SUBSYS_TYPE_BOOKMARK: status = bm_lookup(INODE_SUBSYS_ID(ino), NULL, flags, &stat_buf); break; } if (status < 0) { send_reply(err, req, -status); return; } send_reply(attr, req, &stat_buf, FS_ENTRY_TIMEOUT_SECS); } void fs_op_getxattr ( fuse_req_t req, fuse_ino_t ino, char const *name, size_t buf_max ) { int status = -ENOATTR; switch (INODE_SUBSYS_TYPE(ino)) { case SUBSYS_TYPE_BOOKMARK: status = bm_getxattr(req, INODE_SUBSYS_ID(ino), name, buf_max); break; } if (status < 0) { send_reply(err, req, -status); return; } } void fs_op_init ( void *UNUSED_VAR(user_data), struct fuse_conn_info *UNUSED_VAR(conn) ) { // We're tempted to enable writeback caching in exclusive mode, however, // FUSE_NOTIFY_INVAL_INODE does not clear the cached `st_size` attribute, // leading to bad file size after the backend fails to set bookmark URL. #if 0 if (ctx.flags.exclusive) { conn->want |= FUSE_CAP_WRITEBACK_CACHE; } #endif ctx.fh_map = hashmap_create(fh_entry_comp, fh_entry_hash); ctx.uid = geteuid(); ctx.gid = getegid(); ctx.buf_len = sysconf(_SC_PAGE_SIZE); xassert(ctx.buf_len >= sizeof(union { struct bm_fsck_ctx fsck_ctx; struct bookmarkfs_permd_data permd; })); // The requested buffer size for FUSE_READDIR and FUSE_READDIRPLUS // always equals to the page size on Linux. // // On FreeBSD, it equals to the `nbytes` argument of the underlying // getdirentries() - also equals to the page size when called from // readdir() (unless it is not a multiple of DIRBLKSIZ). ctx.buf = xmalloc(ctx.buf_len); } void fs_op_ioctl ( fuse_req_t req, fuse_ino_t ino, unsigned cmd, void *UNUSED_VAR(arg), struct fuse_file_info *fi, unsigned flags, void const *ibuf, size_t ibuf_len, size_t obuf_len ) { int status = -ENOTTY; // Currently all ioctls only apply to directories. if (!(flags & FUSE_IOCTL_DIR)) { goto fail; } void *cookie = uint2ptr(fi->fh); switch (INODE_SUBSYS_TYPE(ino)) { case SUBSYS_TYPE_INTERNAL: status = intfs_ioctl(req, INODE_SUBSYS_ID(ino), ino, cmd, ibuf, ibuf_len, obuf_len, cookie); break; case SUBSYS_TYPE_BOOKMARK: status = bm_ioctl(req, INODE_SUBSYS_ID(ino), ino, cmd, 0, ibuf, ibuf_len, obuf_len, cookie); break; } if (status < 0) { fail: send_reply(err, req, -status); return; } } void fs_op_link ( fuse_req_t req, fuse_ino_t ino, fuse_ino_t new_parent, char const *new_name ) { int status = -EPERM; if (INODE_SUBSYS_TYPE(ino) != SUBSYS_TYPE_BOOKMARK) { goto fail; } struct fuse_entry_param ep = STRUCT_FUSE_ENTRY_PARAM_INIT; switch (INODE_SUBSYS_TYPE(new_parent)) { case SUBSYS_TYPE_INTERNAL: status = intfs_link(INODE_SUBSYS_ID(ino), INODE_SUBSYS_ID(new_parent), new_name, &ep.attr); break; case SUBSYS_TYPE_TAG: status = bm_set_tag(INODE_SUBSYS_ID(ino), INODE_SUBSYS_ID(new_parent), new_name, &ep.attr); break; } if (status < 0) { fail: send_reply(err, req, -status); return; } ep.ino = ep.attr.st_ino; send_reply(entry, req, &ep); } void fs_op_listxattr ( fuse_req_t req, fuse_ino_t ino, size_t buf_max ) { int status = -E2BIG; if (unlikely(buf_max > ctx.buf_len)) { goto fail; } status = 0; char *buf = ctx.buf; size_t buf_len = 0; switch (INODE_SUBSYS_TYPE(ino)) { case SUBSYS_TYPE_INTERNAL: status = intfs_lsxattr(INODE_SUBSYS_ID(ino), buf, buf_max, &buf_len); break; case SUBSYS_TYPE_BOOKMARK: status = bm_lsxattr(INODE_SUBSYS_ID(ino), buf, buf_max, &buf_len); break; } if (status < 0) { fail: send_reply(err, req, -status); return; } if (buf_max == 0) { send_reply(xattr, req, buf_len); } else { send_reply(buf, req, buf, buf_len); } } void fs_op_lookup ( fuse_req_t req, fuse_ino_t parent, char const *name ) { int status = -EIO; uint32_t flags = 0; struct fuse_entry_param ep = STRUCT_FUSE_ENTRY_PARAM_INIT; switch (INODE_SUBSYS_TYPE(parent)) { case SUBSYS_TYPE_INTERNAL: ep.entry_timeout = FS_ENTRY_TIMEOUT_MAX; status = intfs_lookup(INODE_SUBSYS_ID(parent), name, &ep.attr); break; case SUBSYS_TYPE_TAG: flags |= BOOKMARKFS_BOOKMARK_TYPE(TAG); // fallthrough case SUBSYS_TYPE_BOOKMARK: status = bm_lookup(INODE_SUBSYS_ID(parent), name, flags, &ep.attr); break; } if (status < 0) { send_reply(err, req, -status); return; } ep.ino = ep.attr.st_ino; send_reply(entry, req, &ep); } void fs_op_mkdir ( fuse_req_t req, fuse_ino_t parent, char const *name, mode_t UNUSED_VAR(mode) ) { int status = -EPERM; struct fuse_entry_param ep = STRUCT_FUSE_ENTRY_PARAM_INIT; switch (INODE_SUBSYS_TYPE(parent)) { case SUBSYS_TYPE_INTERNAL: status = intfs_mkdir(INODE_SUBSYS_ID(parent), name, &ep.attr); break; case SUBSYS_TYPE_BOOKMARK: status = bm_mkdir(INODE_SUBSYS_ID(parent), name, SUBSYS_TYPE_BOOKMARK, &ep.attr); break; } if (status < 0) { send_reply(err, req, -status); return; } ep.ino = ep.attr.st_ino; send_reply(entry, req, &ep); } void fs_op_open ( fuse_req_t req, fuse_ino_t ino, struct fuse_file_info *fi ) { int status = -EIO; switch (INODE_SUBSYS_TYPE(ino)) { case SUBSYS_TYPE_BOOKMARK: status = bm_open(INODE_SUBSYS_ID(ino), fi); break; } if (status < 0) { send_reply(err, req, -status); return; } send_reply(open, req, fi); } void fs_op_opendir ( fuse_req_t req, fuse_ino_t ino, struct fuse_file_info *fi ) { int status = -EIO; uint32_t flags = 0; switch (INODE_SUBSYS_TYPE(ino)) { case SUBSYS_TYPE_INTERNAL: status = intfs_opendir(INODE_SUBSYS_ID(ino), ino, fi); break; case SUBSYS_TYPE_TAG: flags |= BOOKMARKFS_BOOKMARK_TYPE(TAG); // fallthrough case SUBSYS_TYPE_BOOKMARK: status = bm_opendir(INODE_SUBSYS_ID(ino), ino, flags, fi); break; } if (status < 0) { send_reply(err, req, -status); return; } send_reply(open, req, fi); } void fs_op_read ( fuse_req_t req, fuse_ino_t ino, size_t buf_max, off_t off, struct fuse_file_info *fi ) { debug_assert(off >= 0); int status = -EIO; struct fs_file_handle *fh = uint2ptr(fi->fh); switch (INODE_SUBSYS_TYPE(ino)) { case SUBSYS_TYPE_BOOKMARK: status = bm_read(req, INODE_SUBSYS_ID(ino), buf_max, off, fh); break; } if (status < 0) { send_reply(err, req, -status); return; } } void fs_op_readdir ( fuse_req_t req, fuse_ino_t ino, size_t buf_max, off_t off, struct fuse_file_info *fi ) { do_readdir(req, ino, buf_max, off, 0, fi); } void fs_op_readdirplus ( fuse_req_t req, fuse_ino_t ino, size_t buf_max, off_t off, struct fuse_file_info *fi ) { do_readdir(req, ino, buf_max, off, BOOKMARK_FLAG(LIST_WITHSTAT), fi); } void fs_op_release ( fuse_req_t req, fuse_ino_t ino, struct fuse_file_info *fi ) { int status = -EIO; switch (INODE_SUBSYS_TYPE(ino)) { case SUBSYS_TYPE_BOOKMARK: status = bm_free(INODE_SUBSYS_ID(ino), fi); break; } send_reply(err, req, -status); } void fs_op_releasedir ( fuse_req_t req, fuse_ino_t ino, struct fuse_file_info *fi ) { int status = -EIO; void *cookie = uint2ptr(fi->fh); switch (INODE_SUBSYS_TYPE(ino)) { case SUBSYS_TYPE_INTERNAL: status = intfs_freedir(INODE_SUBSYS_ID(ino), ino, cookie); break; case SUBSYS_TYPE_BOOKMARK: case SUBSYS_TYPE_TAG: status = bm_freedir(INODE_SUBSYS_ID(ino), ino, cookie); break; } send_reply(err, req, -status); } void fs_op_rename ( fuse_req_t req, fuse_ino_t parent, char const *name, fuse_ino_t new_parent, char const *new_name, unsigned flags ) { int status = -EINVAL; uint32_t rename_flags = 0; #if defined(__linux__) && defined(_GNU_SOURCE) if (flags & RENAME_NOREPLACE) { rename_flags |= BOOKMARKFS_BOOKMARK_RENAME_NOREPLACE; flags &= ~RENAME_NOREPLACE; } #endif if (flags != 0) { goto end; } status = -EPERM; uint64_t parent_id, new_parent_id; int subsys = subsys_type(parent, &parent_id); if (subsys != subsys_type(new_parent, &new_parent_id)) { goto end; } switch (subsys) { case SUBSYS_TYPE_TAG: rename_flags |= BOOKMARKFS_BOOKMARK_TYPE(TAG); // fallthrough case SUBSYS_TYPE_BOOKMARK: status = bm_rename(parent_id, name, new_parent_id, new_name, rename_flags); break; case SUBSYS_TYPE_KEYWORD: rename_flags |= BOOKMARKFS_BOOKMARK_TYPE(KEYWORD); status = bm_rename(0, name, 0, new_name, rename_flags); break; } end: send_reply(err, req, -status); } void fs_op_removexattr ( fuse_req_t req, fuse_ino_t ino, char const *name ) { int status = -ENOATTR; switch (INODE_SUBSYS_TYPE(ino)) { case SUBSYS_TYPE_BOOKMARK: status = bm_rmxattr(INODE_SUBSYS_ID(ino), name); break; } send_reply(err, req, -status); } void fs_op_rmdir ( fuse_req_t req, fuse_ino_t parent, char const *name ) { do_delete(req, parent, name, BOOKMARK_FLAG(DELETE_DIR)); } void fs_op_setattr ( fuse_req_t req, fuse_ino_t ino, struct stat *stat_buf, int mask, struct fuse_file_info *fi ) { int status = -EPERM; bool is_tag = false; switch (INODE_SUBSYS_TYPE(ino)) { case SUBSYS_TYPE_TAG: is_tag = true; // fallthrough case SUBSYS_TYPE_BOOKMARK: status = bm_setattr(INODE_SUBSYS_ID(ino), mask, is_tag, fi, stat_buf); break; } if (status < 0) { send_reply(err, req, -status); } send_reply(attr, req, stat_buf, FS_ENTRY_TIMEOUT_SECS); } void fs_op_setxattr ( fuse_req_t req, fuse_ino_t ino, char const *name, char const *val, size_t val_len, int UNUSED_VAR(flags) ) { int status = -ENOATTR; switch (INODE_SUBSYS_TYPE(ino)) { case SUBSYS_TYPE_BOOKMARK: status = bm_setxattr(INODE_SUBSYS_ID(ino), name, val, val_len); break; } send_reply(err, req, -status); } void fs_op_unlink ( fuse_req_t req, fuse_ino_t parent, char const *name ) { do_delete(req, parent, name, 0); } void fs_op_write ( fuse_req_t req, fuse_ino_t ino, char const *buf, size_t buf_len, off_t off, struct fuse_file_info *fi ) { debug_assert(off >= 0); int status = -EIO; struct fs_file_handle *fh = uint2ptr(fi->fh); switch (INODE_SUBSYS_TYPE(ino)) { case SUBSYS_TYPE_BOOKMARK: status = bm_write(req, fi->flags, buf, buf_len, off, fh); break; } if (status < 0) { send_reply(err, req, -status); return; } }