mirror of
https://git.sr.ht/~cismonx/bookmarkfs
synced 2025-06-07 19:58:50 +00:00
2728 lines
74 KiB
C
2728 lines
74 KiB
C
/**
|
|
* bookmarkfs/src/backend_chromium.c
|
|
*
|
|
* Chromium backend for BookmarkFS.
|
|
* ----
|
|
*
|
|
* Copyright (C) 2024 CismonX <admin@cismon.net>
|
|
*
|
|
* 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 <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
# include "config.h"
|
|
#endif
|
|
|
|
#include <errno.h>
|
|
#include <inttypes.h>
|
|
#include <limits.h>
|
|
#include <signal.h>
|
|
#include <stdbool.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include <fcntl.h>
|
|
#include <iconv.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
|
|
#ifdef BOOKMARKFS_BACKEND_CHROMIUM_WRITE
|
|
# include <nettle/base16.h>
|
|
# include <nettle/md5.h>
|
|
#endif
|
|
|
|
#include "backend.h"
|
|
#include "backend_util.h"
|
|
#include "hash.h"
|
|
#include "hashmap.h"
|
|
#include "ioctl.h"
|
|
#include "json.h"
|
|
#include "lib.h"
|
|
#include "macros.h"
|
|
#include "prng.h"
|
|
#include "sandbox.h"
|
|
#include "uuid.h"
|
|
#include "version.h"
|
|
#include "watcher.h"
|
|
#include "xstd.h"
|
|
|
|
#if defined(SIZEOF_TIME_T) && (SIZEOF_TIME_T != 8)
|
|
# error "64-bit time_t is required"
|
|
#endif
|
|
|
|
#define BACKEND_FILENAME_GUID ( 1u << 16 )
|
|
|
|
// Chromium uses Windows FILETIME epoch instead of Unix epoch.
|
|
//
|
|
// See Chromium source code: /base/time/time.h
|
|
// (`base::Time::kTimeTToMicrosecondsOffset`)
|
|
#define EPOCH_DIFF ( (time_t)((1970 - 1601) * 365 + 89) * 24 * 3600 )
|
|
|
|
#define BOOKMARKS_ROOT_ID 0
|
|
|
|
enum {
|
|
ATTR_KEY_NULL,
|
|
ATTR_KEY_DATE_ADDED,
|
|
ATTR_KEY_GUID,
|
|
ATTR_KEY_TITLE,
|
|
};
|
|
|
|
enum dirty_level {
|
|
DIRTY_LEVEL_NONE,
|
|
DIRTY_LEVEL_METADATA,
|
|
DIRTY_LEVEL_DATA,
|
|
};
|
|
|
|
typedef int (node_iter_func) (
|
|
void *iter_data,
|
|
json_t *node,
|
|
json_t *children,
|
|
void **parent_data_ptr
|
|
);
|
|
|
|
struct assocmap_key {
|
|
uint64_t parent_id;
|
|
char const *name;
|
|
size_t name_len;
|
|
};
|
|
|
|
struct node_entry {
|
|
uint64_t id;
|
|
uint64_t parent_id;
|
|
uint8_t guid[UUID_LEN];
|
|
|
|
char const *name;
|
|
size_t name_len;
|
|
|
|
json_t *node;
|
|
json_t *children;
|
|
};
|
|
|
|
struct backend_ctx {
|
|
struct watcher *watcher;
|
|
struct hashmap *id_map;
|
|
struct hashmap *assoc_map;
|
|
struct hashmap *guid_map;
|
|
|
|
json_t *store;
|
|
json_t *checksum;
|
|
json_t *roots;
|
|
json_t *fake_root;
|
|
|
|
#ifdef BOOKMARKFS_BACKEND_CHROMIUM_WRITE
|
|
uint64_t max_id;
|
|
iconv_t cd;
|
|
enum dirty_level dirty;
|
|
#endif /* defined(BOOKMARKFS_BACKEND_CHROMIUM_WRITE) */
|
|
|
|
int dirfd;
|
|
char const *name;
|
|
|
|
uint32_t flags;
|
|
uint32_t watcher_flags;
|
|
};
|
|
|
|
struct bookmark_gcookie {
|
|
json_t *store;
|
|
json_t *checksum;
|
|
};
|
|
|
|
struct bookmark_lcookie {
|
|
json_t *children;
|
|
size_t idx; // fsck only
|
|
};
|
|
|
|
struct build_node_ctx {
|
|
uint8_t guid[UUID_LEN];
|
|
unsigned long hashcode;
|
|
|
|
struct bookmarkfs_bookmark_stat *stat_buf;
|
|
};
|
|
|
|
#ifdef BOOKMARKFS_BACKEND_CHROMIUM_WRITE
|
|
struct chksum_iter_ctx {
|
|
struct md5_ctx mdctx;
|
|
iconv_t cd;
|
|
char *buf;
|
|
size_t buf_len;
|
|
};
|
|
#endif
|
|
|
|
struct idmap_iter_ctx {
|
|
struct hashmap *id_map;
|
|
struct hashmap *guid_map;
|
|
struct hashmap *assoc_map;
|
|
#ifdef BOOKMARKFS_BACKEND_CHROMIUM_WRITE
|
|
uint64_t max_id;
|
|
#endif
|
|
};
|
|
|
|
struct lookup_ctx {
|
|
unsigned long hashcode;
|
|
unsigned long entry_id;
|
|
uint8_t guid[UUID_LEN];
|
|
};
|
|
|
|
struct node_iter {
|
|
json_t *children;
|
|
size_t idx;
|
|
void *parent_data;
|
|
};
|
|
|
|
struct parsed_mntopts {
|
|
uint32_t watcher_flags;
|
|
uint32_t other_flags;
|
|
};
|
|
|
|
struct parsed_mkfsopts {
|
|
struct timespec btime;
|
|
};
|
|
|
|
// Forward declaration start
|
|
#ifdef BOOKMARKFS_BACKEND_CHROMIUM_WRITE
|
|
static int build_node (struct backend_ctx *, json_t *, char const **,
|
|
size_t, bool, struct build_node_ctx *);
|
|
static int build_node_guid (json_t *, struct hashmap const *, uint8_t *,
|
|
unsigned long *);
|
|
static int build_node_id (json_t *, uint64_t *);
|
|
static int chksum_iter_cb (void *, json_t *, json_t *, void **);
|
|
static int chksum_root (struct backend_ctx *, char *);
|
|
static int chksum_utf16 (struct chksum_iter_ctx *, char const *, size_t);
|
|
static int fsck_apply (struct backend_ctx *, uint64_t,
|
|
struct bookmarkfs_fsck_data const *,
|
|
bookmarkfs_bookmark_fsck_cb *, void *);
|
|
static int init_iconv (iconv_t *);
|
|
static int node_mtime_now (json_t *, json_t **);
|
|
static int parse_mkfsopts (struct bookmarkfs_conf_opt const *,
|
|
struct parsed_mkfsopts *);
|
|
static int store_new (struct timespec *, json_t **);
|
|
static int store_save (struct backend_ctx *);
|
|
static void update_guid (struct node_entry *, struct hashmap *,
|
|
unsigned long, uint8_t *, unsigned long);
|
|
static int update_node_ts (json_t *, struct timespec *);
|
|
#endif /* defined(BOOKMARKFS_BACKEND_CHROMIUM_WRITE) */
|
|
|
|
static int assocmap_comp (union hashmap_key, void const *);
|
|
static unsigned long
|
|
assocmap_hash (void const *);
|
|
static int build_maps (struct backend_ctx *);
|
|
static int build_ts (struct timespec *, char *, size_t);
|
|
static int build_tsnode (struct timespec *, json_t **);
|
|
static void free_bgcookie (struct bookmark_gcookie *);
|
|
static void free_blcookie (struct bookmark_lcookie *);
|
|
static void free_entry_cb (void *, void *);
|
|
static void free_maps (struct hashmap *, struct hashmap *,
|
|
struct hashmap *);
|
|
static int fsck_next (struct backend_ctx const *, uint64_t, json_t *,
|
|
size_t *, bookmarkfs_bookmark_fsck_cb *, void *);
|
|
static int get_attr_type (char const *, uint32_t);
|
|
static int get_attr_val (json_t const *, char const *, uint32_t, json_t **);
|
|
static int guidmap_comp (union hashmap_key, void const *);
|
|
static unsigned long
|
|
guidmap_hash (void const *);
|
|
static unsigned long
|
|
hash_assoc (uint64_t, char const *, size_t);
|
|
static int idmap_comp (union hashmap_key, void const *);
|
|
static unsigned long
|
|
idmap_hash (void const *);
|
|
static int init_watcher (struct backend_ctx *);
|
|
static int iter_node (json_t *, struct node_iter *, size_t,
|
|
node_iter_func *, void *, void *);
|
|
static int iter_roots (json_t *, node_iter_func *, void *, void *);
|
|
static struct node_entry *
|
|
lookup_assoc (struct hashmap const *, uint64_t, char const *,
|
|
size_t, unsigned long *, unsigned long *);
|
|
static struct node_entry *
|
|
lookup_guid (struct hashmap const *, uint8_t const *,
|
|
unsigned long *, unsigned long *);
|
|
static struct node_entry *
|
|
lookup_id (struct hashmap const *, uint64_t, unsigned long *,
|
|
unsigned long *);
|
|
static int lookup_name (struct backend_ctx const *, uint64_t, char const *,
|
|
size_t, struct lookup_ctx *, struct node_entry **);
|
|
static int maps_iter_cb (void *, json_t *, json_t *, void **);
|
|
static int parse_entry (struct backend_ctx const *, json_t const *, off_t,
|
|
bool, struct bookmarkfs_bookmark_entry *);
|
|
static int parse_id (json_t const *, uint64_t *);
|
|
static int parse_guid (char const *, size_t, uint8_t *);
|
|
static int parse_mntopts (struct bookmarkfs_conf_opt const *, uint32_t,
|
|
struct parsed_mntopts *);
|
|
static int parse_stats (struct node_entry const *,
|
|
struct bookmarkfs_bookmark_stat *);
|
|
static int parse_ts (char const *, size_t, struct timespec *);
|
|
static void print_help (uint32_t);
|
|
static void print_version (void);
|
|
static int store_load (struct backend_ctx *);
|
|
// Forward declaration end
|
|
|
|
#ifdef BOOKMARKFS_BACKEND_CHROMIUM_WRITE
|
|
|
|
static int
|
|
build_node (
|
|
struct backend_ctx *ctx,
|
|
json_t *node,
|
|
char const **name,
|
|
size_t name_len,
|
|
bool is_dir,
|
|
struct build_node_ctx *bctx
|
|
) {
|
|
json_t *type;
|
|
if (is_dir) {
|
|
json_object_sset_new(node, "children", json_array());
|
|
type = json_sstring("folder");
|
|
} else {
|
|
json_object_sset_new(node, "url", json_sstring(""));
|
|
type = json_sstring("url");
|
|
}
|
|
json_object_sset_new(node, "type", type);
|
|
|
|
struct timespec ts = { .tv_nsec = UTIME_NOW };
|
|
json_t *date_added = NULL;
|
|
if (unlikely(0 != build_tsnode(&ts, &date_added))) {
|
|
return -EIO;
|
|
}
|
|
json_object_sset_new(node, "date_added", date_added);
|
|
json_object_sset_new(node, "date_last_used", json_sstring("0"));
|
|
|
|
if (unlikely(0 != build_node_id(node, &ctx->max_id))) {
|
|
return -EIO;
|
|
}
|
|
|
|
json_t *name_node = json_stringn(*name, name_len);
|
|
if (name_node == NULL) {
|
|
return -EPERM;
|
|
}
|
|
json_object_sset_new(node, "name", name_node);
|
|
*name = json_string_value(name_node);
|
|
|
|
if (ctx->flags & BACKEND_FILENAME_GUID) {
|
|
// No need to validate. Already done in lookup_name().
|
|
json_object_sset_copy(node, "guid", name_node);
|
|
} else {
|
|
if (unlikely(0 != build_node_guid(node, ctx->guid_map, bctx->guid,
|
|
&bctx->hashcode))
|
|
) {
|
|
return -EIO;
|
|
}
|
|
}
|
|
|
|
if (bctx->stat_buf != NULL) {
|
|
*bctx->stat_buf = (struct bookmarkfs_bookmark_stat) {
|
|
.id = ctx->max_id,
|
|
.value_len = is_dir ? -1 : 0,
|
|
.atime = ts,
|
|
.mtime = ts,
|
|
};
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
build_node_guid (
|
|
json_t *node,
|
|
struct hashmap const *guid_map,
|
|
uint8_t *guid,
|
|
unsigned long *hashcode_ptr
|
|
) {
|
|
do {
|
|
uuid_generate_random(guid);
|
|
|
|
union hashmap_key key = { .ptr = guid };
|
|
unsigned long hashcode = hash_digest(guid, UUID_LEN);
|
|
if (unlikely(NULL != hashmap_search(guid_map, key, hashcode, NULL))) {
|
|
continue;
|
|
}
|
|
|
|
if (hashcode_ptr != NULL) {
|
|
*hashcode_ptr = hashcode;
|
|
}
|
|
} while (0);
|
|
|
|
char guid_str_buf[UUID_HEX_LEN];
|
|
uuid_bin2hex(guid_str_buf, guid);
|
|
|
|
json_t *guid_node = json_stringn_nocheck(guid_str_buf, UUID_HEX_LEN);
|
|
json_object_sset_new(node, "guid", guid_node);
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
build_node_id (
|
|
json_t *node,
|
|
uint64_t *max_id_ptr
|
|
) {
|
|
uint64_t id_val = *max_id_ptr;
|
|
if (unlikely(++id_val > BOOKMARKFS_MAX_ID)) {
|
|
return -1;
|
|
}
|
|
|
|
char id_buf[24];
|
|
int nbytes = snprintf(id_buf, sizeof(id_buf), "%" PRIu64, id_val);
|
|
if (unlikely(nbytes < 0 || (size_t)nbytes >= sizeof(id_buf))) {
|
|
return -1;
|
|
}
|
|
json_object_sset_new(node, "id", json_stringn_nocheck(id_buf, nbytes));
|
|
|
|
*max_id_ptr = id_val;
|
|
return 0;
|
|
}
|
|
|
|
// See Chromium source code: /components/bookmarks/browser/bookmark_codec.cc
|
|
static int
|
|
chksum_iter_cb (
|
|
void *iter_data,
|
|
json_t *node,
|
|
json_t *children,
|
|
void **UNUSED_VAR(parent_data_ptr)
|
|
) {
|
|
struct chksum_iter_ctx *ctx = iter_data;
|
|
|
|
json_t const *id_node = json_object_sget(node, "id");
|
|
char const *id = json_string_value(id_node);
|
|
size_t id_len = json_string_length(id_node);
|
|
debug_assert(id != NULL);
|
|
md5_update(&ctx->mdctx, id_len, (uint8_t const *)id);
|
|
|
|
json_t const *name_node = json_object_sget(node, "name");
|
|
char const *name = json_string_value(name_node);
|
|
size_t name_len = json_string_length(name_node);
|
|
debug_assert(name != NULL);
|
|
if (unlikely(0 != chksum_utf16(ctx, name, name_len))) {
|
|
return -1;
|
|
}
|
|
|
|
if (children != NULL) {
|
|
md5_update(&ctx->mdctx, strlen("folder"), (uint8_t const *)"folder");
|
|
} else {
|
|
md5_update(&ctx->mdctx, strlen("url"), (uint8_t const *)"url");
|
|
|
|
json_t const *url_node = json_object_sget(node, "url");
|
|
char const *url = json_string_value(url_node);
|
|
size_t url_len = json_string_length(url_node);
|
|
debug_assert(url != NULL);
|
|
md5_update(&ctx->mdctx, url_len, (uint8_t const *)url);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
chksum_root (
|
|
struct backend_ctx *ctx,
|
|
char *buf
|
|
) {
|
|
if (ctx->cd == (iconv_t)-1) {
|
|
if (unlikely(0 != init_iconv(&ctx->cd))) {
|
|
return -1;
|
|
}
|
|
}
|
|
int status = -1;
|
|
|
|
struct chksum_iter_ctx iter_ctx = {
|
|
.cd = ctx->cd,
|
|
};
|
|
md5_init(&iter_ctx.mdctx);
|
|
if (0 != iter_roots(ctx->roots, chksum_iter_cb, &iter_ctx, NULL)) {
|
|
goto end;
|
|
}
|
|
|
|
uint8_t digest[MD5_DIGEST_SIZE];
|
|
md5_digest(&iter_ctx.mdctx, MD5_DIGEST_SIZE, digest);
|
|
|
|
base16_encode_update(buf, MD5_DIGEST_SIZE, digest);
|
|
status = 0;
|
|
|
|
end:
|
|
free(iter_ctx.buf);
|
|
return status;
|
|
}
|
|
|
|
static int
|
|
chksum_utf16 (
|
|
struct chksum_iter_ctx *ctx,
|
|
char const *str,
|
|
size_t str_len
|
|
) {
|
|
// A UTF-16 string converted from UTF-8 is at most 2x the length.
|
|
size_t buf_len = str_len * 2 + 1;
|
|
if (ctx->buf_len < buf_len) {
|
|
free(ctx->buf);
|
|
ctx->buf = xmalloc(buf_len);
|
|
ctx->buf_len = buf_len;
|
|
}
|
|
|
|
// A sane iconv() implementation should not write to the input buffer.
|
|
char *in_buf = (char *)str;
|
|
char *out_buf = ctx->buf;
|
|
if ((size_t)-1 == iconv(ctx->cd, &in_buf, &str_len, &out_buf, &buf_len)) {
|
|
log_printf("iconv(): %s", xstrerror(errno));
|
|
return -1;
|
|
}
|
|
md5_update(&ctx->mdctx, out_buf - ctx->buf, (uint8_t const *)ctx->buf);
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
fsck_apply (
|
|
struct backend_ctx *ctx,
|
|
uint64_t parent_id,
|
|
struct bookmarkfs_fsck_data const *fsck_data,
|
|
bookmarkfs_bookmark_fsck_cb *callback,
|
|
void *user_data
|
|
) {
|
|
uint64_t id = fsck_data->id;
|
|
uint64_t extra = 0;
|
|
char const *name = fsck_data->name;
|
|
int result;
|
|
|
|
struct node_entry *entry = lookup_id(ctx->id_map, id, NULL, NULL);
|
|
if (entry == NULL || entry->parent_id != parent_id) {
|
|
return -ENOENT;
|
|
}
|
|
if (entry->name != NULL) {
|
|
// Given ID refers to a valid entry. No fix shall be performed.
|
|
return 0;
|
|
}
|
|
|
|
size_t name_len = strnlen(name, sizeof(fsck_data->name));
|
|
if (0 != validate_filename_fsck(name, name_len, &result, &extra)) {
|
|
goto callback;
|
|
}
|
|
|
|
struct hashmap *assoc_map = ctx->assoc_map;
|
|
union hashmap_key key_assoc = {
|
|
.ptr = &(struct assocmap_key) {
|
|
.parent_id = parent_id,
|
|
.name = name,
|
|
.name_len = name_len,
|
|
},
|
|
};
|
|
unsigned long hashcode_assoc = hash_assoc(id, name, name_len);
|
|
if (NULL != hashmap_search(assoc_map, key_assoc, hashcode_assoc, NULL)) {
|
|
extra = entry->id;
|
|
result = BOOKMARKFS_FSCK_RESULT_NAME_DUPLICATE;
|
|
goto callback;
|
|
}
|
|
|
|
json_t *name_node = json_stringn(name, name_len);
|
|
if (name_node == NULL) {
|
|
extra = BOOKMARKFS_NAME_INVALID_REASON_NOTUTF8;
|
|
result = BOOKMARKFS_FSCK_RESULT_NAME_INVALID;
|
|
goto callback;
|
|
}
|
|
json_object_sset_new(entry->node, "name", name_node);
|
|
ctx->dirty = DIRTY_LEVEL_DATA;
|
|
|
|
entry->name = json_string_value(name_node);
|
|
entry->name_len = name_len;
|
|
*hashmap_insert(assoc_map, key_assoc, hashcode_assoc) = entry;
|
|
return 0;
|
|
|
|
callback:
|
|
return callback(user_data, result, id, extra, name);
|
|
}
|
|
|
|
static int
|
|
init_iconv (
|
|
iconv_t *cd_ptr
|
|
) {
|
|
iconv_t cd = iconv_open("UTF-16LE", "UTF-8");
|
|
if (unlikely(cd == (iconv_t)-1)) {
|
|
log_printf("iconv_open(): %s", xstrerror(errno));
|
|
return -1;
|
|
}
|
|
*cd_ptr = cd;
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
node_mtime_now (
|
|
json_t *node,
|
|
json_t **tsnode_ptr
|
|
) {
|
|
struct timespec ts = { .tv_nsec = UTIME_NOW };
|
|
|
|
json_t *tsnode = NULL;
|
|
if (unlikely(0 != build_tsnode(&ts, &tsnode))) {
|
|
return -1;
|
|
}
|
|
json_object_sset_new(node, "date_modified", tsnode);
|
|
|
|
if (tsnode_ptr != NULL) {
|
|
*tsnode_ptr = tsnode;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
parse_mkfsopts (
|
|
struct bookmarkfs_conf_opt const *opts,
|
|
struct parsed_mkfsopts *parsed_opts
|
|
) {
|
|
BACKEND_OPT_START(opts)
|
|
BACKEND_OPT_KEY("date_added") {
|
|
BACKEND_OPT_VAL_START
|
|
char const *val = BACKEND_OPT_VAL_STR;
|
|
if (0 != parse_ts(val, strlen(val), &parsed_opts->btime)) {
|
|
return BACKEND_OPT_BAD_VAL();
|
|
}
|
|
}
|
|
BACKEND_OPT_END
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
store_new (
|
|
struct timespec *btime,
|
|
json_t **store_ptr
|
|
) {
|
|
json_t *date_added = NULL;
|
|
if (unlikely(0 != build_tsnode(btime, &date_added))) {
|
|
return -1;
|
|
}
|
|
json_t *children = json_array();
|
|
json_t *type = json_sstring("folder");
|
|
json_t *zero_str = json_sstring("0");
|
|
|
|
json_t *roots = json_object();
|
|
#define INIT_ROOT_NODE(roots, key, id, name, guid) \
|
|
{ \
|
|
json_t *root_ = json_object(); \
|
|
json_object_sset_new(root_, "id", json_sstring(id)); \
|
|
json_object_sset_new(root_, "guid", json_sstring(guid)); \
|
|
json_object_sset_new(root_, "name", json_sstring(name)); \
|
|
json_object_sset(root_, "children", children); \
|
|
json_object_sset(root_, "date_added", date_added); \
|
|
json_object_sset(root_, "date_last_used", zero_str); \
|
|
json_object_sset(root_, "date_modified", zero_str); \
|
|
json_object_sset(root_, "type", type); \
|
|
json_object_sset_new(roots, key, root_); \
|
|
}
|
|
INIT_ROOT_NODE(roots, "bookmark_bar", "1", "Bookmarks bar",
|
|
"0bc5d13f-2cba-5d74-951f-3f233fe6c908");
|
|
INIT_ROOT_NODE(roots, "other", "2", "Other bookmarks",
|
|
"82b081ec-3dd3-529c-8475-ab6c344590dd");
|
|
INIT_ROOT_NODE(roots, "synced", "3", "Mobile bookmarks",
|
|
"4cf2e351-0e85-532b-bb37-df045d8f8d0f");
|
|
#undef INIT_ROOT_NODE
|
|
|
|
json_t *store = json_object();
|
|
json_object_sset_new(store, "checksum",
|
|
json_sstring("1e54fbb25d92a354f7aeaf576726429e"));
|
|
json_object_sset_new(store, "roots", roots);
|
|
json_object_sset_new(store, "version", json_integer(1));
|
|
|
|
json_decref(date_added);
|
|
json_decref(children);
|
|
json_decref(type);
|
|
json_decref(zero_str);
|
|
|
|
*store_ptr = store;
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
store_save (
|
|
struct backend_ctx *ctx
|
|
) {
|
|
if (ctx->dirty == DIRTY_LEVEL_NONE) {
|
|
return 0;
|
|
}
|
|
|
|
if (ctx->dirty > DIRTY_LEVEL_METADATA) {
|
|
char buf[MD5_DIGEST_SIZE * 2];
|
|
if (unlikely(0 != chksum_root(ctx, buf))) {
|
|
return -1;
|
|
}
|
|
|
|
json_t *checksum = json_stringn_nocheck(buf, MD5_DIGEST_SIZE * 2);
|
|
json_object_sset_new(ctx->store, "checksum", checksum);
|
|
}
|
|
|
|
size_t flags = JSON_COMPACT;
|
|
if (0 != json_dump_file_at(ctx->store, ctx->dirfd, ctx->name, flags)) {
|
|
return -1;
|
|
}
|
|
|
|
watcher_poll(ctx->watcher);
|
|
ctx->dirty = DIRTY_LEVEL_NONE;
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
update_guid (
|
|
struct node_entry *entry,
|
|
struct hashmap *guid_map,
|
|
unsigned long old_entry_id,
|
|
uint8_t *guid,
|
|
unsigned long hashcode
|
|
) {
|
|
hashmap_entry_delete(guid_map, entry, old_entry_id);
|
|
|
|
union hashmap_key key = { .ptr = guid };
|
|
*hashmap_insert(guid_map, key, hashcode) = entry;
|
|
|
|
memcpy(entry->guid, guid, UUID_LEN);
|
|
}
|
|
|
|
static int
|
|
update_node_ts (
|
|
json_t *node,
|
|
struct timespec *times
|
|
) {
|
|
json_t *ts_node = json_object_sget(node, "date_last_used");
|
|
if (unlikely(0 != build_tsnode(×[0], &ts_node))) {
|
|
return -1;
|
|
}
|
|
|
|
ts_node = json_object_sget(node, "date_modified");
|
|
if (ts_node == NULL) {
|
|
ts_node = json_object_sget(node, "date_added");
|
|
if (unlikely(ts_node == NULL)) {
|
|
return -1;
|
|
}
|
|
json_object_sset_copy(node, "date_modified", ts_node);
|
|
}
|
|
if (unlikely(0 != build_tsnode(×[1], &ts_node))) {
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
#endif /* defined(BOOKMARKFS_BACKEND_CHROMIUM_WRITE) */
|
|
|
|
static int
|
|
assocmap_comp (
|
|
union hashmap_key key,
|
|
void const *entry
|
|
) {
|
|
struct assocmap_key const *k = key.ptr;
|
|
struct node_entry const *e = entry;
|
|
|
|
if (k->parent_id != e->parent_id) {
|
|
return -1;
|
|
}
|
|
if (k->name_len != e->name_len) {
|
|
return -1;
|
|
}
|
|
return memcmp(k->name, e->name, e->name_len);
|
|
}
|
|
|
|
static unsigned long
|
|
assocmap_hash (
|
|
void const *entry
|
|
) {
|
|
struct node_entry const *e = entry;
|
|
|
|
return hash_assoc(e->parent_id, e->name, e->name_len);
|
|
}
|
|
|
|
static int
|
|
build_maps (
|
|
struct backend_ctx *ctx
|
|
) {
|
|
json_t *ts_node = NULL;
|
|
struct timespec now = { .tv_nsec = UTIME_NOW };
|
|
if (unlikely(0 != build_tsnode(&now, &ts_node))) {
|
|
return -1;
|
|
}
|
|
json_t *root = json_object();
|
|
json_t *children = json_array();
|
|
json_object_sset_new(root, "children", children);
|
|
json_object_sset_new(root, "date_added", ts_node);
|
|
json_object_sset_copy(root, "date_last_used", ts_node);
|
|
json_object_sset_copy(root, "date_modified", ts_node);
|
|
|
|
struct hashmap *id_map = hashmap_create(idmap_comp, idmap_hash);
|
|
struct hashmap *guid_map = hashmap_create(guidmap_comp, guidmap_hash);
|
|
struct hashmap *assoc_map = NULL;
|
|
if (!(ctx->flags & BACKEND_FILENAME_GUID)) {
|
|
assoc_map = hashmap_create(assocmap_comp, assocmap_hash);
|
|
}
|
|
|
|
uint64_t root_id = BOOKMARKS_ROOT_ID;
|
|
struct node_entry *root_entry = xmalloc(sizeof(*root_entry));
|
|
*root_entry = (struct node_entry) {
|
|
.id = root_id,
|
|
.parent_id = root_id,
|
|
.guid = {
|
|
// `bookmarks::kRootNodeUuid`
|
|
0x25, 0x09, 0xa7, 0xdc, 0x21, 0x5d, 0x52, 0xf7,
|
|
0xa4, 0x29, 0x8d, 0x80, 0x43, 0x1c, 0x6c, 0x75,
|
|
},
|
|
.name = "",
|
|
.node = root,
|
|
.children = children,
|
|
};
|
|
union hashmap_key key_id = { .u64 = root_id };
|
|
unsigned long hashcode_id = hash_digest(&root_id, sizeof(root_id));
|
|
*hashmap_insert(id_map, key_id, hashcode_id) = root_entry;
|
|
|
|
union hashmap_key key_guid = { .ptr = root_entry->guid };
|
|
unsigned long hashcode_guid = hash_digest(root_entry->guid, UUID_LEN);
|
|
*hashmap_insert(guid_map, key_guid, hashcode_guid) = root_entry;
|
|
|
|
struct idmap_iter_ctx iter_ctx = {
|
|
.id_map = id_map,
|
|
.guid_map = guid_map,
|
|
.assoc_map = assoc_map,
|
|
};
|
|
if (0 != iter_roots(ctx->roots, maps_iter_cb, &iter_ctx, root_entry)) {
|
|
goto fail;
|
|
}
|
|
|
|
free_maps(ctx->id_map, ctx->assoc_map, ctx->guid_map);
|
|
ctx->id_map = id_map;
|
|
ctx->guid_map = guid_map;
|
|
ctx->assoc_map = assoc_map;
|
|
#ifdef BOOKMARKFS_BACKEND_CHROMIUM_WRITE
|
|
ctx->max_id = iter_ctx.max_id;
|
|
#endif
|
|
json_decref(ctx->fake_root);
|
|
ctx->fake_root = root;
|
|
return 0;
|
|
|
|
fail:
|
|
json_decref(root);
|
|
free_maps(id_map, assoc_map, guid_map);
|
|
return -1;
|
|
}
|
|
|
|
static int
|
|
build_ts (
|
|
struct timespec *ts,
|
|
char *buf,
|
|
size_t buf_len
|
|
) {
|
|
if (ts->tv_nsec == UTIME_OMIT) {
|
|
return 0;
|
|
}
|
|
if (ts->tv_nsec == UTIME_NOW) {
|
|
if (unlikely(0 != clock_gettime(CLOCK_REALTIME, ts))) {
|
|
log_printf("clock_gettime(): %s", xstrerror(errno));
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
// XXX: May overflow if system time is badly wrong,
|
|
// but don't bother to check.
|
|
time_t secs = ts->tv_sec + EPOCH_DIFF;
|
|
long nsecs = ts->tv_nsec;
|
|
|
|
long microsecs = nsecs / 1000;
|
|
if (unlikely(microsecs >= 1000000)) {
|
|
secs += microsecs / 1000000;
|
|
microsecs %= 1000000;
|
|
}
|
|
|
|
int nbytes = snprintf(buf, buf_len, "%" PRIuMAX "%06ld",
|
|
(uintmax_t)secs, microsecs);
|
|
if (unlikely(nbytes < 0) || unlikely((size_t)nbytes >= buf_len)) {
|
|
return -1;
|
|
}
|
|
return nbytes;
|
|
}
|
|
|
|
static int
|
|
build_tsnode (
|
|
struct timespec *ts,
|
|
json_t **node_ptr
|
|
) {
|
|
char buf[32];
|
|
int nbytes = build_ts(ts, buf, 32);
|
|
if (unlikely(nbytes < 0)) {
|
|
return -1;
|
|
}
|
|
if (nbytes == 0) {
|
|
// UTIME_OMIT
|
|
return 0;
|
|
}
|
|
|
|
json_t *node = *node_ptr;
|
|
if (node == NULL) {
|
|
*node_ptr = json_stringn_nocheck(buf, nbytes);
|
|
} else {
|
|
json_string_setn_nocheck(node, buf, nbytes);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
free_bgcookie (
|
|
struct bookmark_gcookie *cookie
|
|
) {
|
|
json_decref(cookie->checksum);
|
|
free(cookie);
|
|
}
|
|
|
|
static void
|
|
free_blcookie (
|
|
struct bookmark_lcookie *cookie
|
|
) {
|
|
json_decref(cookie->children);
|
|
free(cookie);
|
|
}
|
|
|
|
static void
|
|
free_entry_cb (
|
|
void *entry,
|
|
void *UNUSED_VAR(user_data)
|
|
) {
|
|
free(entry);
|
|
}
|
|
|
|
static void
|
|
free_maps (
|
|
struct hashmap *id_map,
|
|
struct hashmap *assoc_map,
|
|
struct hashmap *guid_map
|
|
) {
|
|
if (id_map != NULL) {
|
|
hashmap_foreach(id_map, free_entry_cb, NULL);
|
|
}
|
|
hashmap_destroy(id_map);
|
|
hashmap_destroy(assoc_map);
|
|
hashmap_destroy(guid_map);
|
|
}
|
|
|
|
static int
|
|
fsck_next (
|
|
struct backend_ctx const *ctx,
|
|
uint64_t parent_id,
|
|
json_t *children,
|
|
size_t *idx_ptr,
|
|
bookmarkfs_bookmark_fsck_cb *callback,
|
|
void *user_data
|
|
) {
|
|
int status = 0;
|
|
size_t idx = *idx_ptr;
|
|
do {
|
|
json_t const *child = json_array_get(children, idx++);
|
|
if (child == NULL) {
|
|
break;
|
|
}
|
|
|
|
uint64_t id;
|
|
debug_assert(0 == parse_id(child, &id));
|
|
struct node_entry const *entry
|
|
= lookup_id(ctx->id_map, id, NULL, NULL);
|
|
// Since `children` could be a copy, the corresponding entry
|
|
// may already be gone.
|
|
if (entry == NULL || entry->name != NULL) {
|
|
continue;
|
|
}
|
|
|
|
json_t const *name_node = json_object_sget(child, "name");
|
|
char const *name = json_string_value(name_node);
|
|
size_t name_len = json_string_length(name_node);
|
|
debug_assert(name != NULL);
|
|
|
|
uint64_t extra;
|
|
int result;
|
|
if (0 == validate_filename_fsck(name, name_len, &result, &extra)) {
|
|
entry = lookup_assoc(ctx->assoc_map, parent_id, name, name_len,
|
|
NULL, NULL);
|
|
extra = BOOKMARKS_ROOT_ID;
|
|
// The duplicate entry may already be renamed.
|
|
if (entry != NULL) {
|
|
extra = entry->id;
|
|
}
|
|
result = BOOKMARKFS_FSCK_RESULT_NAME_DUPLICATE;
|
|
}
|
|
status = callback(user_data, result, id, extra, name);
|
|
} while (status == 0);
|
|
|
|
*idx_ptr = idx;
|
|
return status;
|
|
}
|
|
|
|
static int
|
|
get_attr_type (
|
|
char const *key,
|
|
uint32_t flags
|
|
) {
|
|
if (key == NULL) {
|
|
return ATTR_KEY_NULL;
|
|
}
|
|
if (0 == strcmp("date_added", key)) {
|
|
return ATTR_KEY_DATE_ADDED;
|
|
}
|
|
if (flags & BACKEND_FILENAME_GUID) {
|
|
if (0 == strcmp("title", key)) {
|
|
return ATTR_KEY_TITLE;
|
|
}
|
|
} else {
|
|
if (0 == strcmp("guid", key)) {
|
|
return ATTR_KEY_GUID;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
static int
|
|
get_attr_val (
|
|
json_t const *node,
|
|
char const *attr_key,
|
|
uint32_t flags,
|
|
json_t **value_ptr
|
|
) {
|
|
json_t *value;
|
|
|
|
int key_type = get_attr_type(attr_key, flags);
|
|
switch (key_type) {
|
|
case ATTR_KEY_NULL:
|
|
value = json_object_sget(node, "url");
|
|
break;
|
|
|
|
case ATTR_KEY_DATE_ADDED:
|
|
value = json_object_sget(node, "date_added");
|
|
break;
|
|
|
|
case ATTR_KEY_GUID:
|
|
value = json_object_sget(node, "guid");
|
|
break;
|
|
|
|
case ATTR_KEY_TITLE:
|
|
value = json_object_sget(node, "name");
|
|
break;
|
|
|
|
default:
|
|
return -ENOATTR;
|
|
}
|
|
if (value == NULL) {
|
|
if (unlikely(attr_key == NULL)) {
|
|
return -EIO;
|
|
}
|
|
return -EISDIR;
|
|
}
|
|
*value_ptr = value;
|
|
return key_type;
|
|
}
|
|
|
|
static int
|
|
guidmap_comp (
|
|
union hashmap_key key,
|
|
void const *entry
|
|
) {
|
|
struct node_entry const *e = entry;
|
|
|
|
return memcmp(key.ptr, e->guid, UUID_LEN);
|
|
}
|
|
|
|
static unsigned long
|
|
guidmap_hash (
|
|
void const *entry
|
|
) {
|
|
struct node_entry const *e = entry;
|
|
|
|
return hash_digest(e->guid, UUID_LEN);
|
|
}
|
|
|
|
static unsigned long
|
|
hash_assoc (
|
|
uint64_t parent_id,
|
|
char const *name,
|
|
size_t name_len
|
|
) {
|
|
struct iovec const bufv[] = {
|
|
{ .iov_base = &parent_id, .iov_len = sizeof(parent_id) },
|
|
{ .iov_base = (char *)name, .iov_len = name_len },
|
|
};
|
|
return hash_digestv(bufv, 2);
|
|
}
|
|
|
|
static int
|
|
idmap_comp (
|
|
union hashmap_key key,
|
|
void const *entry
|
|
) {
|
|
struct node_entry const *e = entry;
|
|
|
|
if (key.u64 != e->id) {
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static unsigned long
|
|
idmap_hash (
|
|
void const *entry
|
|
) {
|
|
struct node_entry const *e = entry;
|
|
|
|
return hash_digest(&e->id, sizeof(uint64_t));
|
|
}
|
|
|
|
static int
|
|
init_watcher (
|
|
struct backend_ctx *ctx
|
|
) {
|
|
sigset_t old, to_block;
|
|
sigemptyset(&to_block);
|
|
// Block these signals for the worker thread, otherwise libfuse
|
|
// session loop may not be promptly terminated upon signal receipt.
|
|
sigaddset(&to_block, SIGHUP);
|
|
sigaddset(&to_block, SIGINT);
|
|
sigaddset(&to_block, SIGTERM);
|
|
xassert(0 == pthread_sigmask(SIG_BLOCK, &to_block, &old));
|
|
|
|
ctx->watcher = watcher_create(ctx->dirfd, ctx->name, ctx->watcher_flags);
|
|
xassert(0 == pthread_sigmask(SIG_SETMASK, &old, NULL));
|
|
if (ctx->watcher == NULL) {
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
iter_node (
|
|
json_t *node,
|
|
struct node_iter *iter_stack,
|
|
size_t iter_stack_size,
|
|
node_iter_func *iter_cb,
|
|
void *iter_data,
|
|
void *parent_data
|
|
) {
|
|
struct node_iter *iter = iter_stack;
|
|
*iter = (struct node_iter) {
|
|
.parent_data = parent_data,
|
|
};
|
|
goto get_children;
|
|
|
|
while (1) {
|
|
node = json_array_get(iter->children, iter->idx++);
|
|
if (node == NULL) {
|
|
if (iter == iter_stack) {
|
|
break;
|
|
}
|
|
--iter;
|
|
continue;
|
|
}
|
|
|
|
get_children:
|
|
parent_data = iter->parent_data;
|
|
json_t *children = json_object_sget(node, "children");
|
|
if (0 != iter_cb(iter_data, node, children, &parent_data)) {
|
|
return -1;
|
|
}
|
|
|
|
if (children == NULL) {
|
|
continue;
|
|
}
|
|
if (++iter == iter_stack + iter_stack_size) {
|
|
log_printf("bookmark directory is too deeply nested (>%zu)",
|
|
iter_stack_size);
|
|
return -1;
|
|
}
|
|
*iter = (struct node_iter) {
|
|
.children = children,
|
|
.parent_data = parent_data,
|
|
};
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
iter_roots (
|
|
json_t *roots,
|
|
node_iter_func *iter_cb,
|
|
void *iter_data,
|
|
void *parent_data
|
|
) {
|
|
#define MAX_NODE_ITER_DEPTH 128
|
|
struct node_iter *iter_stack
|
|
= xmalloc(sizeof(struct node_iter) * MAX_NODE_ITER_DEPTH);
|
|
|
|
int status = -1;
|
|
json_object_foreach_iter(roots, iter) {
|
|
json_t *node = json_object_iter_value(iter);
|
|
|
|
status = iter_node(node, iter_stack, MAX_NODE_ITER_DEPTH, iter_cb,
|
|
iter_data, parent_data);
|
|
if (status != 0) {
|
|
break;
|
|
}
|
|
}
|
|
free(iter_stack);
|
|
return status;
|
|
}
|
|
|
|
static struct node_entry *
|
|
lookup_assoc (
|
|
struct hashmap const *assoc_map,
|
|
uint64_t parent_id,
|
|
char const *name,
|
|
size_t name_len,
|
|
unsigned long *hashcode_ptr,
|
|
unsigned long *entry_id_ptr
|
|
) {
|
|
union hashmap_key key = {
|
|
.ptr = &(struct assocmap_key) {
|
|
.parent_id = parent_id,
|
|
.name = name,
|
|
.name_len = name_len,
|
|
},
|
|
};
|
|
unsigned long hashcode = hash_assoc(parent_id, name, name_len);
|
|
if (hashcode_ptr != NULL) {
|
|
*hashcode_ptr = hashcode;
|
|
}
|
|
return hashmap_search(assoc_map, key, hashcode, entry_id_ptr);
|
|
}
|
|
|
|
static struct node_entry *
|
|
lookup_guid (
|
|
struct hashmap const *guid_map,
|
|
uint8_t const *guid,
|
|
unsigned long *hashcode_ptr,
|
|
unsigned long *entry_id_ptr
|
|
) {
|
|
union hashmap_key key = { .ptr = guid };
|
|
unsigned long hashcode = hash_digest(guid, UUID_LEN);
|
|
if (hashcode_ptr != NULL) {
|
|
*hashcode_ptr = hashcode;
|
|
}
|
|
|
|
return hashmap_search(guid_map, key, hashcode, entry_id_ptr);
|
|
}
|
|
|
|
static struct node_entry *
|
|
lookup_id (
|
|
struct hashmap const *id_map,
|
|
uint64_t id,
|
|
unsigned long *hashcode_ptr,
|
|
unsigned long *entry_id_ptr
|
|
) {
|
|
union hashmap_key key = { .u64 = id };
|
|
unsigned long hashcode = hash_digest(&id, sizeof(id));
|
|
if (hashcode_ptr != NULL) {
|
|
*hashcode_ptr = hashcode;
|
|
}
|
|
|
|
return hashmap_search(id_map, key, hashcode, entry_id_ptr);
|
|
}
|
|
|
|
static int
|
|
lookup_name (
|
|
struct backend_ctx const *ctx,
|
|
uint64_t parent_id,
|
|
char const *name,
|
|
size_t name_len,
|
|
struct lookup_ctx *lctx,
|
|
struct node_entry **entry_ptr
|
|
) {
|
|
unsigned long hashcode = 0;
|
|
unsigned long entry_id = 0;
|
|
struct node_entry *entry = NULL;
|
|
|
|
int status = -1;
|
|
if (ctx->flags & BACKEND_FILENAME_GUID) {
|
|
uint8_t guid_buf[UUID_LEN];
|
|
void *guid = guid_buf;
|
|
if (lctx != NULL) {
|
|
guid = lctx->guid;
|
|
}
|
|
|
|
if (unlikely(0 != parse_guid(name, name_len, guid))) {
|
|
goto end;
|
|
}
|
|
entry = lookup_guid(ctx->guid_map, guid, &hashcode, &entry_id);
|
|
if (entry == NULL) {
|
|
goto end;
|
|
}
|
|
// An entry with the same GUID is found in another directory.
|
|
if (entry->parent_id != parent_id) {
|
|
goto end;
|
|
}
|
|
} else {
|
|
entry = lookup_assoc(ctx->assoc_map, parent_id, name, name_len,
|
|
&hashcode, &entry_id);
|
|
if (entry == NULL) {
|
|
goto end;
|
|
}
|
|
}
|
|
status = 0;
|
|
|
|
end:
|
|
if (lctx != NULL) {
|
|
lctx->hashcode = hashcode;
|
|
lctx->entry_id = entry_id;
|
|
}
|
|
if (entry_ptr != NULL) {
|
|
*entry_ptr = entry;
|
|
}
|
|
return status;
|
|
}
|
|
|
|
static int
|
|
maps_iter_cb (
|
|
void *iter_data,
|
|
json_t *node,
|
|
json_t *children,
|
|
void **parent_data_ptr
|
|
) {
|
|
struct idmap_iter_ctx *ctx = iter_data;
|
|
struct node_entry *parent = *parent_data_ptr;
|
|
|
|
uint64_t id;
|
|
if (unlikely(0 != parse_id(node, &id))) {
|
|
return -1;
|
|
}
|
|
|
|
union hashmap_key key = { .u64 = id };
|
|
unsigned long hashcode = hash_digest(&id, sizeof(id));
|
|
if (unlikely(NULL != hashmap_search(ctx->id_map, key, hashcode, NULL))) {
|
|
log_printf("duplicate bookmark ID %" PRIu64, id);
|
|
return -1;
|
|
}
|
|
|
|
uint8_t guid[UUID_LEN];
|
|
json_t *guid_node = json_object_sget(node, "guid");
|
|
char const *guid_str = json_string_value(guid_node);
|
|
size_t guid_len = json_string_length(guid_node);
|
|
if (unlikely(0 != parse_guid(guid_str, guid_len, guid))) {
|
|
log_printf("bad bookmark GUID %s (ID: %" PRIu64 ")", guid_str, id);
|
|
return -1;
|
|
}
|
|
union hashmap_key key_guid = { .ptr = guid };
|
|
unsigned long hashcode_guid = hash_digest(guid, UUID_LEN);
|
|
if (NULL != hashmap_search(ctx->guid_map, key_guid, hashcode_guid, NULL)) {
|
|
log_printf("duplicate bookmark GUID %s (ID: %" PRIu64 ")",
|
|
guid_str, id);
|
|
return -1;
|
|
}
|
|
|
|
json_t *name_node = json_object_sget(node, "name");
|
|
char const *name = json_string_value(name_node);
|
|
size_t name_len = json_string_length(name_node);
|
|
if (unlikely(name == NULL)) {
|
|
log_printf("no name found for bookmark %" PRIu64, id);
|
|
return -1;
|
|
}
|
|
|
|
struct node_entry *entry = xmalloc(sizeof(*entry));
|
|
entry->id = id;
|
|
entry->parent_id = parent->id;
|
|
entry->name = NULL;
|
|
entry->node = node;
|
|
entry->children = children;
|
|
memcpy(entry->guid, guid, UUID_LEN);
|
|
*hashmap_insert(ctx->id_map, key, hashcode) = entry;
|
|
*hashmap_insert(ctx->guid_map, key_guid, hashcode_guid) = entry;
|
|
|
|
// bookmark bar, mobile bookmarks, other bookmarks, ...
|
|
if (parent->id == BOOKMARKS_ROOT_ID) {
|
|
json_array_append(parent->children, node);
|
|
}
|
|
struct hashmap *assoc_map = ctx->assoc_map;
|
|
// Do not check for name duplicates if using GUID as filename.
|
|
if (assoc_map == NULL) {
|
|
goto end;
|
|
}
|
|
// If the parent entry is ignored from assoc map,
|
|
// all children should also be ignored.
|
|
if (parent->name == NULL && parent->id != BOOKMARKS_ROOT_ID) {
|
|
goto end;
|
|
}
|
|
|
|
if (0 != validate_filename(name, name_len, NULL)) {
|
|
debug_printf("bad bookmark name '%s' (ID: %" PRIu64 ")", name, id);
|
|
goto end;
|
|
}
|
|
union hashmap_key key_assoc = {
|
|
.ptr = &(struct assocmap_key) {
|
|
.parent_id = parent->id,
|
|
.name = name,
|
|
.name_len = name_len,
|
|
},
|
|
};
|
|
unsigned long hashcode_assoc = hash_assoc(parent->id, name, name_len);
|
|
if (NULL != hashmap_search(assoc_map, key_assoc, hashcode_assoc, NULL)) {
|
|
debug_printf("duplicate bookmark name '%s' (ID: %" PRIu64 ")",
|
|
name, id);
|
|
goto end;
|
|
}
|
|
|
|
entry->name = name;
|
|
entry->name_len = name_len;
|
|
*hashmap_insert(assoc_map, key_assoc, hashcode_assoc) = entry;
|
|
|
|
end:
|
|
#ifdef BOOKMARKFS_BACKEND_CHROMIUM_WRITE
|
|
if (id > ctx->max_id) {
|
|
ctx->max_id = id;
|
|
}
|
|
#endif /* defined(BOOKMARKFS_BACKEND_CHROMIUM_WRITE) */
|
|
*parent_data_ptr = entry;
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
parse_entry (
|
|
struct backend_ctx const *ctx,
|
|
json_t const *node,
|
|
off_t next,
|
|
bool with_stat,
|
|
struct bookmarkfs_bookmark_entry *buf
|
|
) {
|
|
uint64_t id;
|
|
debug_assert(0 == parse_id(node, &id));
|
|
|
|
struct node_entry const *entry = lookup_id(ctx->id_map, id, NULL, NULL);
|
|
if (entry == NULL || entry->name == NULL) {
|
|
return -1;
|
|
}
|
|
|
|
buf->name = entry->name;
|
|
buf->next = next;
|
|
buf->stat.id = id;
|
|
if (ctx->flags & BACKEND_FILENAME_GUID) {
|
|
json_t const *guid_node = json_object_sget(entry->node, "guid");
|
|
buf->name = json_string_value(guid_node);
|
|
debug_assert(buf->name != NULL);
|
|
}
|
|
|
|
if (with_stat) {
|
|
if (unlikely(0 != parse_stats(entry, &buf->stat))) {
|
|
return -1;
|
|
}
|
|
} else {
|
|
buf->stat.value_len = entry->children == NULL ? 0 : -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
parse_id (
|
|
json_t const *node,
|
|
uint64_t *id_val_ptr
|
|
) {
|
|
json_t const *id_node = json_object_sget(node, "id");
|
|
if (unlikely(0 == json_string_length(id_node))) {
|
|
return -1;
|
|
}
|
|
|
|
char *end;
|
|
uint64_t id_val = strtoull(json_string_value(id_node), &end, 10);
|
|
if (unlikely(*end != '\0')) {
|
|
return -1;
|
|
}
|
|
if (unlikely(id_val > BOOKMARKFS_MAX_ID)) {
|
|
return -1;
|
|
}
|
|
|
|
if (id_val_ptr != NULL) {
|
|
*id_val_ptr = id_val;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
parse_guid (
|
|
char const *str,
|
|
size_t str_len,
|
|
uint8_t *guid
|
|
) {
|
|
if (str_len != UUID_HEX_LEN) {
|
|
return -1;
|
|
}
|
|
|
|
return uuid_hex2bin(guid, str);
|
|
}
|
|
|
|
static int
|
|
parse_mntopts (
|
|
struct bookmarkfs_conf_opt const *opts,
|
|
uint32_t flags,
|
|
struct parsed_mntopts *parsed_opts
|
|
) {
|
|
uint32_t watcher_flags = WATCHER_NOOP;
|
|
uint32_t other_flags = 0;
|
|
if (flags & BOOKMARKFS_BACKEND_FSCK_ONLY) {
|
|
goto end;
|
|
}
|
|
if (flags & BOOKMARKFS_BACKEND_READONLY) {
|
|
watcher_flags = 0;
|
|
}
|
|
|
|
BACKEND_OPT_START(opts)
|
|
BACKEND_OPT_KEY("filename") {
|
|
BACKEND_OPT_VAL_START
|
|
BACKEND_OPT_VAL("guid") {
|
|
other_flags |= BACKEND_FILENAME_GUID;
|
|
}
|
|
BACKEND_OPT_VAL("title") {
|
|
other_flags &= ~BACKEND_FILENAME_GUID;
|
|
}
|
|
BACKEND_OPT_VAL_END
|
|
}
|
|
BACKEND_OPT_KEY("watcher") {
|
|
BACKEND_OPT_VAL_START
|
|
BACKEND_OPT_VAL("native") {
|
|
watcher_flags = 0;
|
|
}
|
|
BACKEND_OPT_VAL("fallback") {
|
|
watcher_flags = WATCHER_FALLBACK;
|
|
}
|
|
BACKEND_OPT_VAL("none") {
|
|
watcher_flags = WATCHER_NOOP;
|
|
}
|
|
BACKEND_OPT_VAL_END
|
|
}
|
|
BACKEND_OPT_END
|
|
|
|
end:
|
|
parsed_opts->watcher_flags = watcher_flags;
|
|
parsed_opts->other_flags = other_flags;
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
parse_stats (
|
|
struct node_entry const *entry,
|
|
struct bookmarkfs_bookmark_stat *buf
|
|
) {
|
|
json_t const *node = entry->node;
|
|
|
|
json_t const *atime_node = json_object_sget(node, "date_last_used");
|
|
char const *atime = json_string_value(atime_node);
|
|
if (unlikely(atime == NULL)) {
|
|
return -EIO;
|
|
}
|
|
size_t atime_len = json_string_length(atime_node);
|
|
if (unlikely(0 != parse_ts(atime, atime_len, &buf->atime))) {
|
|
return -EIO;
|
|
}
|
|
|
|
json_t const *mtime_node = json_object_sget(node, "date_modified");
|
|
// If the bookmark has not been modified, this field does not exist.
|
|
if (mtime_node == NULL) {
|
|
mtime_node = json_object_sget(node, "date_added");
|
|
}
|
|
char const *mtime = json_string_value(mtime_node);
|
|
if (unlikely(mtime == NULL)) {
|
|
return -EIO;
|
|
}
|
|
size_t mtime_len = json_string_length(mtime_node);
|
|
if (unlikely(0 != parse_ts(mtime, mtime_len, &buf->mtime))) {
|
|
return -EIO;
|
|
}
|
|
|
|
if (entry->children == NULL) {
|
|
json_t *url = json_object_sget(node, "url");
|
|
buf->value_len = json_string_length(url);
|
|
} else {
|
|
buf->value_len = -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
parse_ts (
|
|
char const *str,
|
|
size_t str_len,
|
|
struct timespec *buf
|
|
) {
|
|
time_t secs = 0;
|
|
long microsecs;
|
|
|
|
char *end;
|
|
if (likely(str_len > 6)) {
|
|
str_len -= 6;
|
|
if (unlikely(str_len > 15)) {
|
|
return -1;
|
|
}
|
|
char tmp[16];
|
|
memcpy(tmp, str, str_len);
|
|
tmp[str_len] = '\0';
|
|
secs = strtoll(tmp, &end, 10);
|
|
|
|
if (*end != '\0' || secs < 0 || secs == LLONG_MAX) {
|
|
return -1;
|
|
}
|
|
str += str_len;
|
|
}
|
|
microsecs = strtol(str, &end, 10);
|
|
if (*end != '\0' || microsecs < 0 || microsecs == LONG_MAX) {
|
|
return -1;
|
|
}
|
|
|
|
if (buf != NULL) {
|
|
if (unlikely(secs < EPOCH_DIFF)) {
|
|
// Stay away from negative tv_sec
|
|
secs = EPOCH_DIFF;
|
|
}
|
|
buf->tv_sec = secs - EPOCH_DIFF;
|
|
buf->tv_nsec = microsecs * 1000;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
print_help (
|
|
uint32_t flags
|
|
) {
|
|
char const *options = "";
|
|
if (flags & BOOKMARKFS_FRONTEND_MOUNT) {
|
|
options = "Options:\n"
|
|
" filename=<title/guid> Bookmark file name origin\n"
|
|
" watcher=<native/fallback/none> File watcher type\n"
|
|
"\n";
|
|
} else if (flags & BOOKMARKFS_FRONTEND_MKFS) {
|
|
options = "Options:\n"
|
|
" date_added=<timestamp> Override date_added attribute\n"
|
|
"\n";
|
|
}
|
|
printf("Chromium backend for BookmarkFS\n\n"
|
|
"%s"
|
|
"Run 'info bookmarkfs' for more information.\n\n"
|
|
"Project homepage: <" BOOKMARKFS_HOMEPAGE_URL ">.\n", options);
|
|
}
|
|
|
|
static void
|
|
print_version (void)
|
|
{
|
|
printf("bookmarkfs-backend-chromium %d.%d.%d\n",
|
|
BOOKMARKFS_VER_MAJOR, BOOKMARKFS_VER_MINOR, BOOKMARKFS_VER_PATCH);
|
|
puts(BOOKMARKFS_FEATURE_STRING(DEBUG, "debug"));
|
|
puts(BOOKMARKFS_FEATURE_STRING(BACKEND_CHROMIUM_WRITE, "write"));
|
|
|
|
bookmarkfs_print_lib_version("\n");
|
|
}
|
|
|
|
static int
|
|
store_load (
|
|
struct backend_ctx *ctx
|
|
) {
|
|
struct watcher *watcher = ctx->watcher;
|
|
if (unlikely(watcher == NULL)) {
|
|
if (0 != init_watcher(ctx)) {
|
|
return -1;
|
|
}
|
|
goto do_load;
|
|
}
|
|
|
|
#ifdef BOOKMARKFS_BACKEND_CHROMIUM_WRITE
|
|
// Prioritize client-side modification to the store.
|
|
// Changes made by other processes will be lost.
|
|
if (ctx->dirty > DIRTY_LEVEL_NONE) {
|
|
return 0;
|
|
}
|
|
#endif /* defined(BOOKMARKFS_BACKEND_CHROMIUM_WRITE) */
|
|
|
|
switch (watcher_poll(watcher)) {
|
|
case WATCHER_POLL_ERR:
|
|
return -1;
|
|
|
|
case WATCHER_POLL_NOCHANGE:
|
|
if (ctx->store == NULL) {
|
|
break;
|
|
}
|
|
return 0;
|
|
|
|
case WATCHER_POLL_CHANGED:
|
|
json_decref(ctx->store);
|
|
ctx->store = NULL;
|
|
break;
|
|
|
|
default:
|
|
unreachable();
|
|
}
|
|
|
|
do_load:
|
|
debug_puts("loading store");
|
|
|
|
size_t flags = JSON_REJECT_DUPLICATES;
|
|
json_t *store = json_load_file_at(ctx->dirfd, ctx->name, flags);
|
|
if (store == NULL) {
|
|
return -1;
|
|
}
|
|
|
|
json_t *checksum = json_object_sget(store, "checksum");
|
|
if (unlikely(checksum == NULL)) {
|
|
log_puts("bad bookmark store: no checksum");
|
|
goto fail;
|
|
}
|
|
json_t *version = json_object_sget(store, "version");
|
|
if (unlikely(1 != json_integer_value(version))) {
|
|
log_puts("bad bookmark store: bad version");
|
|
goto fail;
|
|
}
|
|
json_t *roots = json_object_sget(store, "roots");
|
|
if (unlikely(roots == NULL)) {
|
|
log_puts("bad bookmark store: no roots");
|
|
goto fail;
|
|
}
|
|
|
|
ctx->roots = roots;
|
|
if (0 != build_maps(ctx)) {
|
|
goto fail;
|
|
}
|
|
ctx->store = store;
|
|
ctx->checksum = checksum;
|
|
|
|
#ifdef BOOKMARKFS_BACKEND_CHROMIUM_WRITE
|
|
if (!(ctx->flags & BOOKMARKFS_BACKEND_READONLY)) {
|
|
ctx->dirty = DIRTY_LEVEL_NONE;
|
|
}
|
|
#endif
|
|
return 0;
|
|
|
|
fail:
|
|
json_decref(store);
|
|
return -1;
|
|
}
|
|
|
|
static int
|
|
backend_create (
|
|
struct bookmarkfs_backend_conf const *conf,
|
|
struct bookmarkfs_backend_init_resp *resp
|
|
) {
|
|
#ifndef BOOKMARKFS_BACKEND_CHROMIUM_WRITE
|
|
if (!(conf->flags & BOOKMARKFS_READONLY)) {
|
|
log_puts("write support is not enabled on this build");
|
|
return -1;
|
|
}
|
|
#endif
|
|
|
|
struct parsed_mntopts opts = { 0 };
|
|
if (0 != parse_mntopts(conf->opts, conf->flags, &opts)) {
|
|
return -1;
|
|
}
|
|
|
|
char *name;
|
|
int dirfd = basename_opendir(conf->store_path, &name);
|
|
if (dirfd < 0) {
|
|
return -1;
|
|
}
|
|
|
|
uint32_t sandbox_flags = 0;
|
|
#if defined(__linux__)
|
|
if (conf->flags & BOOKMARKFS_BACKEND_NO_SANDBOX) {
|
|
sandbox_flags |= SANDBOX_NOOP;
|
|
} else if (conf->flags & BOOKMARKFS_BACKEND_NO_LANDLOCK) {
|
|
sandbox_flags |= SANDBOX_NO_LANDLOCK;
|
|
}
|
|
#elif defined(__FreeBSD__)
|
|
// Do not enable sandbox in the watcher, since cap_enter()
|
|
// applies to the entire process, not just the current thread.
|
|
// Let the main thread do the favor (after opening the FUSE device).
|
|
sandbox_flags |= SANDBOX_NOOP;
|
|
#else
|
|
# error "not implemented"
|
|
#endif /* defined(__linux__) || defined(__FreeBSD__) */
|
|
uint32_t watcher_flags
|
|
= opts.watcher_flags | (sandbox_flags << WATCHER_SANDBOX_FLAGS_OFFSET);
|
|
|
|
struct backend_ctx *ctx = xmalloc(sizeof(*ctx));
|
|
*ctx = (struct backend_ctx) {
|
|
.name = name,
|
|
.dirfd = dirfd,
|
|
#ifdef BOOKMARKFS_BACKEND_CHROMIUM_WRITE
|
|
.cd = (iconv_t)-1,
|
|
#endif
|
|
.flags = conf->flags | opts.other_flags,
|
|
.watcher_flags = watcher_flags,
|
|
};
|
|
|
|
uint32_t resp_flags = 0;
|
|
if (watcher_flags & WATCHER_NOOP) {
|
|
resp_flags |= BOOKMARKFS_BACKEND_EXCLUSIVE;
|
|
}
|
|
|
|
char const *bookmark_attrs = "guid\0date_added\0";
|
|
if (opts.other_flags & BACKEND_FILENAME_GUID) {
|
|
bookmark_attrs = "title\0date_added\0";
|
|
}
|
|
|
|
resp->name = "chromium";
|
|
resp->backend_ctx = ctx;
|
|
resp->bookmarks_root_id = BOOKMARKS_ROOT_ID;
|
|
resp->flags = resp_flags;
|
|
resp->bookmark_attrs = bookmark_attrs;
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
backend_free (
|
|
void *backend_ctx
|
|
) {
|
|
struct backend_ctx *ctx = backend_ctx;
|
|
if (ctx == NULL) {
|
|
return;
|
|
}
|
|
|
|
#ifdef BOOKMARKFS_BACKEND_CHROMIUM_WRITE
|
|
if (!(ctx->flags & BOOKMARKFS_BACKEND_READONLY)) {
|
|
store_save(ctx);
|
|
}
|
|
if (ctx->cd != (iconv_t)-1) {
|
|
iconv_close(ctx->cd);
|
|
}
|
|
#endif
|
|
|
|
watcher_destroy(ctx->watcher);
|
|
free_maps(ctx->id_map, ctx->assoc_map, ctx->guid_map);
|
|
json_decref(ctx->store);
|
|
json_decref(ctx->fake_root);
|
|
|
|
if (ctx->dirfd >= 0) {
|
|
close(ctx->dirfd);
|
|
}
|
|
free(ctx);
|
|
}
|
|
|
|
static void
|
|
backend_info (
|
|
uint32_t flags
|
|
) {
|
|
if (flags & BOOKMARKFS_BACKEND_INFO_HELP) {
|
|
print_help(flags);
|
|
} else if (flags & BOOKMARKFS_BACKEND_INFO_VERSION) {
|
|
print_version();
|
|
}
|
|
}
|
|
|
|
static int
|
|
backend_init (
|
|
uint32_t flags
|
|
) {
|
|
if (!(flags & BOOKMARKFS_BACKEND_LIB_READY)) {
|
|
if (0 != bookmarkfs_lib_init()) {
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
// Using xmalloc() ensures that most json_*() function calls
|
|
// are always successful (otherwise they will abort()),
|
|
// so that we don't have to make assertions everywhere.
|
|
json_set_alloc_funcs(xmalloc, free);
|
|
json_object_seed(prng_rand());
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
backend_sandbox (
|
|
void *backend_ctx,
|
|
struct bookmarkfs_backend_init_resp *UNUSED_VAR(resp)
|
|
) {
|
|
struct backend_ctx *ctx = backend_ctx;
|
|
|
|
if (ctx->flags & BOOKMARKFS_BACKEND_NO_SANDBOX) {
|
|
return 0;
|
|
}
|
|
|
|
#ifdef BOOKMARKFS_BACKEND_CHROMIUM_WRITE
|
|
if (!(ctx->flags & BOOKMARKFS_BACKEND_READONLY)) {
|
|
// Do not lazy-init iconv in sandbox mode,
|
|
// since it may want to load modules (e.g. from /usr/lib/gconv).
|
|
if (0 != init_iconv(&ctx->cd)) {
|
|
return -1;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// Watcher cannot be lazy-initialized in sandbox mode.
|
|
// Neither can it be initialized in backend_create(),
|
|
// since the calling process may fork per fuse_daemonize().
|
|
if (unlikely(0 != init_watcher(ctx))) {
|
|
return -1;
|
|
}
|
|
|
|
uint32_t sandbox_flags = 0;
|
|
if (ctx->flags & BOOKMARKFS_BACKEND_READONLY) {
|
|
sandbox_flags |= SANDBOX_READONLY;
|
|
}
|
|
if (ctx->flags & BOOKMARKFS_BACKEND_NO_LANDLOCK) {
|
|
sandbox_flags |= SANDBOX_NO_LANDLOCK;
|
|
}
|
|
return sandbox_enter(ctx->dirfd, sandbox_flags);
|
|
}
|
|
|
|
static int
|
|
bookmark_fsck (
|
|
void *backend_ctx,
|
|
uint64_t id,
|
|
struct bookmarkfs_fsck_data const *fsck_data,
|
|
uint32_t UNUSED_VAR(flags),
|
|
bookmarkfs_bookmark_fsck_cb *callback,
|
|
void *user_data,
|
|
void **cookie_ptr
|
|
) {
|
|
struct backend_ctx *ctx = backend_ctx;
|
|
|
|
if (ctx->flags & BACKEND_FILENAME_GUID) {
|
|
return -EPERM;
|
|
}
|
|
if (unlikely(0 != store_load(ctx))) {
|
|
return -EIO;
|
|
}
|
|
|
|
int status = 0;
|
|
struct bookmark_lcookie *cookie;
|
|
size_t idx = 0;
|
|
if (cookie_ptr != NULL) {
|
|
cookie = *cookie_ptr;
|
|
if (cookie != NULL) {
|
|
idx = cookie->idx;
|
|
}
|
|
}
|
|
// Unlike bookmark_list(), always fetch the latest entries during fsck.
|
|
struct node_entry const *entry = lookup_id(ctx->id_map, id, NULL, NULL);
|
|
if (unlikely(entry == NULL || entry->name == NULL)) {
|
|
return -ESTALE;
|
|
}
|
|
json_t *children = entry->children;
|
|
if (children == NULL) {
|
|
return -ENOTDIR;
|
|
}
|
|
|
|
// fsck_rewind
|
|
if (callback == NULL) {
|
|
idx = 0;
|
|
goto end;
|
|
}
|
|
if (fsck_data == NULL) {
|
|
status = fsck_next(ctx, id, children, &idx, callback, user_data);
|
|
} else {
|
|
debug_assert(!(ctx->flags & BOOKMARKFS_BACKEND_READONLY));
|
|
#ifdef BOOKMARKFS_BACKEND_CHROMIUM_WRITE
|
|
status = fsck_apply(ctx, id, fsck_data, callback, user_data);
|
|
#endif
|
|
}
|
|
if (status < 0) {
|
|
return status;
|
|
}
|
|
|
|
end:
|
|
if (cookie_ptr != NULL) {
|
|
if (cookie == NULL) {
|
|
cookie = xmalloc(sizeof(*cookie));
|
|
cookie->children = json_incref(children);
|
|
*cookie_ptr = cookie;
|
|
}
|
|
cookie->idx = idx;
|
|
}
|
|
return status;
|
|
}
|
|
|
|
static int
|
|
bookmark_get (
|
|
void *backend_ctx,
|
|
uint64_t id,
|
|
char const *attr_key,
|
|
bookmarkfs_bookmark_get_cb *callback,
|
|
void *user_data,
|
|
void **cookie_ptr
|
|
) {
|
|
struct backend_ctx *ctx = backend_ctx;
|
|
|
|
if (unlikely(0 != store_load(ctx))) {
|
|
return -EIO;
|
|
}
|
|
|
|
if (cookie_ptr == NULL) {
|
|
goto lookup;
|
|
}
|
|
struct bookmark_gcookie *cookie = *cookie_ptr;
|
|
if (cookie == NULL) {
|
|
goto lookup;
|
|
}
|
|
if (cookie->store != ctx->store) {
|
|
if (attr_key != NULL) {
|
|
goto lookup;
|
|
}
|
|
// Checksum only applies to directory structure and URL values.
|
|
if (!json_equal(cookie->checksum, ctx->checksum)) {
|
|
goto lookup;
|
|
}
|
|
}
|
|
return -EAGAIN;
|
|
|
|
lookup: ;
|
|
struct node_entry const *entry = lookup_id(ctx->id_map, id, NULL, NULL);
|
|
if (unlikely(entry == NULL || entry->name == NULL)) {
|
|
return -ESTALE;
|
|
}
|
|
if (entry->id == BOOKMARKS_ROOT_ID) {
|
|
return attr_key == NULL ? -EISDIR : -ENOATTR;
|
|
}
|
|
|
|
json_t *value_node;
|
|
int status = get_attr_val(entry->node, attr_key, ctx->flags, &value_node);
|
|
if (status < 0) {
|
|
return status;
|
|
}
|
|
|
|
if (callback == NULL) {
|
|
goto end;
|
|
}
|
|
char const *value = json_string_value(value_node);
|
|
if (unlikely(value == NULL)) {
|
|
return -EIO;
|
|
}
|
|
status = callback(user_data, value, json_string_length(value_node));
|
|
if (status < 0) {
|
|
return status;
|
|
}
|
|
|
|
end:
|
|
if (cookie_ptr != NULL) {
|
|
if (cookie == NULL) {
|
|
cookie = xmalloc(sizeof(*cookie));
|
|
*cookie_ptr = cookie;
|
|
} else {
|
|
json_decref(cookie->checksum);
|
|
}
|
|
cookie->store = ctx->store;
|
|
cookie->checksum = json_incref(ctx->checksum);
|
|
}
|
|
return status;
|
|
}
|
|
|
|
static int
|
|
bookmark_list (
|
|
void *backend_ctx,
|
|
uint64_t id,
|
|
off_t off,
|
|
uint32_t flags,
|
|
bookmarkfs_bookmark_list_cb *callback,
|
|
void *user_data,
|
|
void **cookie_ptr
|
|
) {
|
|
struct backend_ctx *ctx = backend_ctx;
|
|
|
|
if (unlikely(0 != store_load(ctx))) {
|
|
return -EIO;
|
|
}
|
|
|
|
int status = 0;
|
|
struct bookmark_lcookie *cookie;
|
|
struct node_entry const *entry = NULL;
|
|
json_t *children;
|
|
if (cookie_ptr != NULL) {
|
|
cookie = *cookie_ptr;
|
|
if (cookie != NULL) {
|
|
// `off == 0` implies rewinddir()
|
|
if (off > 0) {
|
|
children = cookie->children;
|
|
goto do_list;
|
|
}
|
|
}
|
|
}
|
|
entry = lookup_id(ctx->id_map, id, NULL, NULL);
|
|
if (unlikely(entry == NULL || entry->name == NULL)) {
|
|
return -ESTALE;
|
|
}
|
|
children = entry->children;
|
|
if (children == NULL) {
|
|
return -ENOTDIR;
|
|
}
|
|
|
|
do_list:
|
|
if (callback == NULL) {
|
|
goto end;
|
|
}
|
|
|
|
bool with_stat = flags & BOOKMARK_FLAG(LIST_WITHSTAT);
|
|
for (size_t idx = off; ; ++idx) {
|
|
json_t const *child = json_array_get(children, idx);
|
|
if (child == NULL) {
|
|
break;
|
|
}
|
|
|
|
struct bookmarkfs_bookmark_entry buf;
|
|
if (0 != parse_entry(ctx, child, idx + 1, with_stat, &buf)) {
|
|
// Silently ignore bookmark entries with bad or duplicate names.
|
|
continue;
|
|
}
|
|
status = callback(user_data, &buf);
|
|
if (status < 0) {
|
|
return status;
|
|
}
|
|
if (status > 0) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
end:
|
|
if (cookie_ptr != NULL && entry != NULL) {
|
|
if (callback == NULL) {
|
|
// No need to copy. Subsequent call with this cookie
|
|
// will have to re-fetch the children entries nonetheless.
|
|
json_incref(children);
|
|
} else {
|
|
children = json_copy(children);
|
|
}
|
|
if (cookie == NULL) {
|
|
cookie = xmalloc(sizeof(*cookie));
|
|
cookie->idx = 0;
|
|
*cookie_ptr = cookie;
|
|
} else {
|
|
json_decref(cookie->children);
|
|
}
|
|
cookie->children = children;
|
|
}
|
|
return status;
|
|
}
|
|
|
|
int
|
|
bookmark_lookup (
|
|
void *backend_ctx,
|
|
uint64_t id,
|
|
char const *name,
|
|
uint32_t UNUSED_VAR(flags),
|
|
struct bookmarkfs_bookmark_stat *stat_buf
|
|
) {
|
|
struct backend_ctx *ctx = backend_ctx;
|
|
|
|
if (unlikely(0 != store_load(ctx))) {
|
|
return -EIO;
|
|
}
|
|
|
|
struct node_entry *entry = lookup_id(ctx->id_map, id, NULL, NULL);
|
|
if (unlikely(entry == NULL || entry->name == NULL)) {
|
|
return -ESTALE;
|
|
}
|
|
|
|
if (name != NULL) {
|
|
if (entry->children == NULL) {
|
|
return -ENOTDIR;
|
|
}
|
|
if (0 != lookup_name(ctx, id, name, strlen(name), NULL, &entry)) {
|
|
return -ENOENT;
|
|
}
|
|
}
|
|
json_t const *node = entry->node;
|
|
|
|
if (stat_buf == NULL) {
|
|
return 0;
|
|
}
|
|
if (name != NULL) {
|
|
if (unlikely(0 != parse_id(node, &id))) {
|
|
return -EIO;
|
|
}
|
|
}
|
|
stat_buf->id = id;
|
|
return parse_stats(entry, stat_buf);
|
|
}
|
|
|
|
static void
|
|
object_free (
|
|
void *UNUSED_VAR(backend_ctx),
|
|
void *object,
|
|
enum bookmarkfs_object_type object_type
|
|
) {
|
|
if (object == NULL) {
|
|
return;
|
|
}
|
|
|
|
switch (object_type) {
|
|
case BOOKMARKFS_OBJECT_TYPE_BGCOOKIE:
|
|
free_bgcookie(object);
|
|
break;
|
|
|
|
case BOOKMARKFS_OBJECT_TYPE_BLCOOKIE:
|
|
free_blcookie(object);
|
|
break;
|
|
|
|
default:
|
|
unreachable();
|
|
}
|
|
}
|
|
|
|
#ifdef BOOKMARKFS_BACKEND_CHROMIUM_WRITE
|
|
|
|
static int
|
|
backend_mkfs (
|
|
struct bookmarkfs_backend_conf const *conf
|
|
) {
|
|
struct parsed_mkfsopts opts = {
|
|
.btime = { .tv_nsec = UTIME_NOW },
|
|
};
|
|
if (0 != parse_mkfsopts(conf->opts, &opts)) {
|
|
return -1;
|
|
}
|
|
|
|
int open_flags = O_CREAT | O_WRONLY | O_TRUNC;
|
|
if (!(conf->flags & BOOKMARKFS_BACKEND_MKFS_FORCE)) {
|
|
open_flags |= O_EXCL;
|
|
}
|
|
int fd = open(conf->store_path, open_flags, 0600);
|
|
if (fd < 0) {
|
|
log_printf("open(): %s: %s", conf->store_path, xstrerror(errno));
|
|
return -1;
|
|
}
|
|
int status = -1;
|
|
|
|
json_t *store;
|
|
if (0 != store_new(&opts.btime, &store)) {
|
|
goto end;
|
|
}
|
|
status = json_dumpfd_ex(store, fd, JSON_COMPACT);
|
|
json_decref(store);
|
|
|
|
end:
|
|
close(fd);
|
|
return status;
|
|
}
|
|
|
|
static int
|
|
backend_sync (
|
|
void *backend_ctx
|
|
) {
|
|
struct backend_ctx *ctx = backend_ctx;
|
|
debug_assert(!(ctx->flags & BOOKMARKFS_BACKEND_READONLY));
|
|
|
|
return store_save(ctx);
|
|
}
|
|
|
|
static int
|
|
bookmark_create (
|
|
void *backend_ctx,
|
|
uint64_t parent_id,
|
|
char const *name,
|
|
uint32_t flags,
|
|
struct bookmarkfs_bookmark_stat *stat_buf
|
|
) {
|
|
struct backend_ctx *ctx = backend_ctx;
|
|
debug_assert(!(ctx->flags & BOOKMARKFS_BACKEND_READONLY));
|
|
|
|
if (parent_id == BOOKMARKS_ROOT_ID) {
|
|
return -EPERM;
|
|
}
|
|
if (unlikely(0 != store_load(ctx))) {
|
|
return -EIO;
|
|
}
|
|
|
|
// Lookup parent entry
|
|
unsigned long hashcode;
|
|
struct node_entry *parent_entry
|
|
= lookup_id(ctx->id_map, parent_id, &hashcode, NULL);
|
|
if (unlikely(parent_entry == NULL || parent_entry->name == NULL)) {
|
|
return -ESTALE;
|
|
}
|
|
|
|
// Check if entry can be created
|
|
if (parent_entry->children == NULL) {
|
|
return -ENOTDIR;
|
|
}
|
|
|
|
size_t name_len = strlen(name);
|
|
struct lookup_ctx lctx;
|
|
struct node_entry *entry;
|
|
if (0 == lookup_name(ctx, parent_id, name, name_len, &lctx, &entry)) {
|
|
if (stat_buf != NULL) {
|
|
json_t *node = entry->node;
|
|
if (unlikely(0 != parse_id(node, &stat_buf->id))) {
|
|
return -EIO;
|
|
}
|
|
if (unlikely(0 != parse_stats(entry, stat_buf))) {
|
|
return -EIO;
|
|
}
|
|
}
|
|
return -EEXIST;
|
|
}
|
|
if (entry != NULL) {
|
|
// duplicate GUID
|
|
return -EPERM;
|
|
}
|
|
json_t *siblings = parent_entry->children;
|
|
|
|
// Build node
|
|
json_t *node = json_object();
|
|
bool is_dir = flags & BOOKMARK_FLAG(CREATE_DIR);
|
|
struct build_node_ctx bctx = {
|
|
.stat_buf = stat_buf,
|
|
};
|
|
int status = build_node(ctx, node, &name, name_len, is_dir, &bctx);
|
|
if (unlikely(status != 0)) {
|
|
json_decref(node);
|
|
return status;
|
|
}
|
|
json_array_append_new(siblings, node);
|
|
|
|
// Build lookup entry
|
|
entry = xmalloc(sizeof(*entry));
|
|
*entry = (struct node_entry) {
|
|
.id = ctx->max_id,
|
|
.parent_id = parent_entry->id,
|
|
.name = name,
|
|
.name_len = name_len,
|
|
.node = node,
|
|
.children = is_dir ? json_object_sget(node, "children") : NULL,
|
|
};
|
|
|
|
union hashmap_key key = { .u64 = ctx->max_id };
|
|
*hashmap_insert(ctx->id_map, key, hashcode) = entry;
|
|
|
|
void *guid = lctx.guid;
|
|
unsigned long hashcode_guid = lctx.hashcode;
|
|
if (!(ctx->flags & BACKEND_FILENAME_GUID)) {
|
|
guid = bctx.guid;
|
|
hashcode_guid = bctx.hashcode;
|
|
|
|
key.ptr = &(struct assocmap_key) {
|
|
.parent_id = parent_id,
|
|
.name = name,
|
|
.name_len = name_len,
|
|
};
|
|
*hashmap_insert(ctx->assoc_map, key, lctx.hashcode) = entry;
|
|
}
|
|
|
|
key.ptr = guid;
|
|
*hashmap_insert(ctx->guid_map, key, hashcode_guid) = entry;
|
|
|
|
memcpy(entry->guid, guid, UUID_LEN);
|
|
ctx->dirty = DIRTY_LEVEL_DATA;
|
|
|
|
if (unlikely(0 != node_mtime_now(parent_entry->node, NULL))) {
|
|
return -EIO;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
bookmark_delete (
|
|
void *backend_ctx,
|
|
uint64_t parent_id,
|
|
char const *name,
|
|
uint32_t UNUSED_VAR(flags)
|
|
) {
|
|
struct backend_ctx *ctx = backend_ctx;
|
|
debug_assert(!(ctx->flags & BOOKMARKFS_BACKEND_READONLY));
|
|
|
|
if (parent_id == BOOKMARKS_ROOT_ID) {
|
|
return -EPERM;
|
|
}
|
|
if (unlikely(0 != store_load(ctx))) {
|
|
return -EIO;
|
|
}
|
|
|
|
// Lookup parent entry
|
|
struct node_entry *parent_entry
|
|
= lookup_id(ctx->id_map, parent_id, NULL, NULL);
|
|
if (unlikely(parent_entry == NULL || parent_entry->name == NULL)) {
|
|
return -ESTALE;
|
|
}
|
|
if (parent_entry->children == NULL) {
|
|
return -ENOTDIR;
|
|
}
|
|
|
|
// Lookup entry to delete
|
|
struct lookup_ctx lctx;
|
|
struct node_entry *entry;
|
|
if (0 != lookup_name(ctx, parent_id, name, strlen(name), &lctx, &entry)) {
|
|
return -ENOENT;
|
|
};
|
|
json_t *siblings = parent_entry->children;
|
|
json_t const *node = entry->node;
|
|
|
|
// Check if entry can be deleted
|
|
json_t const *children = json_object_sget(node, "children");
|
|
if (children != NULL) {
|
|
if (0 != json_array_size(children)) {
|
|
return -ENOTEMPTY;
|
|
}
|
|
}
|
|
|
|
// Remove from maps
|
|
long guidmap_entry_id = lctx.entry_id;
|
|
if (!(ctx->flags & BACKEND_FILENAME_GUID)) {
|
|
guidmap_entry_id = -1;
|
|
hashmap_entry_delete(ctx->assoc_map, entry, lctx.entry_id);
|
|
}
|
|
hashmap_entry_delete(ctx->guid_map, entry, guidmap_entry_id);
|
|
hashmap_entry_delete(ctx->id_map, entry, -1);
|
|
free(entry);
|
|
|
|
// Remove from store
|
|
json_array_remove(siblings, json_array_search(siblings, node));
|
|
|
|
ctx->dirty = DIRTY_LEVEL_DATA;
|
|
|
|
if (unlikely(0 != node_mtime_now(parent_entry->node, NULL))) {
|
|
return -EIO;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
bookmark_permute (
|
|
void *backend_ctx,
|
|
uint64_t parent_id,
|
|
enum bookmarkfs_permd_op op,
|
|
char const *name1,
|
|
char const *name2,
|
|
uint32_t UNUSED_VAR(flags)
|
|
) {
|
|
struct backend_ctx *ctx = backend_ctx;
|
|
debug_assert(!(ctx->flags & BOOKMARKFS_BACKEND_READONLY));
|
|
|
|
if (parent_id == BOOKMARKS_ROOT_ID) {
|
|
return -EPERM;
|
|
}
|
|
if (unlikely(0 != store_load(ctx))) {
|
|
return -EIO;
|
|
}
|
|
|
|
size_t name1_len = strnlen(name1, NAME_MAX + 1);
|
|
if (0 != validate_filename(name1, name1_len, NULL)) {
|
|
return -EINVAL;
|
|
}
|
|
size_t name2_len = strnlen(name2, NAME_MAX + 1);
|
|
if (0 != validate_filename(name2, name2_len, NULL)) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
struct node_entry const *parent_entry
|
|
= lookup_id(ctx->id_map, parent_id, NULL, NULL);
|
|
if (unlikely(parent_entry == NULL || parent_entry->name == NULL)) {
|
|
return -ESTALE;
|
|
}
|
|
json_t *children = parent_entry->children;
|
|
if (children == NULL) {
|
|
return -ENOTDIR;
|
|
}
|
|
|
|
struct node_entry *entry1;
|
|
if (0 != lookup_name(ctx, parent_id, name1, name1_len, NULL, &entry1)) {
|
|
return -ENOENT;
|
|
}
|
|
|
|
struct node_entry *entry2;
|
|
if (0 != lookup_name(ctx, parent_id, name2, name2_len, NULL, &entry2)) {
|
|
return -ENOENT;
|
|
}
|
|
if (entry2 == entry1) {
|
|
return 0;
|
|
}
|
|
size_t idx1 = json_array_search(children, entry1->node);
|
|
size_t idx2 = json_array_search(children, entry2->node);
|
|
|
|
switch (op) {
|
|
case BOOKMARKFS_PERMD_OP_SWAP:
|
|
debug_assert(entry2 != NULL);
|
|
json_incref(entry1->node);
|
|
json_array_set(children, idx1, entry2->node);
|
|
json_array_set(children, idx2, entry1->node);
|
|
json_decref(entry1->node);
|
|
break;
|
|
|
|
case BOOKMARKFS_PERMD_OP_MOVE_AFTER:
|
|
++idx2;
|
|
// fallthrough
|
|
case BOOKMARKFS_PERMD_OP_MOVE_BEFORE:
|
|
if (idx1 == idx2) {
|
|
return 0;
|
|
}
|
|
json_incref(entry1->node);
|
|
if (idx1 > idx2) {
|
|
json_array_remove(children, idx1);
|
|
}
|
|
json_array_insert_new(children, idx2, entry1->node);
|
|
if (idx1 < idx2) {
|
|
json_array_remove(children, idx1);
|
|
}
|
|
break;
|
|
|
|
default:
|
|
return -EINVAL;
|
|
}
|
|
ctx->dirty = DIRTY_LEVEL_DATA;
|
|
|
|
if (unlikely(0 != node_mtime_now(parent_entry->node, NULL))) {
|
|
return -EIO;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
bookmark_rename (
|
|
void *backend_ctx,
|
|
uint64_t old_parent_id,
|
|
char const *old_name,
|
|
uint64_t new_parent_id,
|
|
char const *new_name,
|
|
uint32_t flags
|
|
) {
|
|
struct backend_ctx *ctx = backend_ctx;
|
|
debug_assert(!(ctx->flags & BOOKMARKFS_BACKEND_READONLY));
|
|
|
|
if (old_parent_id == BOOKMARKS_ROOT_ID
|
|
|| new_parent_id == BOOKMARKS_ROOT_ID) {
|
|
return -EPERM;
|
|
}
|
|
if (0 != store_load(ctx)) {
|
|
return -EIO;
|
|
}
|
|
|
|
// Lookup old entry
|
|
struct node_entry *old_parent
|
|
= lookup_id(ctx->id_map, old_parent_id, NULL, NULL);
|
|
if (unlikely(old_parent == NULL || old_parent->name == NULL)) {
|
|
return -ESTALE;
|
|
}
|
|
if (old_parent->children == NULL) {
|
|
return -ENOTDIR;
|
|
}
|
|
|
|
size_t old_name_len = strlen(old_name);
|
|
struct lookup_ctx old_lctx;
|
|
struct node_entry *old_entry;
|
|
if (0 != lookup_name(ctx, old_parent_id, old_name, old_name_len,
|
|
&old_lctx, &old_entry))
|
|
{
|
|
return -ENOENT;
|
|
}
|
|
json_t *old_node = old_entry->node;
|
|
|
|
// Lookup new entry
|
|
struct node_entry *new_parent = old_parent;
|
|
if (old_parent_id != new_parent_id) {
|
|
new_parent = lookup_id(ctx->id_map, new_parent_id, NULL, NULL);
|
|
if (unlikely(new_parent == NULL || new_parent->name == NULL)) {
|
|
return -ESTALE;
|
|
}
|
|
if (new_parent->children == NULL) {
|
|
return -ENOTDIR;
|
|
}
|
|
}
|
|
|
|
size_t new_name_len = strlen(new_name);
|
|
struct lookup_ctx new_lctx;
|
|
struct node_entry *new_entry;
|
|
if (0 == lookup_name(ctx, new_parent_id, new_name, new_name_len,
|
|
&new_lctx, &new_entry))
|
|
{
|
|
if (flags & BOOKMARKFS_BOOKMARK_RENAME_NOREPLACE) {
|
|
return -EEXIST;
|
|
}
|
|
if (new_entry == old_entry) {
|
|
return 0;
|
|
}
|
|
bool new_is_dir = new_entry->children != NULL;
|
|
if (new_is_dir != (old_entry->children != NULL)) {
|
|
return new_is_dir ? -EISDIR : -ENOTDIR;
|
|
}
|
|
if (new_is_dir && 0 != json_array_size(new_entry->children)) {
|
|
return -ENOTEMPTY;
|
|
}
|
|
} else {
|
|
if (new_entry != NULL) {
|
|
// duplicate GUID
|
|
return -EPERM;
|
|
}
|
|
}
|
|
|
|
if (old_name_len == new_name_len) {
|
|
if (0 == memcmp(old_name, new_name, new_name_len)) {
|
|
debug_assert(old_parent != new_parent);
|
|
goto move_node;
|
|
}
|
|
}
|
|
|
|
// Update node name
|
|
json_t *new_name_node = json_stringn(new_name, new_name_len);
|
|
if (new_name_node == NULL) {
|
|
return -EPERM;
|
|
}
|
|
json_object_sset_new(old_node, "name", new_name_node);
|
|
old_entry->name = json_string_value(new_name_node);
|
|
old_entry->name_len = json_string_length(new_name_node);
|
|
|
|
if (old_parent != new_parent) {
|
|
move_node:
|
|
old_entry->parent_id = new_parent->id;
|
|
if (new_entry == NULL) {
|
|
json_array_append(new_parent->children, old_node);
|
|
} else {
|
|
json_object_update(new_entry->node, old_node);
|
|
old_entry->node = new_entry->node;
|
|
}
|
|
size_t old_idx = json_array_search(old_parent->children, old_node);
|
|
json_array_remove(old_parent->children, old_idx);
|
|
}
|
|
|
|
bool filename_is_guid = ctx->flags & BACKEND_FILENAME_GUID;
|
|
if (new_entry != NULL) {
|
|
long new_guidmap_entry_id = new_lctx.entry_id;
|
|
if (!filename_is_guid) {
|
|
new_guidmap_entry_id = -1;
|
|
hashmap_entry_delete(ctx->assoc_map, new_entry, new_lctx.entry_id);
|
|
}
|
|
hashmap_entry_delete(ctx->guid_map, new_entry, new_guidmap_entry_id);
|
|
hashmap_entry_delete(ctx->id_map, new_entry, -1);
|
|
free(new_entry);
|
|
}
|
|
|
|
if (filename_is_guid) {
|
|
update_guid(old_entry, ctx->guid_map, old_lctx.entry_id, new_lctx.guid,
|
|
new_lctx.hashcode);
|
|
} else {
|
|
hashmap_entry_delete(ctx->assoc_map, old_entry, old_lctx.entry_id);
|
|
|
|
union hashmap_key key = {
|
|
.ptr = &(struct assocmap_key) {
|
|
.parent_id = new_parent_id,
|
|
.name = old_entry->name,
|
|
.name_len = new_name_len,
|
|
},
|
|
};
|
|
*hashmap_insert(ctx->assoc_map, key, new_lctx.hashcode) = old_entry;
|
|
}
|
|
ctx->dirty = DIRTY_LEVEL_DATA;
|
|
|
|
json_t *tsnode_now;
|
|
if (unlikely(0 != node_mtime_now(old_parent->node, &tsnode_now))) {
|
|
return -EIO;
|
|
}
|
|
if (old_parent != new_parent) {
|
|
json_object_sset_copy(new_parent->node, "date_modified", tsnode_now);
|
|
}
|
|
if (ctx->flags & BOOKMARKFS_BACKEND_CTIME) {
|
|
json_object_sset_copy(old_node, "date_modified", tsnode_now);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
bookmark_set (
|
|
void *backend_ctx,
|
|
uint64_t id,
|
|
char const *attr_key,
|
|
uint32_t flags,
|
|
void const *val,
|
|
size_t val_len
|
|
) {
|
|
struct backend_ctx *ctx = backend_ctx;
|
|
debug_assert(!(ctx->flags & BOOKMARKFS_BACKEND_READONLY));
|
|
|
|
if (id == BOOKMARKS_ROOT_ID) {
|
|
return -EPERM;
|
|
}
|
|
if (unlikely(0 != store_load(ctx))) {
|
|
return -EIO;
|
|
}
|
|
|
|
unsigned long entry_id;
|
|
struct node_entry *entry = lookup_id(ctx->id_map, id, NULL, &entry_id);
|
|
if (unlikely(entry == NULL || entry->name == NULL)) {
|
|
return -ESTALE;
|
|
}
|
|
if (entry->parent_id == BOOKMARKS_ROOT_ID) {
|
|
return -EPERM;
|
|
}
|
|
|
|
if (flags & BOOKMARK_FLAG(SET_TIME)) {
|
|
debug_assert(val_len == 2);
|
|
// Without UTIME_NOW, it is safe to cast away the const qualifier.
|
|
struct timespec *times = (struct timespec *)val;
|
|
if (unlikely(0 != update_node_ts(entry->node, times))) {
|
|
return -EIO;
|
|
}
|
|
if (ctx->dirty < DIRTY_LEVEL_METADATA) {
|
|
ctx->dirty = DIRTY_LEVEL_METADATA;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
json_t *val_node;
|
|
int key_type = get_attr_val(entry->node, attr_key, ctx->flags, &val_node);
|
|
if (key_type < 0) {
|
|
return key_type;
|
|
}
|
|
|
|
bool nocheck = true;
|
|
switch (key_type) {
|
|
case ATTR_KEY_DATE_ADDED:
|
|
if (0 != parse_ts(val, val_len, NULL)) {
|
|
return -EINVAL;
|
|
}
|
|
break;
|
|
|
|
case ATTR_KEY_GUID: ;
|
|
uint8_t guid[UUID_LEN];
|
|
if (0 != parse_guid(val, val_len, guid)) {
|
|
return -EINVAL;
|
|
}
|
|
unsigned long hashcode;
|
|
struct node_entry *entry_found
|
|
= lookup_guid(ctx->guid_map, guid, &hashcode, NULL);
|
|
if (entry_found != NULL) {
|
|
// Must not overwrite existing entry.
|
|
return entry_found->id == id ? 0 : -EPERM;
|
|
}
|
|
update_guid(entry, ctx->guid_map, entry_id, guid, hashcode);
|
|
break;
|
|
|
|
case ATTR_KEY_TITLE:
|
|
if (0 != validate_filename(val, val_len, NULL)) {
|
|
return -EINVAL;
|
|
}
|
|
nocheck = false;
|
|
break;
|
|
}
|
|
|
|
if (nocheck) {
|
|
json_string_setn_nocheck(val_node, val, val_len);
|
|
} else {
|
|
if (0 != json_string_setn(val_node, val, val_len)) {
|
|
// The new value is not valid UTF-8 and cannot fit in JSON.
|
|
return -EINVAL;
|
|
}
|
|
}
|
|
ctx->dirty = DIRTY_LEVEL_DATA;
|
|
|
|
if (key_type != ATTR_KEY_NULL && ctx->flags & BOOKMARKFS_BACKEND_CTIME) {
|
|
if (unlikely(0 != node_mtime_now(entry->node, NULL))) {
|
|
return -EIO;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
#endif /* defined(BOOKMARKFS_BACKEND_CHROMIUM_WRITE) */
|
|
|
|
BOOKMARKFS_API
|
|
struct bookmarkfs_backend const bookmarkfs_backend_chromium = {
|
|
.backend_create = backend_create,
|
|
.backend_free = backend_free,
|
|
.backend_info = backend_info,
|
|
.backend_init = backend_init,
|
|
.backend_sandbox = backend_sandbox,
|
|
|
|
.bookmark_fsck = bookmark_fsck,
|
|
.bookmark_get = bookmark_get,
|
|
.bookmark_list = bookmark_list,
|
|
.bookmark_lookup = bookmark_lookup,
|
|
|
|
.object_free = object_free,
|
|
|
|
#ifdef BOOKMARKFS_BACKEND_CHROMIUM_WRITE
|
|
.backend_mkfs = backend_mkfs,
|
|
.backend_sync = backend_sync,
|
|
|
|
.bookmark_create = bookmark_create,
|
|
.bookmark_delete = bookmark_delete,
|
|
.bookmark_permute = bookmark_permute,
|
|
.bookmark_rename = bookmark_rename,
|
|
.bookmark_set = bookmark_set,
|
|
#endif /* defined(BOOKMARKFS_BACKEND_CHROMIUM_WRITE) */
|
|
};
|