mirror of
https://git.sr.ht/~cismonx/bookmarkfs
synced 2025-06-07 11:48:51 +00:00
When a bookmark is deleted, if there are no other bookmarks
referencing the corresponding `moz_place` entry, tag and keyword
references to that entry are considered "dangling" references,
and shall be automatically removed.
Also reverts commit b5fa6960ef
,
since the NULL title check is no longer necessary.
3920 lines
111 KiB
C
3920 lines
111 KiB
C
/**
|
|
* bookmarkfs/src/backend_firefox.c
|
|
*
|
|
* Firefox 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 <stdbool.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include <fcntl.h>
|
|
#include <unistd.h>
|
|
|
|
#ifdef BOOKMARKFS_BACKEND_FIREFOX_WRITE
|
|
# include <nettle/base64.h>
|
|
# include <uriparser/Uri.h>
|
|
#endif
|
|
|
|
#include "backend.h"
|
|
#include "backend_util.h"
|
|
#include "db.h"
|
|
#include "hash.h"
|
|
#include "hashmap.h"
|
|
#include "lib.h"
|
|
#include "macros.h"
|
|
#include "prng.h"
|
|
#include "sandbox.h"
|
|
#include "version.h"
|
|
#include "xstd.h"
|
|
|
|
#define BM_XATTR_NULL 0
|
|
#define BM_XATTR_DESC 1
|
|
#define BM_XATTR_TITLE 2
|
|
#define BM_XATTR_GUID 3
|
|
#define BM_XATTR_DATE_ADDED 4
|
|
#define BM_XATTR_KEYWORD 5
|
|
#define MOZBM_XATTR_START BM_XATTR_TITLE
|
|
|
|
#define BACKEND_EXCLUSIVE_LOCK ( 1u << 16 )
|
|
#define BACKEND_FILENAME_GUID ( 1u << 17 )
|
|
#define BACKEND_ASSUME_TITLE_DISTINCT ( 1u << 18 )
|
|
|
|
#define BOOKMARKFS_BOOKMARK_LOOKUP_VALIDATE_GUID ( 1u << 8 )
|
|
|
|
#define GUID_LEN 9
|
|
#define GUID_STR_LEN 12
|
|
|
|
#define BOOKMARKS_ROOT_GUID "root________"
|
|
#define TAGS_ROOT_GUID "tags________"
|
|
|
|
#define DO_QUERY(ctx, stmt_ptr, sql, query_cb, query_cb_data, \
|
|
result, BEFORE_PREPARE, BEFORE_QUERY, ...) \
|
|
do { \
|
|
sqlite3_stmt *stmt_ = *(stmt_ptr); \
|
|
BEFORE_PREPARE \
|
|
if (stmt_ == NULL) { \
|
|
stmt_ = db_prepare((ctx)->db, sql, strlen(sql), true); \
|
|
if (unlikely(stmt_ == NULL)) { \
|
|
(result) = -EIO; \
|
|
break; \
|
|
} \
|
|
*(stmt_ptr) = stmt_; \
|
|
} \
|
|
struct db_stmt_bind_item const bind_[] = { __VA_ARGS__ }; \
|
|
BEFORE_QUERY \
|
|
(result) = db_query(stmt_, bind_, DB_BIND_ITEMS_CNT(bind_), \
|
|
true, (query_cb), (query_cb_data)); \
|
|
} while (0)
|
|
#define MOZBM_MAXPOS(parent_id) \
|
|
"SELECT max(`position`) FROM `moz_bookmarks` WHERE `parent` = " parent_id
|
|
|
|
enum {
|
|
STMT_BOOKMARK_GET,
|
|
STMT_BOOKMARK_GET_EX,
|
|
STMT_BOOKMARK_LIST,
|
|
STMT_BOOKMARK_LIST_EX,
|
|
STMT_BOOKMARK_LIST_KEYWORD,
|
|
STMT_BOOKMARK_LIST_KEYWORD_EX,
|
|
STMT_BOOKMARK_LIST_TAG,
|
|
STMT_BOOKMARK_LIST_TAG_EX,
|
|
STMT_BOOKMARK_LOOKUP_ASSOC,
|
|
STMT_BOOKMARK_LOOKUP_ID,
|
|
STMT_BOOKMARK_LOOKUP_KEYWORD,
|
|
STMT_BOOKMARK_LOOKUP_TAG_ASSOC,
|
|
STMT_DATA_VERSION,
|
|
#ifdef BOOKMARKFS_BACKEND_FIREFOX_WRITE
|
|
PERSISTED_STMT_WRITE_START,
|
|
STMT_BEGIN = PERSISTED_STMT_WRITE_START,
|
|
STMT_COMMIT,
|
|
STMT_ROLLBACK,
|
|
STMT_MOZPLACE_ADDREF,
|
|
STMT_MOZPLACE_ADDREF_ID,
|
|
STMT_MOZPLACE_DELETE,
|
|
STMT_MOZPLACE_DELREF,
|
|
STMT_MOZPLACE_INSERT,
|
|
STMT_MOZPLACE_UPDATE,
|
|
STMT_MOZORIGIN_DELETE,
|
|
STMT_MOZORIGIN_GET,
|
|
STMT_MOZORIGIN_INSERT,
|
|
STMT_MOZBM_DELETE_DIR,
|
|
STMT_MOZBM_DELETE_URL,
|
|
STMT_MOZBM_GET_TITLE,
|
|
STMT_MOZBM_INSERT,
|
|
STMT_MOZBM_LOOKUP,
|
|
STMT_MOZBM_LOOKUP_ID,
|
|
STMT_MOZBM_MOVE,
|
|
STMT_MOZBM_MTIME_UPDATE,
|
|
STMT_MOZBM_POS_SHIFT,
|
|
STMT_MOZBM_POS_UPDATE,
|
|
STMT_MOZBM_PURGE,
|
|
STMT_MOZBM_PURGE_CHECK,
|
|
STMT_MOZBM_UPDATE,
|
|
STMT_MOZBMDEL_INSERT,
|
|
STMT_MOZKW_DELETE,
|
|
STMT_MOZKW_INSERT,
|
|
STMT_MOZKW_LOOKUP,
|
|
STMT_MOZKW_PURGE,
|
|
STMT_MOZKW_RENAME,
|
|
STMT_TAG_ENTRY_LOOKUP,
|
|
#endif /* defined(BOOKMARKFS_BACKEND_FIREFOX_WRITE) */
|
|
PERSISTED_STMT_END,
|
|
};
|
|
|
|
struct backend_ctx {
|
|
sqlite3 *db;
|
|
uint64_t bookmarks_root_id;
|
|
uint64_t tags_root_id;
|
|
uint32_t flags;
|
|
|
|
struct sqlite3_stmt *stmts[PERSISTED_STMT_END];
|
|
};
|
|
|
|
struct bookmark_gcookie {
|
|
int64_t data_version;
|
|
};
|
|
|
|
struct bookmark_lcookie {
|
|
struct hashmap *dentry_map;
|
|
size_t idx; // fsck only
|
|
};
|
|
|
|
struct bookmark_dentry {
|
|
uint64_t id;
|
|
unsigned long hashcode;
|
|
|
|
size_t name_len;
|
|
char name[];
|
|
};
|
|
|
|
struct bookmark_name_key {
|
|
size_t len;
|
|
char const *val;
|
|
};
|
|
|
|
struct bookmark_get_ctx {
|
|
uint64_t tags_root_id;
|
|
bookmarkfs_bookmark_get_cb *callback;
|
|
void *user_data;
|
|
|
|
int status;
|
|
};
|
|
|
|
struct bookmark_list_ctx {
|
|
uint64_t tags_root_id;
|
|
size_t next;
|
|
struct hashmap *dentry_map;
|
|
db_query_row_func *row_func;
|
|
union {
|
|
bookmarkfs_bookmark_check_cb *check;
|
|
bookmarkfs_bookmark_list_cb *list;
|
|
} callback;
|
|
void *user_data;
|
|
bool check_name;
|
|
bool with_stat;
|
|
|
|
int status;
|
|
};
|
|
|
|
struct bookmark_lookup_ctx {
|
|
uint64_t tags_root_id;
|
|
struct bookmarkfs_bookmark_stat *stat_buf;
|
|
|
|
int status;
|
|
};
|
|
|
|
struct mozbm_check_ctx {
|
|
int64_t id;
|
|
struct hashmap *dentry_map;
|
|
|
|
int status;
|
|
};
|
|
|
|
struct mozbm_delete_ctx {
|
|
char guid_buf[GUID_STR_LEN];
|
|
char const *guid;
|
|
|
|
int64_t place_id;
|
|
};
|
|
|
|
struct mozplace_addref_ctx {
|
|
struct timespec *atime_buf;
|
|
int64_t id;
|
|
};
|
|
|
|
struct mozbm {
|
|
int64_t id;
|
|
int64_t place_id;
|
|
int64_t parent_id;
|
|
int64_t pos;
|
|
char const *title;
|
|
size_t title_len;
|
|
int64_t date_added;
|
|
int64_t last_modified;
|
|
char const *guid;
|
|
};
|
|
|
|
struct mozkw {
|
|
int64_t id;
|
|
char const *keyword;
|
|
size_t keyword_len;
|
|
int64_t place_id;
|
|
};
|
|
|
|
struct mozorigin {
|
|
int64_t id;
|
|
char const *prefix;
|
|
size_t prefix_len;
|
|
char const *host;
|
|
size_t host_len;
|
|
};
|
|
|
|
struct mozplace {
|
|
int64_t id;
|
|
char const *url;
|
|
size_t url_len;
|
|
int64_t url_hash;
|
|
char const *rev_host;
|
|
size_t rev_host_len;
|
|
int64_t last_visit_date;
|
|
int64_t origin_id;
|
|
char const *desc;
|
|
size_t desc_len;
|
|
};
|
|
|
|
struct parsed_mkfsopts {
|
|
int64_t date_added;
|
|
};
|
|
|
|
struct parsed_mntopts {
|
|
uint32_t flags;
|
|
};
|
|
|
|
// Forward declaration start
|
|
#ifdef BOOKMARKFS_BACKEND_FIREFOX_WRITE
|
|
static int bookmark_do_create (struct backend_ctx *, uint64_t,
|
|
char const *, size_t, bool,
|
|
struct bookmarkfs_bookmark_stat *);
|
|
static int bookmark_do_delete (struct backend_ctx *, uint64_t,
|
|
char const *, size_t, bool);
|
|
static int fsck_apply (struct backend_ctx *, uint64_t,
|
|
struct bookmarkfs_fsck_data const *,
|
|
struct bookmark_list_ctx *);
|
|
static char * gen_random_guid (char *);
|
|
static bool is_valid_guid (char const *, size_t);
|
|
static int keyword_create (struct backend_ctx *, char const *, size_t,
|
|
struct bookmarkfs_bookmark_stat *);
|
|
static int mozbm_check_cb (void *, sqlite3_stmt *);
|
|
static int mozbm_delete (struct backend_ctx *, int64_t, bool, bool);
|
|
static int mozbm_delete_cb (void *, sqlite3_stmt *);
|
|
static int mozbm_get_title (struct backend_ctx *, int64_t, int64_t,
|
|
db_query_row_func *, void *);
|
|
static int mozbm_insert (struct backend_ctx *, struct mozbm *);
|
|
static int mozbm_lookup (struct backend_ctx *, int64_t,
|
|
char const *, size_t, bool, struct mozbm *);
|
|
static int mozbm_lookup_id (struct backend_ctx *, struct mozbm *);
|
|
static int mozbm_move (struct backend_ctx *, int64_t, int64_t,
|
|
int64_t, char const *, size_t);
|
|
static int mozbm_mtime_update (struct backend_ctx *, int64_t, int64_t *);
|
|
static int mozbm_pos_shift (struct backend_ctx *, int64_t, int64_t,
|
|
int64_t *, enum bookmarkfs_permd_op);
|
|
static int mozbm_pos_update (struct backend_ctx *, int64_t, int64_t);
|
|
static int mozbm_purge (struct backend_ctx *, int64_t);
|
|
static int mozbm_purge_check (struct backend_ctx *, int64_t);
|
|
static int mozbm_update (struct backend_ctx *, struct mozbm *);
|
|
static int mozbmdel_insert (struct backend_ctx *, char const *);
|
|
static int mozkw_delete (struct backend_ctx *, char const *, size_t);
|
|
static int mozkw_insert (struct backend_ctx *, struct mozkw *);
|
|
static int mozkw_lookup (struct backend_ctx *, struct mozkw *);
|
|
static int mozkw_purge (struct backend_ctx *, int64_t);
|
|
static int mozkw_rename (struct backend_ctx *, char const *,
|
|
char const *, uint32_t);
|
|
static int mozorigin_delete (struct backend_ctx *, int64_t);
|
|
static int mozorigin_get (struct backend_ctx *, char const *, size_t,
|
|
char const *, size_t, int64_t *);
|
|
static int mozorigin_insert (struct backend_ctx *, struct mozorigin *);
|
|
static int mozplace_addref (struct backend_ctx *, char const *, size_t,
|
|
int64_t *, struct timespec *);
|
|
static int mozplace_addref_cb (void *, sqlite3_stmt *);
|
|
static int mozplace_addref_id (struct backend_ctx *, int64_t);
|
|
static int mozplace_delete (struct backend_ctx *, int64_t, int64_t);
|
|
static int mozplace_delref (struct backend_ctx *, int64_t, int);
|
|
static int mozplace_insert (struct backend_ctx *, struct mozplace *);
|
|
static int mozplace_purge (struct backend_ctx *, int64_t);
|
|
static int mozplace_update (struct backend_ctx *, struct mozplace *);
|
|
static int64_t mozplace_url_hash (char const *, size_t);
|
|
static int parse_mkfsopts (struct bookmarkfs_conf_opt const *,
|
|
struct parsed_mkfsopts *);
|
|
static int parse_mozurl_host (char const *, size_t, size_t *,
|
|
char const **, size_t *);
|
|
static int parse_usecs (char const *, size_t, int64_t *);
|
|
static int store_new (sqlite3 *, int64_t);
|
|
static int store_sync (sqlite3 *);
|
|
static int tag_entry_add (struct backend_ctx *, uint64_t,
|
|
char const *, size_t,
|
|
struct bookmarkfs_bookmark_stat *);
|
|
static int tag_entry_delete (struct backend_ctx *, uint64_t,
|
|
char const *, size_t);
|
|
static int tag_entry_lookup (struct backend_ctx *, struct mozbm *);
|
|
static int64_t timespec_to_usecs (struct timespec const *);
|
|
static int txn_begin (struct backend_ctx *);
|
|
static int txn_end (struct backend_ctx *);
|
|
static int txn_rollback (struct backend_ctx *, int);
|
|
static int64_t usecs_now (struct timespec *);
|
|
#endif /* defined(BOOKMARKFS_BACKEND_FIREFOX_WRITE) */
|
|
|
|
static int bookmark_do_get (struct backend_ctx *, uint64_t, int,
|
|
struct bookmark_get_ctx *);
|
|
static int bookmark_do_list (struct backend_ctx *, uint64_t, off_t,
|
|
uint32_t, struct bookmark_list_ctx *);
|
|
static int bookmark_do_lookup (struct backend_ctx *, uint64_t,
|
|
char const *, size_t, uint32_t,
|
|
struct bookmarkfs_bookmark_stat *);
|
|
static int bookmark_check_cb (void *, sqlite3_stmt *);
|
|
static int bookmark_get_cb (void *, sqlite3_stmt *);
|
|
static int bookmark_list_cb (void *, sqlite3_stmt *);
|
|
static int bookmark_lookup_cb (void *, sqlite3_stmt *);
|
|
static int dentmap_comp (union hashmap_key, void const *);
|
|
static unsigned long
|
|
dentmap_hash (void const *);
|
|
static void free_blcookie (struct bookmark_lcookie *);
|
|
static void free_dentmap (struct hashmap *);
|
|
static void free_dentmap_entry (void *, void *);
|
|
static int get_xattr_id (char const *, uint32_t);
|
|
static int64_t get_data_version (struct backend_ctx *);
|
|
static bool is_valid_id (int64_t);
|
|
static void usecs_to_timespec (struct timespec *, int64_t);
|
|
static int parse_mntopts (struct bookmarkfs_conf_opt const *,
|
|
uint32_t, struct parsed_mntopts *);
|
|
static void print_help (uint32_t);
|
|
static void print_version (void);
|
|
static int store_init (sqlite3 *, uint64_t *, uint64_t *);
|
|
static int store_check_cb (void *, sqlite3_stmt *);
|
|
// Forward declaration end
|
|
|
|
#ifdef BOOKMARKFS_BACKEND_FIREFOX_WRITE
|
|
|
|
static int
|
|
bookmark_do_create (
|
|
struct backend_ctx *ctx,
|
|
uint64_t parent_id,
|
|
char const *name,
|
|
size_t name_len,
|
|
bool is_dir,
|
|
struct bookmarkfs_bookmark_stat *stat_buf
|
|
) {
|
|
if (parent_id == ctx->bookmarks_root_id) {
|
|
return -EPERM;
|
|
}
|
|
|
|
int status = bookmark_do_lookup(ctx, parent_id, name, name_len,
|
|
BOOKMARK_FLAG(LOOKUP_VALIDATE_GUID), stat_buf);
|
|
if (status == 0) {
|
|
return -EEXIST;
|
|
}
|
|
if (status != -ENOENT) {
|
|
return status;
|
|
}
|
|
|
|
stat_buf->value_len = -1;
|
|
int64_t place_id = 0;
|
|
if (!is_dir) {
|
|
stat_buf->value_len = 0;
|
|
status = mozplace_addref(ctx, STR_WITHLEN("about:blank"), &place_id,
|
|
&stat_buf->atime);
|
|
if (status < 0) {
|
|
return status;
|
|
}
|
|
}
|
|
|
|
int64_t date_added = usecs_now(&stat_buf->mtime);
|
|
if (unlikely(date_added < 0)) {
|
|
return -EIO;
|
|
}
|
|
char const *guid = name;
|
|
char guid_buf[GUID_STR_LEN];
|
|
if (!(ctx->flags & BACKEND_FILENAME_GUID)) {
|
|
guid = gen_random_guid(guid_buf);
|
|
}
|
|
struct mozbm cols = {
|
|
.place_id = place_id,
|
|
.parent_id = parent_id,
|
|
.title = name,
|
|
.title_len = name_len,
|
|
.date_added = date_added,
|
|
.guid = guid,
|
|
};
|
|
status = mozbm_insert(ctx, &cols);
|
|
if (status < 0) {
|
|
return status;
|
|
}
|
|
stat_buf->id = cols.id;
|
|
|
|
status = mozbm_mtime_update(ctx, parent_id, &date_added);
|
|
if (status < 0) {
|
|
return status;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
bookmark_do_delete (
|
|
struct backend_ctx *ctx,
|
|
uint64_t parent_id,
|
|
char const *name,
|
|
size_t name_len,
|
|
bool is_dir
|
|
) {
|
|
if (parent_id == ctx->bookmarks_root_id) {
|
|
return -EPERM;
|
|
}
|
|
|
|
struct mozbm cols;
|
|
int status = mozbm_lookup(ctx, parent_id, name, name_len, false, &cols);
|
|
if (status < 0) {
|
|
return status;
|
|
}
|
|
if (unlikely((cols.place_id == 0) != is_dir)) {
|
|
return is_dir ? -ENOTDIR : -EISDIR;
|
|
}
|
|
|
|
status = mozbm_delete(ctx, cols.id, is_dir, true);
|
|
if (status < 0) {
|
|
return status;
|
|
}
|
|
status = mozbm_mtime_update(ctx, parent_id, NULL);
|
|
if (status < 0) {
|
|
return status;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
fsck_apply (
|
|
struct backend_ctx *ctx,
|
|
uint64_t parent_id,
|
|
struct bookmarkfs_fsck_data const *fsck_data,
|
|
struct bookmark_list_ctx *fctx
|
|
) {
|
|
int status = txn_begin(ctx);
|
|
if (unlikely(status < 0)) {
|
|
return status;
|
|
}
|
|
|
|
struct hashmap *map = fctx->dentry_map;
|
|
uint64_t id = fsck_data->id;
|
|
struct mozbm_check_ctx qctx = {
|
|
.id = id,
|
|
.dentry_map = map,
|
|
};
|
|
status = mozbm_get_title(ctx, id, parent_id, mozbm_check_cb, &qctx);
|
|
if (status < 0) {
|
|
goto fail;
|
|
}
|
|
if (qctx.status < 0) {
|
|
status = qctx.status;
|
|
goto fail;
|
|
}
|
|
if (qctx.status > 0) {
|
|
goto end;
|
|
}
|
|
|
|
uint64_t extra = 0;
|
|
char const *name = fsck_data->name;
|
|
size_t name_len = strnlen(name, sizeof(fsck_data->name));
|
|
int result;
|
|
if (0 != validate_filename_fsck(name, name_len, &result, &extra)) {
|
|
goto callback;
|
|
}
|
|
|
|
union hashmap_key key = {
|
|
.ptr = &(struct bookmark_name_key) {
|
|
.val = name,
|
|
.len = name_len,
|
|
},
|
|
};
|
|
unsigned long hashcode = hash_digest(name, name_len);
|
|
if (map == NULL) {
|
|
map = hashmap_create(dentmap_comp, dentmap_hash);
|
|
fctx->dentry_map = map;
|
|
goto update_name;
|
|
}
|
|
struct bookmark_dentry *dentry = hashmap_search(map, key, hashcode, NULL);
|
|
if (dentry != NULL) {
|
|
extra = dentry->id;
|
|
result = BOOKMARKFS_FSCK_RESULT_NAME_DUPLICATE;
|
|
goto callback;
|
|
}
|
|
|
|
update_name: ;
|
|
struct mozbm cols = {
|
|
.id = id,
|
|
.place_id = -1,
|
|
.title = name,
|
|
.title_len = name_len,
|
|
.date_added = -1,
|
|
.last_modified = -1,
|
|
};
|
|
status = mozbm_update(ctx, &cols);
|
|
if (status < 0) {
|
|
goto fail;
|
|
}
|
|
|
|
dentry = xmalloc(sizeof(*dentry) + name_len);
|
|
dentry->id = id;
|
|
dentry->hashcode = hashcode;
|
|
dentry->name_len = name_len;
|
|
memcpy(dentry->name, name, name_len);
|
|
|
|
hashmap_insert(map, hashcode, dentry);
|
|
goto end;
|
|
|
|
callback:
|
|
status = fctx->callback.check(fctx->user_data, result, id, extra, name);
|
|
if (status < 0) {
|
|
goto fail;
|
|
}
|
|
|
|
end:
|
|
return txn_end(ctx);
|
|
|
|
fail:
|
|
return txn_rollback(ctx, status);
|
|
}
|
|
|
|
static char *
|
|
gen_random_guid (
|
|
char *out
|
|
) {
|
|
struct base64_encode_ctx ctx;
|
|
base64url_encode_init(&ctx);
|
|
|
|
uint64_t const buf[] = { prng_rand(), prng_rand() };
|
|
base64_encode_final(&ctx, out +
|
|
base64_encode_update(&ctx, out, GUID_LEN, (uint8_t const *)buf));
|
|
return out;
|
|
}
|
|
|
|
static bool
|
|
is_valid_guid (
|
|
char const *str,
|
|
size_t len
|
|
) {
|
|
if (len != GUID_STR_LEN) {
|
|
return false;
|
|
}
|
|
|
|
struct base64_decode_ctx ctx;
|
|
base64url_decode_init(&ctx);
|
|
|
|
uint8_t buf[BASE64_DECODE_LENGTH(GUID_STR_LEN)];
|
|
if (!base64_decode_update(&ctx, &len, buf, len, str)) {
|
|
return false;
|
|
}
|
|
if (!base64_decode_final(&ctx)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static int
|
|
keyword_create (
|
|
struct backend_ctx *ctx,
|
|
char const *keyword,
|
|
size_t keyword_len,
|
|
struct bookmarkfs_bookmark_stat *stat_buf
|
|
) {
|
|
struct mozbm bm_cols = {
|
|
.id = stat_buf->id,
|
|
};
|
|
int status = mozbm_lookup_id(ctx, &bm_cols);
|
|
if (status < 0) {
|
|
return status;
|
|
}
|
|
if (bm_cols.place_id == 0) {
|
|
return -EPERM;
|
|
}
|
|
|
|
struct mozkw kw_cols = {
|
|
.keyword = keyword,
|
|
.keyword_len = keyword_len,
|
|
.place_id = bm_cols.place_id,
|
|
};
|
|
status = mozkw_insert(ctx, &kw_cols);
|
|
if (status < 0) {
|
|
return status;
|
|
}
|
|
|
|
status = bookmark_do_lookup(ctx, bm_cols.id, NULL, 0, 0, stat_buf);
|
|
if (status < 0) {
|
|
return status;
|
|
}
|
|
status = mozplace_addref_id(ctx, bm_cols.place_id);
|
|
if (status < 0) {
|
|
return status;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
mozbm_check_cb (
|
|
void *user_data,
|
|
sqlite3_stmt *stmt
|
|
) {
|
|
struct mozbm_check_ctx *ctx = user_data;
|
|
|
|
char const *name = (char const *)sqlite3_column_text(stmt, 0);
|
|
size_t name_len = sqlite3_column_bytes(stmt, 0);
|
|
if (unlikely(name == NULL)) {
|
|
name = "";
|
|
}
|
|
if (0 != validate_filename(name, name_len, NULL)) {
|
|
return 1;
|
|
}
|
|
|
|
struct hashmap *map = ctx->dentry_map;
|
|
if (map == NULL) {
|
|
ctx->status = 1;
|
|
return 1;
|
|
}
|
|
union hashmap_key key = {
|
|
.ptr = &(struct bookmark_name_key) {
|
|
.val = name,
|
|
.len = name_len,
|
|
},
|
|
};
|
|
unsigned long hashcode = hash_digest(name, name_len);
|
|
struct bookmark_dentry *dentry = hashmap_search(map, key, hashcode, NULL);
|
|
if (dentry == NULL || dentry->id == (uint64_t)ctx->id) {
|
|
// fsck_apply() was given an ID not previously returned by fsck_next().
|
|
ctx->status = -ENOENT;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
static int
|
|
mozbm_delete (
|
|
struct backend_ctx *ctx,
|
|
int64_t id,
|
|
bool is_dir,
|
|
bool purge
|
|
) {
|
|
#define MOZBM_DELETE_(cond) \
|
|
"DELETE FROM `moz_bookmarks` WHERE `id` = ? " cond \
|
|
"RETURNING iif(`syncStatus` = 2, `guid`, NULL), `fk`"
|
|
#define MOZBM_DELETE_DIR MOZBM_DELETE_( \
|
|
"AND `id` NOT IN (SELECT DISTINCT `parent` FROM `moz_bookmarks`) ")
|
|
#define MOZBM_DELETE_URL MOZBM_DELETE_("")
|
|
|
|
sqlite3_stmt **stmt_ptr = &ctx->stmts[STMT_MOZBM_DELETE_URL];
|
|
char const *sql = MOZBM_DELETE_URL;
|
|
if (is_dir) {
|
|
stmt_ptr = &ctx->stmts[STMT_MOZBM_DELETE_DIR];
|
|
sql = MOZBM_DELETE_DIR;
|
|
}
|
|
|
|
struct mozbm_delete_ctx qctx;
|
|
ssize_t nrows;
|
|
DO_QUERY(ctx, stmt_ptr, sql, mozbm_delete_cb, &qctx, nrows, , ,
|
|
DB_QUERY_BIND_INT64(id),
|
|
);
|
|
if (nrows < 0) {
|
|
return nrows;
|
|
}
|
|
if (is_dir) {
|
|
if (nrows == 0) {
|
|
return -ENOTEMPTY;
|
|
}
|
|
} else {
|
|
// The ID is alwayed obtained from a previous query in
|
|
// the same transaction. This shall not happen.
|
|
xassert(nrows > 0);
|
|
int status = mozplace_delref(ctx, qctx.place_id, purge ? 0 : 1);
|
|
if (status < 0) {
|
|
return status;
|
|
}
|
|
}
|
|
if (qctx.guid == NULL) {
|
|
return 0;
|
|
}
|
|
return mozbmdel_insert(ctx, qctx.guid);
|
|
}
|
|
|
|
static int
|
|
mozbm_delete_cb (
|
|
void *user_data,
|
|
sqlite3_stmt *stmt
|
|
) {
|
|
struct mozbm_delete_ctx *ctx = user_data;
|
|
|
|
char const *guid = (char const *)sqlite3_column_text(stmt, 0);
|
|
size_t guid_len = sqlite3_column_bytes(stmt, 0);
|
|
if (guid_len != GUID_STR_LEN) {
|
|
ctx->guid = NULL;
|
|
} else {
|
|
ctx->guid = memcpy(ctx->guid_buf, guid, guid_len);
|
|
}
|
|
|
|
ctx->place_id = sqlite3_column_int64(stmt, 1);
|
|
return 1;
|
|
}
|
|
|
|
static int
|
|
mozbm_get_title (
|
|
struct backend_ctx *ctx,
|
|
int64_t id,
|
|
int64_t parent_id,
|
|
db_query_row_func *row_func,
|
|
void *user_data
|
|
) {
|
|
sqlite3_stmt **stmt_ptr = &ctx->stmts[STMT_MOZBM_GET_TITLE];
|
|
char const *sql = "SELECT `title` FROM `moz_bookmarks` "
|
|
"WHERE `id` = ? AND `parent` = ?";
|
|
|
|
ssize_t nrows;
|
|
DO_QUERY(ctx, stmt_ptr, sql, row_func, user_data, nrows, , ,
|
|
DB_QUERY_BIND_INT64(id),
|
|
DB_QUERY_BIND_INT64(parent_id),
|
|
);
|
|
if (nrows < 0) {
|
|
return nrows;
|
|
}
|
|
if (nrows == 0) {
|
|
return -ESTALE;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
mozbm_insert (
|
|
struct backend_ctx *ctx,
|
|
struct mozbm *cols
|
|
) {
|
|
sqlite3_stmt **stmt_ptr = &ctx->stmts[STMT_MOZBM_INSERT];
|
|
char const *sql =
|
|
"INSERT INTO `moz_bookmarks` (`parent`, `position`, `title`, "
|
|
"`dateAdded`, `lastModified`, `type`, `fk`, `guid`, `syncStatus`) "
|
|
"VALUES (?1, safeincr((" MOZBM_MAXPOS("?1") ")), ?2, ?3, ?3, "
|
|
"?4, nullif(?5, -1), ?6, 1)";
|
|
|
|
int status;
|
|
DO_QUERY(ctx, stmt_ptr, sql, NULL, NULL, status, prepare:, ,
|
|
DB_QUERY_BIND_INT64(cols->parent_id),
|
|
DB_QUERY_BIND_TEXT(cols->title, cols->title_len),
|
|
DB_QUERY_BIND_INT64(cols->date_added),
|
|
DB_QUERY_BIND_INT64(cols->place_id == 0 ? 2 : 1),
|
|
DB_QUERY_BIND_INT64(cols->place_id),
|
|
DB_QUERY_BIND_TEXT(cols->guid, GUID_STR_LEN),
|
|
);
|
|
if (status < 0) {
|
|
// duplicate GUID
|
|
if (unlikely(status == -EEXIST)) {
|
|
goto prepare;
|
|
}
|
|
return status;
|
|
}
|
|
|
|
cols->id = sqlite3_last_insert_rowid(ctx->db);
|
|
if (!is_valid_id(cols->id)) {
|
|
return -ENOSPC;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
mozbm_lookup (
|
|
struct backend_ctx *ctx,
|
|
int64_t parent_id,
|
|
char const *name,
|
|
size_t name_len,
|
|
bool validate_guid,
|
|
struct mozbm *cols
|
|
) {
|
|
#define MOZBM_LOOKUP(col) \
|
|
"SELECT `id`, `fk`, `position` FROM `moz_bookmarks` " \
|
|
"WHERE `parent` = ? AND `" col "` = ? ORDER BY `position` LIMIT 1"
|
|
|
|
struct sqlite3_stmt **stmt_ptr = &ctx->stmts[STMT_MOZBM_LOOKUP];
|
|
char const *sql = MOZBM_LOOKUP("title");
|
|
if (ctx->flags & BACKEND_FILENAME_GUID) {
|
|
if (validate_guid && !is_valid_guid(name, name_len)) {
|
|
return -EPERM;
|
|
}
|
|
sql = MOZBM_LOOKUP("guid");
|
|
}
|
|
|
|
int64_t values[3];
|
|
ssize_t nrows;
|
|
DO_QUERY(ctx, stmt_ptr, sql, db_query_i64_cb, values, nrows, , ,
|
|
DB_QUERY_BIND_INT64(parent_id),
|
|
DB_QUERY_BIND_TEXT(name, name_len),
|
|
);
|
|
if (nrows < 0) {
|
|
return nrows;
|
|
}
|
|
if (nrows == 0) {
|
|
return -ENOENT;
|
|
}
|
|
cols->id = values[0];
|
|
cols->place_id = values[1];
|
|
cols->pos = values[2];
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
mozbm_lookup_id (
|
|
struct backend_ctx *ctx,
|
|
struct mozbm *cols
|
|
) {
|
|
#define MOZBM_LOOKUP_ID(col) \
|
|
"SELECT `id`, `fk` FROM `moz_bookmarks` WHERE (`fk`, `" col "`) = " \
|
|
"(SELECT `fk`, `" col "` FROM `moz_bookmarks` WHERE `id` = ?) " \
|
|
"ORDER BY `id` LIMIT 1"
|
|
|
|
sqlite3_stmt **stmt_ptr = &ctx->stmts[STMT_MOZBM_LOOKUP_ID];
|
|
char const *sql = MOZBM_LOOKUP_ID("title");
|
|
if (ctx->flags & BACKEND_FILENAME_GUID) {
|
|
sql = MOZBM_LOOKUP_ID("guid");
|
|
}
|
|
|
|
int64_t values[2];
|
|
ssize_t nrows;
|
|
DO_QUERY(ctx, stmt_ptr, sql, db_query_i64_cb, values, nrows, , ,
|
|
DB_QUERY_BIND_INT64(cols->id),
|
|
);
|
|
if (nrows < 0) {
|
|
return nrows;
|
|
}
|
|
if (nrows == 0) {
|
|
return -ESTALE;
|
|
}
|
|
cols->id = values[0];
|
|
cols->place_id = values[1];
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
mozbm_move (
|
|
struct backend_ctx *ctx,
|
|
int64_t id,
|
|
int64_t new_parent,
|
|
int64_t new_position,
|
|
char const *new_name,
|
|
size_t new_name_len
|
|
) {
|
|
#define MOZBM_MOVE(col) "UPDATE `moz_bookmarks` " \
|
|
"SET (`parent`, `" col "`, `position`) = (?1, ifnull(?2, `" col "`), " \
|
|
"ifnull(?3, safeincr((" MOZBM_MAXPOS("?1") ")))) " \
|
|
"WHERE `id` = ?4"
|
|
|
|
sqlite3_stmt **stmt_ptr = &ctx->stmts[STMT_MOZBM_MOVE];
|
|
char const *sql = MOZBM_MOVE("title");
|
|
if (ctx->flags & BACKEND_FILENAME_GUID) {
|
|
sql = MOZBM_MOVE("guid");
|
|
}
|
|
|
|
int status;
|
|
DO_QUERY(ctx, stmt_ptr, sql, NULL, NULL, status, , ,
|
|
DB_QUERY_BIND_INT64(new_parent),
|
|
DB_QUERY_BIND_TEXT(new_name, new_name_len),
|
|
DB_QUERY_BIND_INT64(new_position),
|
|
DB_QUERY_BIND_INT64(id),
|
|
);
|
|
if (status < 0) {
|
|
return status;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
mozbm_mtime_update (
|
|
struct backend_ctx *ctx,
|
|
int64_t id,
|
|
int64_t *usecs_ptr
|
|
) {
|
|
sqlite3_stmt **stmt_ptr = &ctx->stmts[STMT_MOZBM_MTIME_UPDATE];
|
|
char const *sql =
|
|
"UPDATE `moz_bookmarks` SET `lastModified` = ? WHERE `id` = ?";
|
|
|
|
int64_t usecs = -1;
|
|
if (usecs_ptr != NULL) {
|
|
usecs = *usecs_ptr;
|
|
}
|
|
if (usecs < 0) {
|
|
usecs = usecs_now(NULL);
|
|
if (unlikely(usecs < 0)) {
|
|
return -EIO;
|
|
}
|
|
}
|
|
|
|
int status;
|
|
DO_QUERY(ctx, stmt_ptr, sql, NULL, NULL, status, , ,
|
|
DB_QUERY_BIND_INT64(usecs),
|
|
DB_QUERY_BIND_INT64(id),
|
|
);
|
|
if (status < 0) {
|
|
return status;
|
|
}
|
|
if (usecs_ptr != NULL) {
|
|
*usecs_ptr = usecs;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
mozbm_pos_shift (
|
|
struct backend_ctx *ctx,
|
|
int64_t parent_id,
|
|
int64_t pos_start,
|
|
int64_t *pos_end_ptr,
|
|
enum bookmarkfs_permd_op op
|
|
) {
|
|
int64_t pos_end = *pos_end_ptr;
|
|
if (unlikely(pos_start == pos_end)) {
|
|
// Somehow two bookmarks share a same position...
|
|
return -EIO;
|
|
}
|
|
|
|
int64_t diff = 0;
|
|
if (pos_start < pos_end) {
|
|
if (op == BOOKMARKFS_PERMD_OP_MOVE_BEFORE) {
|
|
if (--pos_end == pos_start) {
|
|
return 0;
|
|
}
|
|
*pos_end_ptr = pos_end;
|
|
} else if (op != BOOKMARKFS_PERMD_OP_MOVE_AFTER) {
|
|
return -EINVAL;
|
|
}
|
|
++pos_start;
|
|
} else {
|
|
if (op == BOOKMARKFS_PERMD_OP_MOVE_AFTER) {
|
|
if (++pos_end == pos_start) {
|
|
return 0;
|
|
}
|
|
*pos_end_ptr = pos_end;
|
|
} else if (op != BOOKMARKFS_PERMD_OP_MOVE_BEFORE) {
|
|
return -EINVAL;
|
|
}
|
|
--pos_start;
|
|
|
|
diff = 2;
|
|
int64_t tmp = pos_end;
|
|
pos_end = pos_start;
|
|
pos_start = tmp;
|
|
}
|
|
|
|
sqlite3_stmt **stmt_ptr = &ctx->stmts[STMT_MOZBM_POS_SHIFT];
|
|
char const *sql =
|
|
"UPDATE `moz_bookmarks` SET `position` = `position` + (? - 1) "
|
|
"WHERE `parent` = ? AND `position` BETWEEN ? AND ?";
|
|
|
|
int status;
|
|
DO_QUERY(ctx, stmt_ptr, sql, NULL, NULL, status, , ,
|
|
DB_QUERY_BIND_INT64(diff),
|
|
DB_QUERY_BIND_INT64(parent_id),
|
|
DB_QUERY_BIND_INT64(pos_start),
|
|
DB_QUERY_BIND_INT64(pos_end),
|
|
);
|
|
if (status < 0) {
|
|
return status;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
static int
|
|
mozbm_pos_update (
|
|
struct backend_ctx *ctx,
|
|
int64_t id,
|
|
int64_t new_pos
|
|
) {
|
|
sqlite3_stmt **stmt_ptr = &ctx->stmts[STMT_MOZBM_POS_UPDATE];
|
|
char const *sql =
|
|
"UPDATE `moz_bookmarks` SET `position` = ? WHERE `id` = ?";
|
|
|
|
int status;
|
|
DO_QUERY(ctx, stmt_ptr, sql, NULL, NULL, status, , ,
|
|
DB_QUERY_BIND_INT64(new_pos),
|
|
DB_QUERY_BIND_INT64(id),
|
|
);
|
|
if (status < 0) {
|
|
return status;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
static int
|
|
mozbm_purge (
|
|
struct backend_ctx *ctx,
|
|
int64_t place_id
|
|
) {
|
|
sqlite3_stmt **stmt_ptr = &ctx->stmts[STMT_MOZBM_PURGE];
|
|
char const *sql = "DELETE FROM `moz_bookmarks` WHERE `fk` = ?";
|
|
|
|
int status;
|
|
DO_QUERY(ctx, stmt_ptr, sql, NULL, NULL, status, , ,
|
|
DB_QUERY_BIND_INT64(place_id),
|
|
);
|
|
if (status < 0) {
|
|
return status;
|
|
}
|
|
return sqlite3_changes(ctx->db);
|
|
}
|
|
|
|
static int
|
|
mozbm_purge_check (
|
|
struct backend_ctx *ctx,
|
|
int64_t place_id
|
|
) {
|
|
sqlite3_stmt **stmt_ptr = &ctx->stmts[STMT_MOZBM_PURGE_CHECK];
|
|
char const *sql = "SELECT COUNT(*) FROM `moz_bookmarks` "
|
|
"WHERE `fk` = ? AND `title` IS NOT NULL";
|
|
|
|
int64_t result;
|
|
ssize_t nrows;
|
|
DO_QUERY(ctx, stmt_ptr, sql, db_query_i64_cb, &result, nrows, , ,
|
|
DB_QUERY_BIND_INT64(place_id),
|
|
);
|
|
if (nrows < 0) {
|
|
return nrows;
|
|
}
|
|
debug_assert(nrows == 1);
|
|
return result == 0;
|
|
}
|
|
|
|
static int
|
|
mozbm_update (
|
|
struct backend_ctx *ctx,
|
|
struct mozbm *cols
|
|
) {
|
|
sqlite3_stmt **stmt_ptr = &ctx->stmts[STMT_MOZBM_UPDATE];
|
|
char const *sql = "UPDATE `moz_bookmarks` "
|
|
"SET (`fk`, `title`, `guid`, `dateAdded`, `lastModified`) "
|
|
"= (ifnull(?, `fk`), ifnull(?, `title`), ifnull(?, `guid`), "
|
|
"ifnull(?, `dateAdded`), ifnull(?, `lastModified`)) "
|
|
"WHERE `id` = ? RETURNING `fk`";
|
|
|
|
ssize_t nrows;
|
|
DO_QUERY(ctx, stmt_ptr, sql, db_query_i64_cb, &cols->place_id, nrows, , ,
|
|
DB_QUERY_BIND_INT64(cols->place_id),
|
|
DB_QUERY_BIND_TEXT(cols->title, cols->title_len),
|
|
DB_QUERY_BIND_TEXT(cols->guid, GUID_STR_LEN),
|
|
DB_QUERY_BIND_INT64(cols->date_added),
|
|
DB_QUERY_BIND_INT64(cols->last_modified),
|
|
DB_QUERY_BIND_INT64(cols->id),
|
|
);
|
|
if (nrows < 0) {
|
|
// duplicate GUID
|
|
if (nrows == -EEXIST) {
|
|
nrows = -EPERM;
|
|
}
|
|
return nrows;
|
|
}
|
|
if (nrows == 0) {
|
|
return -ESTALE;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
mozbmdel_insert (
|
|
struct backend_ctx *ctx,
|
|
char const *guid
|
|
) {
|
|
sqlite3_stmt **stmt_ptr = &ctx->stmts[STMT_MOZBMDEL_INSERT];
|
|
char const *sql =
|
|
"INSERT OR IGNORE INTO `moz_bookmarks_deleted` (`guid`, `dateRemoved`)"
|
|
"VALUES (?, ?)";
|
|
|
|
int64_t date_removed = usecs_now(NULL);
|
|
if (unlikely(date_removed < 0)) {
|
|
return -EIO;
|
|
}
|
|
|
|
int status;
|
|
DO_QUERY(ctx, stmt_ptr, sql, NULL, NULL, status, , ,
|
|
DB_QUERY_BIND_TEXT(guid, GUID_STR_LEN),
|
|
DB_QUERY_BIND_INT64(date_removed),
|
|
);
|
|
if (status < 0) {
|
|
return status;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
mozkw_delete (
|
|
struct backend_ctx *ctx,
|
|
char const *name,
|
|
size_t name_len
|
|
) {
|
|
sqlite3_stmt **stmt_ptr = &ctx->stmts[STMT_MOZKW_DELETE];
|
|
char const *sql = "DELETE FROM `moz_keywords` "
|
|
"WHERE `keyword` = ? RETURNING `place_id`";
|
|
|
|
int64_t place_id;
|
|
int status;
|
|
DO_QUERY(ctx, stmt_ptr, sql, db_query_i64_cb, &place_id, status, , ,
|
|
DB_QUERY_BIND_TEXT(name, name_len),
|
|
);
|
|
if (status < 0) {
|
|
return status;
|
|
}
|
|
status = mozplace_delref(ctx, place_id, 1);
|
|
if (status < 0) {
|
|
return status;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
mozkw_insert (
|
|
struct backend_ctx *ctx,
|
|
struct mozkw *cols
|
|
) {
|
|
sqlite3_stmt **stmt_ptr = &ctx->stmts[STMT_MOZKW_INSERT];
|
|
char const *sql =
|
|
"INSERT INTO `moz_keywords` (`keyword`, `place_id`) VALUES (?, ?)";
|
|
|
|
int status;
|
|
DO_QUERY(ctx, stmt_ptr, sql, NULL, NULL, status, , ,
|
|
DB_QUERY_BIND_TEXT(cols->keyword, cols->keyword_len),
|
|
DB_QUERY_BIND_INT64(cols->place_id),
|
|
);
|
|
if (status < 0) {
|
|
// May fail with -EEXIST here.
|
|
return status;
|
|
}
|
|
|
|
cols->id = sqlite3_last_insert_rowid(ctx->db);
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
mozkw_lookup (
|
|
struct backend_ctx *ctx,
|
|
struct mozkw *cols
|
|
) {
|
|
sqlite3_stmt **stmt_ptr = &ctx->stmts[STMT_MOZKW_LOOKUP];
|
|
char const *sql =
|
|
"SELECT `id`, `place_id` FROM `moz_keywords` WHERE `keyword` = ?";
|
|
|
|
int64_t values[2];
|
|
ssize_t nrows;
|
|
DO_QUERY(ctx, stmt_ptr, sql, db_query_i64_cb, values, nrows, , ,
|
|
DB_QUERY_BIND_TEXT(cols->keyword, cols->keyword_len),
|
|
);
|
|
if (nrows < 0) {
|
|
return nrows;
|
|
}
|
|
if (nrows == 0) {
|
|
return -ENOENT;
|
|
}
|
|
cols->id = values[0];
|
|
cols->place_id = values[1];
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
mozkw_purge (
|
|
struct backend_ctx *ctx,
|
|
int64_t place_id
|
|
) {
|
|
sqlite3_stmt **stmt_ptr = &ctx->stmts[STMT_MOZKW_PURGE];
|
|
char const *sql = "DELETE FROM `moz_keywords` WHERE `place_id` = ?";
|
|
|
|
int status;
|
|
DO_QUERY(ctx, stmt_ptr, sql, NULL, NULL, status, , ,
|
|
DB_QUERY_BIND_INT64(place_id),
|
|
);
|
|
if (status < 0) {
|
|
return status;
|
|
}
|
|
return sqlite3_changes(ctx->db);
|
|
}
|
|
|
|
static int
|
|
mozkw_rename (
|
|
struct backend_ctx *ctx,
|
|
char const *old_name,
|
|
char const *new_name,
|
|
uint32_t flags
|
|
) {
|
|
struct mozkw old_cols = {
|
|
.keyword = old_name,
|
|
.keyword_len = strlen(old_name),
|
|
};
|
|
int status = mozkw_lookup(ctx, &old_cols);
|
|
if (status < 0) {
|
|
if (status != -ENOENT) {
|
|
return status;
|
|
}
|
|
} else {
|
|
if (flags & BOOKMARKFS_BOOKMARK_RENAME_NOREPLACE) {
|
|
return -EEXIST;
|
|
}
|
|
status = mozplace_delref(ctx, old_cols.place_id, 1);
|
|
if (status < 0) {
|
|
return status;
|
|
}
|
|
}
|
|
|
|
sqlite3_stmt **stmt_ptr = &ctx->stmts[STMT_MOZKW_RENAME];
|
|
char const *sql =
|
|
"UPDATE OR REPLACE `moz_keywords` SET `id` = ? WHERE `keyword` = ?";
|
|
|
|
DO_QUERY(ctx, stmt_ptr, sql, NULL, NULL, status, , ,
|
|
DB_QUERY_BIND_INT64(old_cols.id),
|
|
DB_QUERY_BIND_TEXT(new_name, strlen(new_name)),
|
|
);
|
|
if (status < 0) {
|
|
return status;
|
|
}
|
|
if (0 == sqlite3_changes(ctx->db)) {
|
|
return -ENOENT;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
mozorigin_delete (
|
|
struct backend_ctx *ctx,
|
|
int64_t id
|
|
) {
|
|
sqlite3_stmt **stmt_ptr = &ctx->stmts[STMT_MOZORIGIN_DELETE];
|
|
char const *sql = "DELETE FROM `moz_origins` WHERE `id` = ? "
|
|
"AND `id` NOT IN (SELECT DISTINCT `origin_id` FROM `moz_places`)";
|
|
|
|
int status;
|
|
DO_QUERY(ctx, stmt_ptr, sql, NULL, NULL, status, , ,
|
|
DB_QUERY_BIND_INT64(id),
|
|
);
|
|
if (status < 0) {
|
|
return status;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
mozorigin_get (
|
|
struct backend_ctx *ctx,
|
|
char const *prefix,
|
|
size_t prefix_len,
|
|
char const *host,
|
|
size_t host_len,
|
|
int64_t *id_ptr
|
|
) {
|
|
sqlite3_stmt **stmt_ptr = &ctx->stmts[STMT_MOZORIGIN_GET];
|
|
char const *sql =
|
|
"SELECT `id` FROM `moz_origins` WHERE `prefix` = ? AND `host` = ?";
|
|
|
|
ssize_t nrows;
|
|
DO_QUERY(ctx, stmt_ptr, sql, db_query_i64_cb, id_ptr, nrows, , ,
|
|
DB_QUERY_BIND_TEXT(prefix, prefix_len),
|
|
DB_QUERY_BIND_TEXT(host, host_len),
|
|
);
|
|
if (nrows < 0) {
|
|
return nrows;
|
|
}
|
|
if (nrows == 0) {
|
|
return -ENOENT;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
mozorigin_insert (
|
|
struct backend_ctx *ctx,
|
|
struct mozorigin *cols
|
|
) {
|
|
sqlite3_stmt **stmt_ptr = &ctx->stmts[STMT_MOZORIGIN_INSERT];
|
|
char const *sql =
|
|
"INSERT INTO `moz_origins` (`prefix`, `host`, `frecency`, "
|
|
"`recalc_frecency`, `recalc_alt_frecency`) "
|
|
"VALUES (?, ?, 1, 1, 1)";
|
|
|
|
int status;
|
|
DO_QUERY(ctx, stmt_ptr, sql, NULL, NULL, status, , ,
|
|
DB_QUERY_BIND_TEXT(cols->prefix, cols->prefix_len),
|
|
DB_QUERY_BIND_TEXT(cols->host, cols->host_len),
|
|
);
|
|
if (status < 0) {
|
|
return status;
|
|
}
|
|
|
|
cols->id = sqlite3_last_insert_rowid(ctx->db);
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
mozplace_addref (
|
|
struct backend_ctx *ctx,
|
|
char const *url,
|
|
size_t url_len,
|
|
int64_t *id_ptr,
|
|
struct timespec *atime_buf
|
|
) {
|
|
size_t prefix_len;
|
|
char const *host;
|
|
size_t host_len;
|
|
if (0 != parse_mozurl_host(url, url_len, &prefix_len, &host, &host_len)) {
|
|
log_puts("parse_mozurl_host(): bad url");
|
|
return -EINVAL;
|
|
}
|
|
int64_t url_hash = mozplace_url_hash(url, url_len);
|
|
|
|
sqlite3_stmt **stmt_ptr = &ctx->stmts[STMT_MOZPLACE_ADDREF];
|
|
char const *sql =
|
|
"UPDATE `moz_places` SET `foreign_count` = `foreign_count` + 1 "
|
|
"WHERE `url_hash` = ? AND `url` = ? RETURNING `id`, `last_visit_date`";
|
|
|
|
struct mozplace_addref_ctx qctx;
|
|
ssize_t nrows;
|
|
DO_QUERY(ctx, stmt_ptr, sql, mozplace_addref_cb, &qctx, nrows, ,
|
|
{
|
|
qctx.atime_buf = atime_buf;
|
|
},
|
|
DB_QUERY_BIND_INT64(url_hash),
|
|
DB_QUERY_BIND_TEXT(url, url_len),
|
|
);
|
|
if (nrows < 0) {
|
|
return nrows;
|
|
}
|
|
if (nrows > 0) {
|
|
*id_ptr = qctx.id;
|
|
return 0;
|
|
}
|
|
|
|
int64_t origin_id;
|
|
int status = mozorigin_get(ctx, url, prefix_len, host, host_len,
|
|
&origin_id);
|
|
if (status < 0) do {
|
|
if (status != -ENOENT) {
|
|
return status;
|
|
}
|
|
|
|
struct mozorigin cols = {
|
|
.prefix = url,
|
|
.prefix_len = prefix_len,
|
|
.host = host,
|
|
.host_len = host_len,
|
|
};
|
|
status = mozorigin_insert(ctx, &cols);
|
|
if (status < 0) {
|
|
return status;
|
|
}
|
|
origin_id = cols.id;
|
|
} while (0);
|
|
|
|
char *rev_host = xmalloc(host_len + 1);
|
|
for (size_t idx = 0; idx < host_len; ++idx) {
|
|
rev_host[idx] = host[host_len - idx - 1];
|
|
}
|
|
rev_host[host_len] = '.';
|
|
|
|
struct mozplace cols = {
|
|
.url = url,
|
|
.url_len = url_len,
|
|
.url_hash = url_hash,
|
|
.rev_host = rev_host,
|
|
.rev_host_len = host_len + 1,
|
|
.origin_id = origin_id,
|
|
};
|
|
status = mozplace_insert(ctx, &cols);
|
|
if (status < 0) {
|
|
goto end;
|
|
}
|
|
*id_ptr = cols.id;
|
|
|
|
end:
|
|
free(rev_host);
|
|
return status;
|
|
}
|
|
|
|
static int
|
|
mozplace_addref_cb (
|
|
void *user_data,
|
|
sqlite3_stmt *stmt
|
|
) {
|
|
struct mozplace_addref_ctx *ctx = user_data;
|
|
|
|
ctx->id = sqlite3_column_int64(stmt, 0);
|
|
if (ctx->atime_buf != NULL) {
|
|
usecs_to_timespec(ctx->atime_buf, sqlite3_column_int64(stmt, 1));
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
static int
|
|
mozplace_addref_id (
|
|
struct backend_ctx *ctx,
|
|
int64_t id
|
|
) {
|
|
sqlite3_stmt **stmt_ptr = &ctx->stmts[STMT_MOZPLACE_ADDREF_ID];
|
|
char const *sql = "UPDATE `moz_places` "
|
|
"SET `foreign_count` = `foreign_count` + 1 WHERE `id` = ?";
|
|
|
|
int status;
|
|
DO_QUERY(ctx, stmt_ptr, sql, NULL, NULL, status, , ,
|
|
DB_QUERY_BIND_INT64(id),
|
|
);
|
|
if (status < 0) {
|
|
return status;
|
|
}
|
|
if (0 == sqlite3_changes(ctx->db)) {
|
|
return -ESTALE;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
mozplace_delete (
|
|
struct backend_ctx *ctx,
|
|
int64_t id,
|
|
int64_t origin_id
|
|
) {
|
|
sqlite3_stmt **stmt_ptr = &ctx->stmts[STMT_MOZPLACE_DELETE];
|
|
char const *sql = "DELETE FROM `moz_places` WHERE `id` = ?";
|
|
|
|
ssize_t nrows;
|
|
DO_QUERY(ctx, stmt_ptr, sql, NULL, NULL, nrows, , ,
|
|
DB_QUERY_BIND_INT64(id),
|
|
);
|
|
if (nrows < 0) {
|
|
return nrows;
|
|
}
|
|
if (unlikely(0 == sqlite3_changes(ctx->db))) {
|
|
return -EIO;
|
|
}
|
|
return mozorigin_delete(ctx, origin_id);
|
|
}
|
|
|
|
static int
|
|
mozplace_delref (
|
|
struct backend_ctx *ctx,
|
|
int64_t id,
|
|
int purge
|
|
) {
|
|
sqlite3_stmt **stmt_ptr = &ctx->stmts[STMT_MOZPLACE_DELREF];
|
|
char const *sql =
|
|
"UPDATE `moz_places` SET `foreign_count` = `foreign_count` - ? "
|
|
"WHERE `id` = ? RETURNING `foreign_count`, `origin_id`";
|
|
|
|
ssize_t nrows;
|
|
int64_t result[2]; // `foreign_count`, `origin_id`
|
|
DO_QUERY(ctx, stmt_ptr, sql, db_query_i64_cb, result, nrows, , ,
|
|
DB_QUERY_BIND_INT64(purge == 0 ? 1 : purge),
|
|
DB_QUERY_BIND_INT64(id),
|
|
);
|
|
if (nrows < 0) {
|
|
return nrows;
|
|
}
|
|
if (unlikely(nrows == 0)) {
|
|
return -EIO;
|
|
}
|
|
if (result[0] == 0) {
|
|
// `foreign_count` reaches 0, delete row.
|
|
return mozplace_delete(ctx, id, result[1]);
|
|
}
|
|
if (purge > 0) {
|
|
return 0;
|
|
}
|
|
return mozplace_purge(ctx, id);
|
|
}
|
|
|
|
static int
|
|
mozplace_insert (
|
|
struct backend_ctx *ctx,
|
|
struct mozplace *cols
|
|
) {
|
|
sqlite3_stmt **stmt_ptr = &ctx->stmts[STMT_MOZPLACE_INSERT];
|
|
char const *sql =
|
|
"INSERT INTO `moz_places` (`url`, `rev_host`, `guid`, `frecency`, "
|
|
"`foreign_count`, `url_hash`, `origin_id`, `recalc_frecency`) "
|
|
"VALUES (?, ?, ?, 1, 1, ?, ?, 1)";
|
|
|
|
char guid_buf[GUID_STR_LEN];
|
|
int status;
|
|
DO_QUERY(ctx, stmt_ptr, sql, NULL, NULL, status, prepare:, ,
|
|
DB_QUERY_BIND_TEXT(cols->url, cols->url_len),
|
|
DB_QUERY_BIND_TEXT(cols->rev_host, cols->rev_host_len),
|
|
DB_QUERY_BIND_TEXT(gen_random_guid(guid_buf), GUID_STR_LEN),
|
|
DB_QUERY_BIND_INT64(cols->url_hash),
|
|
DB_QUERY_BIND_INT64(cols->origin_id),
|
|
);
|
|
if (status < 0) {
|
|
// duplicate GUID
|
|
if (unlikely(status == -EEXIST)) {
|
|
goto prepare;
|
|
}
|
|
return status;
|
|
}
|
|
|
|
cols->id = sqlite3_last_insert_rowid(ctx->db);
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
mozplace_purge (
|
|
struct backend_ctx *ctx,
|
|
int64_t id
|
|
) {
|
|
int status = mozbm_purge_check(ctx, id);
|
|
if (status <= 0) {
|
|
return status;
|
|
}
|
|
status = mozbm_purge(ctx, id);
|
|
if (status < 0) {
|
|
return status;
|
|
}
|
|
int changes = status;
|
|
|
|
status = mozkw_purge(ctx, id);
|
|
if (status < 0) {
|
|
return status;
|
|
}
|
|
changes += status;
|
|
|
|
if (changes == 0) {
|
|
return 0;
|
|
}
|
|
return mozplace_delref(ctx, id, changes);
|
|
}
|
|
|
|
static int
|
|
mozplace_update (
|
|
struct backend_ctx *ctx,
|
|
struct mozplace *cols
|
|
) {
|
|
if (cols->url == NULL) {
|
|
goto do_update;
|
|
}
|
|
int64_t new_id;
|
|
int status = mozplace_addref(ctx, cols->url, cols->url_len, &new_id, NULL);
|
|
if (status < 0) {
|
|
return status;
|
|
}
|
|
status = mozplace_delref(ctx, cols->id, 1);
|
|
if (status < 0) {
|
|
return status;
|
|
}
|
|
cols->id = new_id;
|
|
|
|
do_update: ;
|
|
sqlite3_stmt **stmt_ptr = &ctx->stmts[STMT_MOZPLACE_UPDATE];
|
|
char const *sql = "UPDATE `moz_places` "
|
|
"SET (`last_visit_date`, `description`) "
|
|
"= (ifnull(?, `last_visit_date`), ifnull(?, `description`)) "
|
|
"WHERE `id` = ?";
|
|
|
|
DO_QUERY(ctx, stmt_ptr, sql, NULL, NULL, status, , ,
|
|
DB_QUERY_BIND_INT64(cols->last_visit_date),
|
|
DB_QUERY_BIND_TEXT(cols->desc, cols->desc_len),
|
|
DB_QUERY_BIND_INT64(cols->id),
|
|
);
|
|
if (status < 0) {
|
|
return status;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Calculate the 48-bit URL hash for a given string.
|
|
* Unspecified result if the string does not contain a colon.
|
|
*
|
|
* See function `HashURL()` in mozilla-central source code:
|
|
* /toolkit/components/places/Helpers.cpp
|
|
*/
|
|
static int64_t
|
|
mozplace_url_hash (
|
|
char const *url,
|
|
size_t url_len
|
|
) {
|
|
#define MAX_URL_HASH_LEN 1500
|
|
#define ROTL32(v, b) ( (v) << (b) | (v) >> (32 - (b)) )
|
|
|
|
if (url_len > MAX_URL_HASH_LEN) {
|
|
url_len = MAX_URL_HASH_LEN;
|
|
}
|
|
|
|
uint64_t prefix_hash = UINT64_MAX;
|
|
uint32_t str_hash = 0;
|
|
for (char const *end = url + url_len; url < end; ++url) {
|
|
uint32_t ch = *url;
|
|
if (prefix_hash == UINT64_MAX && ch == ':') {
|
|
prefix_hash = str_hash;
|
|
}
|
|
str_hash = (ROTL32(str_hash, 5) ^ ch) * UINT32_C(0x9e3779b9);
|
|
}
|
|
return (prefix_hash & 0xffff) << 32 | str_hash;
|
|
}
|
|
|
|
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 *end;
|
|
int64_t val = strtoll(BACKEND_OPT_VAL_STR, &end, 10);
|
|
if (*end != '\0' || val < 0 || val == LLONG_MAX) {
|
|
return BACKEND_OPT_BAD_VAL();
|
|
}
|
|
parsed_opts->date_added = val;
|
|
}
|
|
BACKEND_OPT_END
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
parse_mozurl_host (
|
|
char const *url,
|
|
size_t len,
|
|
size_t *prefix_len_ptr,
|
|
char const **host_ptr,
|
|
size_t *host_len_ptr
|
|
) {
|
|
UriUriA uri;
|
|
char const *end = url + len;
|
|
if (URI_SUCCESS != uriParseSingleUriExA(&uri, url, end, NULL)) {
|
|
return -1;
|
|
}
|
|
bool has_authority = false;
|
|
|
|
int status = -1;
|
|
if (uri.scheme.first == NULL) {
|
|
goto end;
|
|
}
|
|
|
|
char const *host_end = end;
|
|
UriPathSegmentA *path = uri.pathHead;
|
|
if (path != NULL) {
|
|
host_end = path->text.first - 1;
|
|
}
|
|
if (uri.hostText.afterLast != NULL) {
|
|
host_end = uri.hostText.afterLast;
|
|
if (host_end < end && *host_end == ']') {
|
|
++host_end;
|
|
}
|
|
}
|
|
if (uri.portText.afterLast != NULL) {
|
|
host_end = uri.portText.afterLast;
|
|
has_authority = true;
|
|
}
|
|
|
|
char const *host = host_end;
|
|
if (uri.hostText.first != NULL) {
|
|
host = uri.hostText.first;
|
|
if (host[-1] == '[') {
|
|
--host;
|
|
}
|
|
if (host_end - host > 0) {
|
|
has_authority = true;
|
|
}
|
|
}
|
|
|
|
char const *prefix_end = uri.scheme.afterLast + 1;
|
|
if (uri.userInfo.first != NULL) {
|
|
has_authority = true;
|
|
}
|
|
if (has_authority) {
|
|
prefix_end += 2;
|
|
}
|
|
|
|
*prefix_len_ptr = prefix_end - url;
|
|
*host_ptr = host;
|
|
*host_len_ptr = host_end - host;
|
|
status = 0;
|
|
|
|
end:
|
|
uriFreeUriMembersA(&uri);
|
|
return status;
|
|
}
|
|
|
|
static int
|
|
parse_usecs (
|
|
char const *str,
|
|
size_t str_len,
|
|
int64_t *usecs_ptr
|
|
) {
|
|
#define MAX_TIME_STR_LEN 19
|
|
if (str_len > MAX_TIME_STR_LEN) {
|
|
return -1;
|
|
}
|
|
char buf[MAX_TIME_STR_LEN + 1];
|
|
#undef MAX_TIME_STR_LEN
|
|
memcpy(buf, str, str_len);
|
|
buf[str_len] = '\0';
|
|
|
|
char *end;
|
|
int64_t usecs = strtoll(buf, &end, 10);
|
|
if (*end != '\0' || usecs < 0 || usecs == LLONG_MAX) {
|
|
return -1;
|
|
}
|
|
|
|
*usecs_ptr = usecs;
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
store_new (
|
|
sqlite3 *db,
|
|
int64_t date_added
|
|
) {
|
|
#define CREATE_TABLE_EX(tbl, cols, extra) \
|
|
{ STR_WITHLEN("CREATE TABLE `moz_" tbl "` (" cols")" extra) }
|
|
#define CREATE_TABLE(tbl, cols) CREATE_TABLE_EX(tbl, cols, "")
|
|
#define CREATE_INDEX_(u, tbl, idx, cols) \
|
|
{ STR_WITHLEN("CREATE " u "INDEX `moz_" tbl "_" idx "`" \
|
|
" ON `moz_" tbl "` (" cols ")") }
|
|
#define CREATE_INDEX(tbl, idx, cols) CREATE_INDEX_("", tbl, idx, cols)
|
|
#define CREATE_UINDEX(tbl, idx, cols) CREATE_INDEX_("UNIQUE ", tbl, idx, cols)
|
|
|
|
struct sql_withlen {
|
|
char const *str;
|
|
size_t len;
|
|
} const tables[] = {
|
|
// moz_bookmarks
|
|
CREATE_TABLE("bookmarks",
|
|
"`id`" " INTEGER PRIMARY KEY, "
|
|
"`type`" " INT, "
|
|
"`fk`" " INT DEFAULT NULL, "
|
|
"`parent`" " INT, "
|
|
"`position`" " INT, "
|
|
"`title`" " TEXT, "
|
|
"`keyword_id`" " INT, "
|
|
"`folder_type`" " TEXT, "
|
|
"`dateAdded`" " INT, "
|
|
"`lastModified`" " INT, "
|
|
"`guid`" " TEXT, "
|
|
"`syncStatus`" " INT NOT NULL DEFAULT 0, "
|
|
"`syncChangeCounter`" " INT NOT NULL DEFAULT 1"
|
|
),
|
|
CREATE_INDEX("bookmarks", "itemindex", "`fk`, `type`"),
|
|
CREATE_INDEX("bookmarks", "parentindex", "`parent`, `position`"),
|
|
CREATE_INDEX("bookmarks", "itemlastmodifiedindex",
|
|
"`fk`, `lastModified`"),
|
|
CREATE_INDEX("bookmarks", "dateaddedindex", "`dateAdded`"),
|
|
CREATE_UINDEX("bookmarks", "guid_uniqueindex", "`guid`"),
|
|
// moz_origins
|
|
CREATE_TABLE("origins",
|
|
"`id`" " INTEGER PRIMARY KEY, "
|
|
"`prefix`" " TEXT NOT NULL, "
|
|
"`host`" " TEXT NOT NULL, "
|
|
"`frecency`" " INT NOT NULL, "
|
|
"`recalc_frecency`" " INT NOT NULL DEFAULT 0, "
|
|
"`alt_frecency`" " INT, "
|
|
"`recalc_alt_frecency`" " INT NOT NULL DEFAULT 0, "
|
|
"UNIQUE (`prefix`, `host`)"
|
|
),
|
|
// moz_places
|
|
CREATE_TABLE("places",
|
|
"`id`" " INTEGER PRIMARY KEY, "
|
|
"`url`" " TEXT, "
|
|
"`title`" " TEXT, "
|
|
"`rev_host`" " TEXT, "
|
|
"`visit_count`" " INT DEFAULT 0, "
|
|
"`hidden`" " INT DEFAULT 0 NOT NULL, "
|
|
"`typed`" " INT DEFAULT 0 NOT NULL, "
|
|
"`frecency`" " INT DEFAULT -1 NOT NULL, "
|
|
"`last_visit_date`" " INT, "
|
|
"`guid`" " TEXT, "
|
|
"`foreign_count`" " INT DEFAULT 0 NOT NULL, "
|
|
"`url_hash`" " INT DEFAULT 0 NOT NULL, "
|
|
"`description`" " TEXT, "
|
|
"`preview_image_url`" " TEXT, "
|
|
"`site_name`" " TEXT, "
|
|
"`origin_id`" " INT REFERENCES `moz_origins`(`id`), "
|
|
"`recalc_frecency`" " INT NOT NULL DEFAULT 0, "
|
|
"`alt_frecency`" " INT, "
|
|
"`recalc_alt_frecency`" " INT NOT NULL DEFAULT 0"
|
|
),
|
|
CREATE_INDEX("places", "url_hashindex", "`url_hash`"),
|
|
CREATE_INDEX("places", "hostindex", "`rev_host`"),
|
|
CREATE_INDEX("places", "visitcount", "`visit_count`"),
|
|
CREATE_INDEX("places", "frecencyindex", "`frecency`"),
|
|
CREATE_INDEX("places", "lastvisitdateindex", "`last_visit_date`"),
|
|
CREATE_UINDEX("places", "guid_uniqueindex", "`guid`"),
|
|
CREATE_INDEX("places", "originidindex", "`origin_id`"),
|
|
CREATE_INDEX("places", "altfrecencyindex", "`alt_frecency`"),
|
|
// moz_keywords
|
|
CREATE_TABLE("keywords",
|
|
"`id`" " INTEGER PRIMARY KEY AUTOINCREMENT, "
|
|
"`keyword`" " TEXT UNIQUE, "
|
|
"`place_id`" " INT, "
|
|
"`post_data`" " TEXT"
|
|
),
|
|
CREATE_UINDEX("keywords", "placepostdata_uniqueindex",
|
|
"`place_id`, `post_data`"),
|
|
// moz_anno_attributes
|
|
CREATE_TABLE("anno_attributes",
|
|
"`id`" " INTEGER PRIMARY KEY, "
|
|
"`name`" " TEXT UNIQUE NOT NULL"
|
|
),
|
|
// moz_annos
|
|
CREATE_TABLE("annos",
|
|
"`id`" " INTEGER PRIMARY KEY, "
|
|
"`place_id`" " INT NOT NULL, "
|
|
"`anno_attribute_id`" " INT, "
|
|
"`content`" " TEXT, "
|
|
"`flags`" " INT DEFAULT 0, "
|
|
"`expiration`" " INT DEFAULT 0, "
|
|
"`type`" " INT DEFAULT 0, "
|
|
"`dateAdded`" " INT DEFAULT 0, "
|
|
"`lastModified`" " INT DEFAULT 0"
|
|
),
|
|
CREATE_UINDEX("annos", "placeattributeindex",
|
|
"`place_id`, `anno_attribute_id`"),
|
|
// moz_bookmarks_deleted
|
|
CREATE_TABLE("bookmarks_deleted",
|
|
"`guid`" " TEXT PRIMARY KEY, "
|
|
"`dateRemoved`" " INT NOT NULL DEFAULT 0"
|
|
),
|
|
// moz_historyvisits
|
|
CREATE_TABLE("historyvisits",
|
|
"`id`" " INTEGER PRIMARY KEY, "
|
|
"`from_visit`" " INT, "
|
|
"`place_id`" " INT, "
|
|
"`visit_date`" " INT, "
|
|
"`visit_type`" " INT, "
|
|
"`session`" " INT, "
|
|
"`source`" " INT DEFAULT 0 NOT NULL, "
|
|
"`triggeringPlaceId`" " INT"
|
|
),
|
|
CREATE_INDEX("historyvisits", "placedateindex",
|
|
"`place_id`, `visit_date`"),
|
|
CREATE_INDEX("historyvisits", "fromindex", "`from_visit`"),
|
|
CREATE_INDEX("historyvisits", "dateindex", "`visit_date`"),
|
|
// moz_inputhistory
|
|
CREATE_TABLE("inputhistory",
|
|
"`place_id`" " INT NOT NULL, "
|
|
"`input`" " TEXT NOT NULL, "
|
|
"`use_count`" " INT, "
|
|
"PRIMARY KEY (`place_id`, `input`)"
|
|
),
|
|
// moz_items_annos
|
|
CREATE_TABLE("items_annos",
|
|
"`id` " " INTEGER PRIMARY KEY, "
|
|
"`item_id`" " INT NOT NULL, "
|
|
"`anno_attribute_id`" " INT, "
|
|
"`content`" " TEXT, "
|
|
"`flags`" " INT DEFAULT 0, "
|
|
"`expiration`" " INT DEFAULT 0, "
|
|
"`type`" " INT DEFAULT 0, "
|
|
"`dateAdded`" " INT DEFAULT 0, "
|
|
"`lastModified`" " INT DEFAULT 0"
|
|
),
|
|
CREATE_UINDEX("items_annos", "itemattributeindex",
|
|
"`item_id`, `anno_attribute_id`"),
|
|
// moz_meta
|
|
CREATE_TABLE_EX("meta",
|
|
"`key`" " TEXT PRIMARY KEY, "
|
|
"`value`" " NOT NULL",
|
|
"WITHOUT ROWID"
|
|
),
|
|
// moz_places_metadata
|
|
CREATE_TABLE("places_metadata",
|
|
"`id`" " INTEGER PRIMARY KEY, "
|
|
"`place_id`" " INT NOT NULL, "
|
|
"`referrer_place_id`" " INT, "
|
|
"`created_at`" " INT NOT NULL DEFAULT 0, "
|
|
"`updated_at`" " INT NOT NULL DEFAULT 0, "
|
|
"`total_view_time`" " INT NOT NULL DEFAULT 0, "
|
|
"`typing_time`" " INT NOT NULL DEFAULT 0, "
|
|
"`key_presses`" " INT NOT NULL DEFAULT 0, "
|
|
"`scrolling_time`" " INT NOT NULL DEFAULT 0, "
|
|
"`scrolling_distance`" " INT NOT NULL DEFAULT 0, "
|
|
"`document_type`" " INT NOT NULL DEFAULT 0, "
|
|
"`search_query_id`" " INT, "
|
|
"FOREIGN KEY (`place_id`) REFERENCES `moz_places`(`id`)"
|
|
" ON DELETE CASCADE, "
|
|
"FOREIGN KEY (`referrer_place_id`) REFERENCES `moz_places`(`id`)"
|
|
" ON DELETE CASCADE, "
|
|
"FOREIGN KEY (`search_query_id`)"
|
|
" REFERENCES `moz_places_metadata_search_queries`(`id`)"
|
|
" ON DELETE CASCADE"
|
|
" CHECK(`place_id` != `referrer_place_id`)"
|
|
),
|
|
CREATE_UINDEX("places_metadata", "placecreated_uniqueindex",
|
|
"`place_id`, `created_at`"),
|
|
CREATE_INDEX("places_metadata", "referrerindex",
|
|
"`referrer_place_id`"),
|
|
// moz_places_metadata_search_queries
|
|
CREATE_TABLE("places_metadata_search_queries",
|
|
"`id`" " INTEGER PRIMARY KEY, "
|
|
"`terms`" " TEXT NOT NULL UNIQUE"
|
|
),
|
|
// moz_previews_tombstones
|
|
CREATE_TABLE_EX("previews_tombstones",
|
|
"`hash`" " TEXT PRIMARY KEY",
|
|
"WITHOUT ROWID"
|
|
),
|
|
};
|
|
for (size_t i = 0; i < sizeof(tables) / sizeof(struct sql_withlen); ++i) {
|
|
struct sql_withlen const *sql = tables + i;
|
|
|
|
if (0 != db_exec(db, sql->str, sql->len, NULL, NULL)) {
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
#define MOZBM_ROOT(id_, parent_, pos_, title_, guid_) \
|
|
{ \
|
|
.id = (id_), \
|
|
.parent_id = (parent_), \
|
|
.pos = (pos_), \
|
|
.title = (title_), \
|
|
.title_len = strlen(title_), \
|
|
.guid = (guid_), \
|
|
}
|
|
struct mozbm const bmroots[] = {
|
|
MOZBM_ROOT(1, 0, 0, "", BOOKMARKS_ROOT_GUID),
|
|
MOZBM_ROOT(2, 1, 0, "menu", "menu________"),
|
|
MOZBM_ROOT(3, 1, 1, "toolbar", "toolbar_____"),
|
|
MOZBM_ROOT(4, 1, 2, "tags", TAGS_ROOT_GUID),
|
|
MOZBM_ROOT(5, 1, 3, "unfiled", "unfiled_____"),
|
|
MOZBM_ROOT(6, 1, 4, "mobile", "mobile______"),
|
|
};
|
|
char const *sql =
|
|
"INSERT INTO `moz_bookmarks` (`id`, `type`, `parent`, `position`, "
|
|
"`title`, `dateAdded`, `lastModified`, `guid`) "
|
|
"VALUES (?1, 2, ?2, ?3, ?4, ?5, ?5, ?6)";
|
|
sqlite3_stmt *stmt = db_prepare(db, sql, strlen(sql), true);
|
|
if (unlikely(stmt == NULL)) {
|
|
return -1;
|
|
}
|
|
int status = -1;
|
|
|
|
for (size_t i = 0; i < sizeof(bmroots) / sizeof(struct mozbm); ++i) {
|
|
struct mozbm const *bmroot = bmroots + i;
|
|
|
|
struct db_stmt_bind_item const bind_items[] = {
|
|
DB_QUERY_BIND_INT64(bmroot->id),
|
|
DB_QUERY_BIND_INT64(bmroot->parent_id),
|
|
DB_QUERY_BIND_INT64(bmroot->pos),
|
|
DB_QUERY_BIND_TEXT(bmroot->title, bmroot->title_len),
|
|
DB_QUERY_BIND_INT64(date_added),
|
|
DB_QUERY_BIND_TEXT(bmroot->guid, GUID_STR_LEN),
|
|
};
|
|
size_t bind_cnt = DB_BIND_ITEMS_CNT(bind_items);
|
|
if (0 != db_query(stmt, bind_items, bind_cnt, true, NULL, NULL)) {
|
|
goto end;
|
|
}
|
|
}
|
|
status = 0;
|
|
|
|
end:
|
|
sqlite3_finalize(stmt);
|
|
return status;
|
|
}
|
|
|
|
static int
|
|
store_sync (
|
|
sqlite3 *db
|
|
) {
|
|
int err = sqlite3_wal_checkpoint(db, "main");
|
|
if (err != SQLITE_OK) {
|
|
log_printf("sqlite3_wal_checkpoint(): %s", sqlite3_errmsg(db));
|
|
return -db_errno(err);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
tag_entry_add (
|
|
struct backend_ctx *ctx,
|
|
uint64_t parent_id,
|
|
char const *name,
|
|
size_t name_len,
|
|
struct bookmarkfs_bookmark_stat *stat_buf
|
|
) {
|
|
struct mozbm cols = {
|
|
.id = stat_buf->id,
|
|
.parent_id = parent_id,
|
|
};
|
|
int status = mozbm_lookup_id(ctx, &cols);
|
|
if (status < 0) {
|
|
return status;
|
|
}
|
|
if (cols.place_id == 0) {
|
|
return -EPERM;
|
|
}
|
|
status = tag_entry_lookup(ctx, &cols);
|
|
if (status == 0) {
|
|
return -EEXIST;
|
|
}
|
|
if (status != -ENOENT) {
|
|
return status;
|
|
}
|
|
|
|
int64_t date_added = usecs_now(&stat_buf->mtime);
|
|
if (unlikely(date_added < 0)) {
|
|
return -EIO;
|
|
}
|
|
char guid_buf[GUID_STR_LEN];
|
|
cols.date_added = date_added;
|
|
cols.guid = gen_random_guid(guid_buf);
|
|
status = mozbm_insert(ctx, &cols);
|
|
if (status < 0) {
|
|
return status;
|
|
}
|
|
|
|
status = bookmark_do_lookup(ctx, parent_id, name, name_len,
|
|
BOOKMARKFS_BOOKMARK_TYPE(TAG), stat_buf);
|
|
if (status < 0) {
|
|
if (status == ENOENT) {
|
|
status = EPERM;
|
|
}
|
|
return status;
|
|
}
|
|
status = mozplace_addref_id(ctx, cols.place_id);
|
|
if (status < 0) {
|
|
return status;
|
|
}
|
|
status = mozbm_mtime_update(ctx, parent_id, &date_added);
|
|
if (status < 0) {
|
|
return status;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
tag_entry_delete (
|
|
struct backend_ctx *ctx,
|
|
uint64_t parent_id,
|
|
char const *name,
|
|
size_t name_len
|
|
) {
|
|
struct bookmarkfs_bookmark_stat stat_buf;
|
|
int status = bookmark_do_lookup(ctx, parent_id, name, name_len,
|
|
BOOKMARKFS_BOOKMARK_TYPE(TAG), &stat_buf);
|
|
if (status < 0) {
|
|
return status;
|
|
}
|
|
|
|
struct mozbm cols = {
|
|
.id = stat_buf.id,
|
|
.parent_id = parent_id,
|
|
};
|
|
status = mozbm_lookup_id(ctx, &cols);
|
|
if (status < 0) {
|
|
return status;
|
|
}
|
|
if (unlikely(cols.place_id == 0)) {
|
|
// Bad bookmark file. Tag entry should not be a directory.
|
|
return -EIO;
|
|
}
|
|
status = tag_entry_lookup(ctx, &cols);
|
|
if (status < 0) {
|
|
return status;
|
|
}
|
|
|
|
status = mozbm_delete(ctx, cols.id, false, false);
|
|
if (status < 0) {
|
|
return status;
|
|
}
|
|
status = mozbm_mtime_update(ctx, parent_id, NULL);
|
|
if (status < 0) {
|
|
return status;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
tag_entry_lookup (
|
|
struct backend_ctx *ctx,
|
|
struct mozbm *cols
|
|
) {
|
|
sqlite3_stmt **stmt_ptr = &ctx->stmts[STMT_TAG_ENTRY_LOOKUP];
|
|
char const *sql = "SELECT `id` FROM `moz_bookmarks` "
|
|
"WHERE `parent` = ? AND `fk` = ? LIMIT 1";
|
|
|
|
ssize_t nrows;
|
|
DO_QUERY(ctx, stmt_ptr, sql, db_query_i64_cb, &cols->id, nrows, , ,
|
|
DB_QUERY_BIND_INT64(cols->parent_id),
|
|
DB_QUERY_BIND_INT64(cols->place_id),
|
|
);
|
|
if (nrows < 0) {
|
|
return nrows;
|
|
}
|
|
if (nrows == 0) {
|
|
return -ENOENT;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int64_t
|
|
timespec_to_usecs (
|
|
struct timespec const *ts
|
|
) {
|
|
return ts->tv_sec * 1000000 + ts->tv_nsec / 1000;
|
|
}
|
|
|
|
static int
|
|
txn_begin (
|
|
struct backend_ctx *ctx
|
|
) {
|
|
return db_txn_begin(ctx->db, &ctx->stmts[STMT_BEGIN]);
|
|
}
|
|
|
|
static int
|
|
txn_end (
|
|
struct backend_ctx *ctx
|
|
) {
|
|
int status = db_txn_commit(ctx->db, &ctx->stmts[STMT_COMMIT]);
|
|
if (status < 0) {
|
|
return txn_rollback(ctx, status);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
txn_rollback (
|
|
struct backend_ctx *ctx,
|
|
int old_status
|
|
) {
|
|
db_txn_rollback(ctx->db, &ctx->stmts[STMT_ROLLBACK]);
|
|
return old_status;
|
|
}
|
|
|
|
static int64_t
|
|
usecs_now (
|
|
struct timespec *ts_buf
|
|
) {
|
|
struct timespec ts_tmp;
|
|
if (ts_buf == NULL) {
|
|
ts_buf = &ts_tmp;
|
|
}
|
|
xgetrealtime(ts_buf);
|
|
return timespec_to_usecs(ts_buf);
|
|
}
|
|
|
|
#endif /* defined(BOOKMARKFS_BACKEND_FIREFOX_WRITE) */
|
|
|
|
static int
|
|
bookmark_do_get (
|
|
struct backend_ctx *ctx,
|
|
uint64_t id,
|
|
int xattr_id,
|
|
struct bookmark_get_ctx *qctx
|
|
) {
|
|
#define BOOKMARK_GET_(cols, join) "SELECT CASE ? " cols "END " \
|
|
"FROM `moz_bookmarks` `b` " join "WHERE `b`.`id` = ?"
|
|
#define BOOKMARK_GET(cols) BOOKMARK_GET_(cols \
|
|
"WHEN " STRINGIFY(BM_XATTR_DATE_ADDED) " THEN `b`.`dateAdded` " \
|
|
"WHEN " STRINGIFY(BM_XATTR_KEYWORD) " THEN " \
|
|
"(SELECT `keyword` FROM `moz_keywords` `k` " \
|
|
"WHERE `k`.`place_id` = `b`.`fk`) ", )
|
|
#define BOOKMARK_GET_EX BOOKMARK_GET_( \
|
|
"WHEN " STRINGIFY(BM_XATTR_NULL) " THEN `p`.`url` " \
|
|
"WHEN " STRINGIFY(BM_XATTR_DESC) " THEN `p`.`description` " \
|
|
, "LEFT JOIN `moz_places` `p` ON `b`.`fk` = `p`.`id` ")
|
|
#define BOOKMARK_GET_WITH_GUID \
|
|
BOOKMARK_GET("WHEN " STRINGIFY(BM_XATTR_GUID) " THEN `b`.`guid` ")
|
|
#define BOOKMARK_GET_WITH_TITLE \
|
|
BOOKMARK_GET("WHEN " STRINGIFY(BM_XATTR_TITLE) " THEN `b`.`title` ")
|
|
|
|
sqlite3_stmt **stmt_ptr = &ctx->stmts[STMT_BOOKMARK_GET_EX];
|
|
char const *sql = BOOKMARK_GET_EX;
|
|
if (xattr_id >= MOZBM_XATTR_START) {
|
|
stmt_ptr = &ctx->stmts[STMT_BOOKMARK_GET];
|
|
sql = BOOKMARK_GET_WITH_GUID;
|
|
if (ctx->flags & BACKEND_FILENAME_GUID) {
|
|
sql = BOOKMARK_GET_WITH_TITLE;
|
|
}
|
|
}
|
|
|
|
ssize_t nrows;
|
|
DO_QUERY(ctx, stmt_ptr, sql, bookmark_get_cb, qctx, nrows, ,
|
|
{
|
|
qctx->tags_root_id = ctx->tags_root_id;
|
|
},
|
|
DB_QUERY_BIND_INT64(xattr_id),
|
|
DB_QUERY_BIND_INT64(id),
|
|
);
|
|
if (nrows < 0) {
|
|
return nrows;
|
|
}
|
|
if (nrows == 0) {
|
|
return -ESTALE;
|
|
}
|
|
if (qctx->status < 0) {
|
|
return qctx->status;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
bookmark_do_list (
|
|
struct backend_ctx *ctx,
|
|
uint64_t id,
|
|
off_t off,
|
|
uint32_t flags,
|
|
struct bookmark_list_ctx *qctx
|
|
) {
|
|
#define BOOKMARK_LIST_(col, join) \
|
|
"SELECT `b`.`id`, `b`.`position`, " col "FROM `moz_bookmarks` `b` " join \
|
|
"WHERE `b`.`parent` = ? AND `b`.`position` >= ? ORDER BY `b`.`position`"
|
|
// Type: 1 -> bookmark; 2 -> folder; 3 -> separator.
|
|
#define BOOKMARK_LIST_NOJOIN_(col) BOOKMARK_LIST_(col ", 1 - `b`.`type` ", )
|
|
#define BOOKMARK_LIST_EX_COLS_ \
|
|
"ifnull(length(CAST(`p`.`url` AS BLOB)), -1), `b`.`lastModified`, " \
|
|
"`p`.`last_visit_date`"
|
|
#define BOOKMARK_LIST_EX_(col) \
|
|
BOOKMARK_LIST_(col ", " BOOKMARK_LIST_EX_COLS_ " ", \
|
|
"LEFT JOIN `moz_places` `p` ON `b`.`fk` = `p`.`id` ")
|
|
#define BOOKMARK_LIST_TITLE BOOKMARK_LIST_NOJOIN_("`b`.`title`")
|
|
#define BOOKMARK_LIST_GUID BOOKMARK_LIST_NOJOIN_("`b`.`guid`")
|
|
#define BOOKMARK_LIST_TITLE_EX BOOKMARK_LIST_EX_("`b`.`title`")
|
|
#define BOOKMARK_LIST_GUID_EX BOOKMARK_LIST_EX_("`b`.`guid`")
|
|
|
|
#define BOOKMARK_COL_BY_FK_(col) \
|
|
"SELECT `t`.`" col "` FROM `moz_bookmarks` `t` " \
|
|
"WHERE `t`.`fk` = `b`.`fk` ORDER BY `t`.`id` LIMIT 1"
|
|
#define BOOKMARK_LIST_TAG_TITLE \
|
|
BOOKMARK_LIST_NOJOIN_("(" BOOKMARK_COL_BY_FK_("title") ")")
|
|
#define BOOKMARK_LIST_TAG_GUID \
|
|
BOOKMARK_LIST_NOJOIN_("(" BOOKMARK_COL_BY_FK_("guid") ")")
|
|
#define BOOKMARK_LIST_TAG_TITLE_EX \
|
|
BOOKMARK_LIST_EX_("(" BOOKMARK_COL_BY_FK_("title") ")")
|
|
#define BOOKMARK_LIST_TAG_GUID_EX \
|
|
BOOKMARK_LIST_EX_("(" BOOKMARK_COL_BY_FK_("guid") ")")
|
|
|
|
#define BOOKMARK_LIST_KEYWORD_(cols, join) \
|
|
"SELECT min(`b`.`id`), `k`.`id`, `k`.`keyword`" cols " " \
|
|
"FROM `moz_keywords` `k` " join \
|
|
"JOIN `moz_bookmarks` `b` ON `k`.`place_id` = `b`.`fk` " \
|
|
"WHERE `k`.`id` >= ?2 GROUP BY `k`.`place_id` ORDER BY `k`.`id`"
|
|
#define BOOKMARK_LIST_KEYWORD BOOKMARK_LIST_KEYWORD_(,)
|
|
#define BOOKMARK_LIST_KEYWORD_EX \
|
|
BOOKMARK_LIST_KEYWORD_(", " BOOKMARK_LIST_EX_COLS_, \
|
|
"JOIN `moz_places` `p` ON `k`.`place_id` = `p`.`id` ")
|
|
|
|
uint32_t bookmark_type = flags & BOOKMARKFS_BOOKMARK_TYPE_MASK;
|
|
bookmark_type >>= BOOKMARKFS_BOOKMARK_TYPE_SHIFT;
|
|
if (bookmark_type == BOOKMARKFS_BOOKMARK_TYPE_TAG
|
|
&& id == ctx->tags_root_id
|
|
) {
|
|
bookmark_type = BOOKMARKFS_BOOKMARK_TYPE_BOOKMARK;
|
|
}
|
|
|
|
bool with_stat = flags & BOOKMARK_FLAG(LIST_WITHSTAT);
|
|
int stmt_idx_table[3][2] = {
|
|
{ STMT_BOOKMARK_LIST, STMT_BOOKMARK_LIST_EX },
|
|
{ STMT_BOOKMARK_LIST_TAG, STMT_BOOKMARK_LIST_TAG_EX },
|
|
{ STMT_BOOKMARK_LIST_KEYWORD, STMT_BOOKMARK_LIST_KEYWORD_EX },
|
|
};
|
|
sqlite3_stmt **stmt_ptr
|
|
= &ctx->stmts[stmt_idx_table[bookmark_type][with_stat]];
|
|
|
|
bool filename_is_guid = ctx->flags & BACKEND_FILENAME_GUID;
|
|
char const *sql_table[3][2][2] = {
|
|
{ { BOOKMARK_LIST_TITLE, BOOKMARK_LIST_TITLE_EX },
|
|
{ BOOKMARK_LIST_GUID, BOOKMARK_LIST_GUID_EX }, },
|
|
{ { BOOKMARK_LIST_TAG_TITLE, BOOKMARK_LIST_TAG_TITLE_EX },
|
|
{ BOOKMARK_LIST_TAG_GUID, BOOKMARK_LIST_TAG_GUID_EX }, },
|
|
{ { BOOKMARK_LIST_KEYWORD, BOOKMARK_LIST_KEYWORD_EX },
|
|
{ BOOKMARK_LIST_KEYWORD, BOOKMARK_LIST_KEYWORD_EX }, },
|
|
};
|
|
char const *sql = sql_table[bookmark_type][filename_is_guid][with_stat];
|
|
|
|
ssize_t nrows;
|
|
DO_QUERY(ctx, stmt_ptr, sql, qctx->row_func, qctx, nrows, ,
|
|
{
|
|
bool name_distinct = filename_is_guid
|
|
|| !BOOKMARKFS_BOOKMARK_IS_TYPE(flags, BOOKMARK)
|
|
|| (ctx->flags & BACKEND_ASSUME_TITLE_DISTINCT);
|
|
|
|
qctx->tags_root_id = ctx->tags_root_id;
|
|
qctx->check_name = !name_distinct;
|
|
qctx->with_stat = with_stat;
|
|
},
|
|
DB_QUERY_BIND_INT64(id),
|
|
DB_QUERY_BIND_INT64(off),
|
|
);
|
|
if (nrows < 0) {
|
|
return nrows;
|
|
}
|
|
return qctx->status;
|
|
}
|
|
|
|
static int
|
|
bookmark_do_lookup (
|
|
struct backend_ctx *ctx,
|
|
uint64_t id,
|
|
char const *name,
|
|
size_t name_len,
|
|
uint32_t flags,
|
|
struct bookmarkfs_bookmark_stat *stat_buf
|
|
) {
|
|
#define BOOKMARK_LOOKUP_(join, where) \
|
|
"SELECT `b`.`id`, `b`.`lastModified`, " \
|
|
"`p`.`last_visit_date`, length(CAST(`p`.`url` AS BLOB)) " \
|
|
"FROM `moz_bookmarks` `b` " \
|
|
join "JOIN `moz_places` `p` ON `b`.`fk` = `p`.`id` WHERE " where
|
|
#define BOOKMARK_LOOKUP_ASSOC_(col) \
|
|
BOOKMARK_LOOKUP_("LEFT ", "`b`.`parent` = ? AND `b`.`" col "` = ? ") \
|
|
"ORDER BY `b`.`position` LIMIT 1"
|
|
#define BOOKMARK_LOOKUP_ID_(join, val) \
|
|
BOOKMARK_LOOKUP_(join, "`b`.`id` = " val " ")
|
|
#define BOOKMARK_LOOKUP_ID BOOKMARK_LOOKUP_ID_("LEFT ", "?")
|
|
#define BOOKMARK_LOOKUP_GUID BOOKMARK_LOOKUP_ASSOC_("guid")
|
|
#define BOOKMARK_LOOKUP_TITLE BOOKMARK_LOOKUP_ASSOC_("title")
|
|
|
|
#define BOOKMARK_TAG_BY_PARENT_ \
|
|
"SELECT `fk` FROM `moz_bookmarks` WHERE `parent` = ?"
|
|
#define BOOKMARK_ID_BY_TAG_(idx, col) \
|
|
"SELECT `id` FROM `moz_bookmarks` " \
|
|
"INDEXED BY `moz_bookmarks_" idx "index` " \
|
|
"WHERE `fk` IN (" BOOKMARK_TAG_BY_PARENT_ ") AND `" col "` = ? " \
|
|
"ORDER BY `id` LIMIT 1"
|
|
#define BOOKMARK_LOOKUP_TAG_(idx, col) \
|
|
BOOKMARK_LOOKUP_ID_(, "(" BOOKMARK_ID_BY_TAG_(idx, col) ")")
|
|
#define BOOKMARK_LOOKUP_TAG_GUID BOOKMARK_LOOKUP_TAG_("guid_unique", "guid")
|
|
#define BOOKMARK_LOOKUP_TAG_TITLE BOOKMARK_LOOKUP_TAG_("item", "title")
|
|
|
|
#define PLACE_ID_BY_KEYWORD(val) \
|
|
"SELECT `place_id` FROM `moz_keywords` WHERE `keyword` = " val
|
|
#define BOOKMARK_LOOKUP_PLACE_ID_(val) \
|
|
BOOKMARK_LOOKUP_(, "`b`.`fk` = " val " ORDER BY `b`.`id` LIMIT 1")
|
|
#define BOOKMARK_LOOKUP_KEYWORD_(val) \
|
|
BOOKMARK_LOOKUP_PLACE_ID_("(" PLACE_ID_BY_KEYWORD(val) ")")
|
|
#define BOOKMARK_LOOKUP_KEYWORD BOOKMARK_LOOKUP_KEYWORD_("?2")
|
|
|
|
int stmt_idx = STMT_BOOKMARK_LOOKUP_ID;
|
|
char const *sql = BOOKMARK_LOOKUP_ID;
|
|
if (name == NULL) {
|
|
if (BOOKMARKFS_BOOKMARK_IS_TYPE(flags, KEYWORD)) {
|
|
*stat_buf = (struct bookmarkfs_bookmark_stat) {
|
|
.value_len = -1,
|
|
};
|
|
return 0;
|
|
}
|
|
goto query;
|
|
}
|
|
|
|
bool filename_is_guid = ctx->flags & BACKEND_FILENAME_GUID;
|
|
#ifdef BOOKMARKFS_BACKEND_FIREFOX_WRITE
|
|
if (filename_is_guid && (flags & BOOKMARK_FLAG(LOOKUP_VALIDATE_GUID))) {
|
|
if (!is_valid_guid(name, name_len)) {
|
|
return -EPERM;
|
|
}
|
|
}
|
|
#endif /* defined(BOOKMARKFS_BACKEND_FIREFOX_WRITE) */
|
|
|
|
uint32_t bookmark_type = flags & BOOKMARKFS_BOOKMARK_TYPE_MASK;
|
|
switch (bookmark_type) {
|
|
case BOOKMARKFS_BOOKMARK_TYPE(TAG):
|
|
if (id != ctx->tags_root_id) {
|
|
stmt_idx = STMT_BOOKMARK_LOOKUP_TAG_ASSOC;
|
|
break;
|
|
}
|
|
bookmark_type = BOOKMARKFS_BOOKMARK_TYPE(BOOKMARK);
|
|
// fallthrough
|
|
case BOOKMARKFS_BOOKMARK_TYPE(BOOKMARK):
|
|
stmt_idx = STMT_BOOKMARK_LOOKUP_ASSOC;
|
|
break;
|
|
|
|
case BOOKMARKFS_BOOKMARK_TYPE(KEYWORD):
|
|
stmt_idx = STMT_BOOKMARK_LOOKUP_KEYWORD;
|
|
break;
|
|
}
|
|
bookmark_type >>= BOOKMARKFS_BOOKMARK_TYPE_SHIFT;
|
|
|
|
char const *sql_table[3][2] = {
|
|
{ BOOKMARK_LOOKUP_TITLE, BOOKMARK_LOOKUP_GUID },
|
|
{ BOOKMARK_LOOKUP_TAG_TITLE, BOOKMARK_LOOKUP_TAG_GUID },
|
|
{ BOOKMARK_LOOKUP_KEYWORD, BOOKMARK_LOOKUP_KEYWORD },
|
|
};
|
|
sql = sql_table[bookmark_type][filename_is_guid];
|
|
|
|
query: ;
|
|
sqlite3_stmt **stmt_ptr = &ctx->stmts[stmt_idx];
|
|
|
|
ssize_t nrows;
|
|
struct bookmark_lookup_ctx qctx;
|
|
DO_QUERY(ctx, stmt_ptr, sql, bookmark_lookup_cb, &qctx, nrows, ,
|
|
{
|
|
qctx.tags_root_id = name == NULL ? UINT64_MAX : ctx->tags_root_id;
|
|
qctx.stat_buf = stat_buf;
|
|
},
|
|
DB_QUERY_BIND_INT64(id),
|
|
DB_QUERY_BIND_TEXT(name, name_len),
|
|
);
|
|
if (nrows < 0) {
|
|
return nrows;
|
|
}
|
|
if (nrows == 0) {
|
|
return -ENOENT;
|
|
}
|
|
int status = qctx.status;
|
|
if (status == -ENOENT && name == NULL) {
|
|
status = -ESTALE;
|
|
}
|
|
return status;
|
|
}
|
|
|
|
static int
|
|
bookmark_check_cb (
|
|
void *user_data,
|
|
sqlite3_stmt *stmt
|
|
) {
|
|
struct bookmark_list_ctx *ctx = user_data;
|
|
debug_assert(ctx->check_name);
|
|
|
|
int64_t id = sqlite3_column_int64(stmt, 0);
|
|
if (unlikely(!is_valid_id(id))) {
|
|
log_printf("bad bookmark ID %" PRIi64, id);
|
|
goto fail;
|
|
}
|
|
|
|
int64_t position = sqlite3_column_int64(stmt, 1);
|
|
if (unlikely(position < 0)) {
|
|
goto fail;
|
|
}
|
|
ctx->next = position + 1;
|
|
|
|
if (-2 == sqlite3_column_int64(stmt, 3)) {
|
|
// skip separator
|
|
return 0;
|
|
}
|
|
|
|
char const *name = (char const *)sqlite3_column_text(stmt, 2);
|
|
size_t name_len = sqlite3_column_bytes(stmt, 2);
|
|
if (unlikely(name == NULL)) {
|
|
name = "";
|
|
}
|
|
uint64_t extra;
|
|
int result;
|
|
if (0 != validate_filename_fsck(name, name_len, &result, &extra)) {
|
|
// Only valid names shall be inserted into dentry map.
|
|
goto found;
|
|
}
|
|
|
|
struct hashmap *map = ctx->dentry_map;
|
|
if (map == NULL) {
|
|
return 0;
|
|
}
|
|
union hashmap_key key = {
|
|
.ptr = &(struct bookmark_name_key) {
|
|
.val = name,
|
|
.len = name_len,
|
|
},
|
|
};
|
|
unsigned long hashcode = hash_digest(name, name_len);
|
|
struct bookmark_dentry *dentry = hashmap_search(map, key, hashcode, NULL);
|
|
if (dentry == NULL) {
|
|
dentry = xmalloc(sizeof(*dentry) + name_len);
|
|
dentry->id = id;
|
|
dentry->hashcode = hashcode;
|
|
dentry->name_len = name_len;
|
|
memcpy(dentry->name, name, name_len);
|
|
|
|
if (map == NULL) {
|
|
map = hashmap_create(dentmap_comp, dentmap_hash);
|
|
ctx->dentry_map = map;
|
|
}
|
|
hashmap_insert(map, hashcode, dentry);
|
|
return 0;
|
|
}
|
|
if (dentry->id == (uint64_t)id) {
|
|
return 0;
|
|
}
|
|
extra = dentry->id;
|
|
result = BOOKMARKFS_FSCK_RESULT_NAME_DUPLICATE;
|
|
|
|
found:
|
|
ctx->status = ctx->callback.check(ctx->user_data, result, id, extra, name);
|
|
return ctx->status;
|
|
|
|
fail:
|
|
ctx->status = -EIO;
|
|
return 1;
|
|
}
|
|
|
|
static int
|
|
bookmark_get_cb (
|
|
void *user_data,
|
|
sqlite3_stmt *stmt
|
|
) {
|
|
struct bookmark_get_ctx *ctx = user_data;
|
|
|
|
char const *val = (char const *)sqlite3_column_text(stmt, 0);
|
|
size_t len = sqlite3_column_bytes(stmt, 0);
|
|
if (unlikely(val == NULL)) {
|
|
val = "";
|
|
}
|
|
|
|
ctx->status = ctx->callback(ctx->user_data, val, len);
|
|
return 1;
|
|
}
|
|
|
|
static int
|
|
bookmark_list_cb (
|
|
void *user_data,
|
|
sqlite3_stmt *stmt
|
|
) {
|
|
struct bookmark_list_ctx *ctx = user_data;
|
|
|
|
struct bookmarkfs_bookmark_entry entry;
|
|
|
|
int64_t id = sqlite3_column_int64(stmt, 0);
|
|
if (unlikely(!is_valid_id(id))) {
|
|
log_printf("bad bookmark ID %" PRIi64, id);
|
|
goto fail;
|
|
}
|
|
if (unlikely((uint64_t)id == ctx->tags_root_id)) {
|
|
return 0;
|
|
}
|
|
entry.stat.id = id;
|
|
|
|
int64_t position = sqlite3_column_int64(stmt, 1);
|
|
if (unlikely(position < 0)) {
|
|
goto fail;
|
|
}
|
|
entry.off = position + 1;
|
|
|
|
char const *name = (char const *)sqlite3_column_text(stmt, 2);
|
|
size_t name_len = sqlite3_column_bytes(stmt, 2);
|
|
if (unlikely(name == NULL)) {
|
|
name = "";
|
|
}
|
|
if (0 != validate_filename(name, name_len, NULL)) {
|
|
debug_printf("bad bookmark name '%s' (ID: %" PRIi64 ")", name, id);
|
|
return 0;
|
|
}
|
|
|
|
if (!ctx->check_name) {
|
|
goto set_name;
|
|
}
|
|
|
|
struct hashmap *map = ctx->dentry_map;
|
|
if (map == NULL) {
|
|
map = hashmap_create(dentmap_comp, dentmap_hash);
|
|
ctx->dentry_map = map;
|
|
}
|
|
|
|
union hashmap_key key = {
|
|
.ptr = &(struct bookmark_name_key) {
|
|
.val = name,
|
|
.len = name_len,
|
|
},
|
|
};
|
|
unsigned long hashcode = hash_digest(name, name_len);
|
|
struct bookmark_dentry *dentry = hashmap_search(map, key, hashcode, NULL);
|
|
if (dentry != NULL) {
|
|
if (dentry->id == (uint64_t)id) {
|
|
goto set_name;
|
|
}
|
|
debug_printf("duplicate bookmark name '%s' (ID: %" PRIi64 ")",
|
|
name, id);
|
|
return 0;
|
|
}
|
|
|
|
set_name:
|
|
entry.name = name;
|
|
|
|
entry.stat.value_len = sqlite3_column_int64(stmt, 3);
|
|
if (ctx->with_stat) {
|
|
usecs_to_timespec(&entry.stat.mtime, sqlite3_column_int64(stmt, 4));
|
|
usecs_to_timespec(&entry.stat.atime, sqlite3_column_int64(stmt, 5));
|
|
}
|
|
|
|
ctx->status = ctx->callback.list(ctx->user_data, &entry);
|
|
if (ctx->status == 0 && ctx->check_name && dentry == NULL) {
|
|
dentry = xmalloc(sizeof(*dentry) + name_len);
|
|
dentry->id = id;
|
|
dentry->hashcode = hashcode;
|
|
dentry->name_len = name_len;
|
|
memcpy(dentry->name, name, name_len);
|
|
|
|
hashmap_insert(map, hashcode, dentry);
|
|
}
|
|
return ctx->status;
|
|
|
|
fail:
|
|
ctx->status = -EIO;
|
|
return 1;
|
|
}
|
|
|
|
static int
|
|
bookmark_lookup_cb (
|
|
void *user_data,
|
|
sqlite3_stmt *stmt
|
|
) {
|
|
struct bookmark_lookup_ctx *ctx = user_data;
|
|
|
|
struct bookmarkfs_bookmark_stat *stat_buf = ctx->stat_buf;
|
|
|
|
int64_t id = sqlite3_column_int64(stmt, 0);
|
|
if (unlikely(!is_valid_id(id))) {
|
|
ctx->status = -EIO;
|
|
return 1;
|
|
}
|
|
if (unlikely((uint64_t)id == ctx->tags_root_id)) {
|
|
ctx->status = -ENOENT;
|
|
return 1;
|
|
}
|
|
stat_buf->id = id;
|
|
|
|
usecs_to_timespec(&stat_buf->mtime, sqlite3_column_int64(stmt, 1));
|
|
usecs_to_timespec(&stat_buf->atime, sqlite3_column_int64(stmt, 2));
|
|
|
|
ssize_t len = -1;
|
|
if (SQLITE_INTEGER == sqlite3_column_type(stmt, 3)) {
|
|
len = sqlite3_column_int64(stmt, 3);
|
|
if (unlikely(len < 0)) {
|
|
ctx->status = -EIO;
|
|
return 1;
|
|
}
|
|
}
|
|
stat_buf->value_len = len;
|
|
ctx->status = 0;
|
|
return 1;
|
|
}
|
|
|
|
static int
|
|
dentmap_comp (
|
|
union hashmap_key key,
|
|
void const *entry
|
|
) {
|
|
struct bookmark_name_key const *name = key.ptr;
|
|
struct bookmark_dentry const *dentry = entry;
|
|
|
|
if (name->len != dentry->name_len) {
|
|
return -1;
|
|
}
|
|
return memcmp(name->val, dentry->name, name->len);
|
|
}
|
|
|
|
static unsigned long
|
|
dentmap_hash (
|
|
void const *entry
|
|
) {
|
|
struct bookmark_dentry const *dentry = entry;
|
|
|
|
return dentry->hashcode;
|
|
}
|
|
|
|
static void
|
|
free_blcookie (
|
|
struct bookmark_lcookie *cookie
|
|
) {
|
|
free_dentmap(cookie->dentry_map);
|
|
free(cookie);
|
|
}
|
|
|
|
static void
|
|
free_dentmap (
|
|
struct hashmap *dentry_map
|
|
) {
|
|
if (dentry_map == NULL) {
|
|
return;
|
|
}
|
|
|
|
hashmap_foreach(dentry_map, free_dentmap_entry, NULL);
|
|
hashmap_destroy(dentry_map);
|
|
}
|
|
|
|
static void
|
|
free_dentmap_entry (
|
|
void *UNUSED_VAR(user_data),
|
|
void *entry
|
|
) {
|
|
struct bookmark_dentry *dentry = entry;
|
|
|
|
free(dentry);
|
|
}
|
|
|
|
static int
|
|
get_xattr_id (
|
|
char const *name,
|
|
uint32_t flags
|
|
) {
|
|
if (name == NULL) {
|
|
return BM_XATTR_NULL;
|
|
}
|
|
if (0 == strcmp("date_added", name)) {
|
|
return BM_XATTR_DATE_ADDED;
|
|
}
|
|
if (0 == strcmp("description", name)) {
|
|
return BM_XATTR_DESC;
|
|
}
|
|
if (flags & BACKEND_FILENAME_GUID) {
|
|
if (0 == strcmp("title", name)) {
|
|
return BM_XATTR_TITLE;
|
|
}
|
|
} else {
|
|
if (0 == strcmp("guid", name)) {
|
|
return BM_XATTR_GUID;
|
|
}
|
|
}
|
|
if (0 == strcmp("keyword", name)) {
|
|
return BM_XATTR_KEYWORD;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
static int64_t
|
|
get_data_version (
|
|
struct backend_ctx *ctx
|
|
) {
|
|
int64_t data_version = -1;
|
|
ssize_t nrows = db_exec(ctx->db, SQL_PRAGMA("data_version"),
|
|
&ctx->stmts[STMT_DATA_VERSION], &data_version);
|
|
if (nrows < 0) {
|
|
return nrows;
|
|
}
|
|
if (unlikely(data_version < 0)) {
|
|
log_printf("bad data version %" PRIi64 ", should be of uint32 value",
|
|
data_version);
|
|
}
|
|
return data_version;
|
|
}
|
|
|
|
static bool
|
|
is_valid_id (
|
|
int64_t id
|
|
) {
|
|
if (id <= 0) {
|
|
return false;
|
|
}
|
|
if ((uint64_t)id > BOOKMARKFS_MAX_ID) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static void
|
|
usecs_to_timespec (
|
|
struct timespec *ts_buf,
|
|
int64_t microsecs
|
|
) {
|
|
if (unlikely(microsecs < 0)) {
|
|
microsecs = 0;
|
|
}
|
|
|
|
ts_buf->tv_sec = microsecs / 1000000;
|
|
ts_buf->tv_nsec = (microsecs % 1000000) * 1000;
|
|
}
|
|
|
|
static int
|
|
parse_mntopts (
|
|
struct bookmarkfs_conf_opt const *opts,
|
|
uint32_t flags,
|
|
struct parsed_mntopts *parsed_opts
|
|
) {
|
|
unsigned backend_flags = BACKEND_EXCLUSIVE_LOCK;
|
|
if (flags & BOOKMARKFS_BACKEND_FSCK_ONLY) {
|
|
goto end;
|
|
}
|
|
if (flags & BOOKMARKFS_BACKEND_READONLY) {
|
|
backend_flags = 0;
|
|
}
|
|
|
|
BACKEND_OPT_START(opts)
|
|
BACKEND_OPT_KEY("filename") {
|
|
BACKEND_OPT_VAL_START
|
|
BACKEND_OPT_VAL("guid") {
|
|
backend_flags |= BACKEND_FILENAME_GUID;
|
|
}
|
|
BACKEND_OPT_VAL("title") {
|
|
backend_flags &= ~BACKEND_FILENAME_GUID;
|
|
}
|
|
BACKEND_OPT_VAL_END
|
|
}
|
|
BACKEND_OPT_KEY("lock") {
|
|
BACKEND_OPT_VAL_START
|
|
BACKEND_OPT_VAL("exclusive") {
|
|
backend_flags |= BACKEND_EXCLUSIVE_LOCK;
|
|
}
|
|
BACKEND_OPT_VAL("normal") {
|
|
backend_flags &= ~BACKEND_EXCLUSIVE_LOCK;
|
|
}
|
|
BACKEND_OPT_VAL_END
|
|
}
|
|
BACKEND_OPT_KEY("assume_title_distinct") {
|
|
BACKEND_OPT_NO_VAL
|
|
backend_flags |= BACKEND_ASSUME_TITLE_DISTINCT;
|
|
}
|
|
BACKEND_OPT_END
|
|
|
|
end:
|
|
parsed_opts->flags = backend_flags;
|
|
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"
|
|
" lock=<exclusive/normal> Database connection locking mode\n"
|
|
" assume_title_distinct\n"
|
|
"\n";
|
|
} else if (flags & BOOKMARKFS_FRONTEND_MKFS) {
|
|
options = "Options:\n"
|
|
" date_added=<timestamp> Override date_added attribute\n"
|
|
"\n";
|
|
}
|
|
printf("Firefox 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-firefox %d.%d.%d\n",
|
|
BOOKMARKFS_VER_MAJOR, BOOKMARKFS_VER_MINOR, BOOKMARKFS_VER_PATCH);
|
|
puts(BOOKMARKFS_FEATURE_STRING(DEBUG, "debug"));
|
|
puts(BOOKMARKFS_FEATURE_STRING(BACKEND_FIREFOX_WRITE, "write"));
|
|
|
|
bookmarkfs_print_lib_version("\n");
|
|
}
|
|
|
|
static int
|
|
store_init (
|
|
sqlite3 *db,
|
|
uint64_t *bookmarks_root_id_ptr,
|
|
uint64_t *tags_root_id_ptr
|
|
) {
|
|
int status = -EIO;
|
|
|
|
int64_t user_version;
|
|
if (1 != db_exec(db, SQL_PRAGMA("user_version"), NULL, &user_version)) {
|
|
return status;
|
|
}
|
|
// The oldest schema version supported by modern Firefox is 52,
|
|
// which was used in Firefox 62-68. Fortunately, it has not changed
|
|
// in a way that makes it incompatible with this backend.
|
|
//
|
|
// Schema version 78 is the latest version, used since Firefox 132.
|
|
// Bump this version whenever a new schema version is available
|
|
// (after ensuring that no incompatible changes are made).
|
|
if (user_version < 52 || user_version > 78) {
|
|
log_printf("unsupported schema version %" PRIi64, user_version);
|
|
return status;
|
|
}
|
|
|
|
char const *sql = "SELECT `id` FROM `moz_bookmarks` WHERE `guid` = ?";
|
|
sqlite3_stmt *stmt = db_prepare(db, sql, strlen(sql), false);
|
|
if (unlikely(stmt == NULL)) {
|
|
return status;
|
|
}
|
|
|
|
struct store_check_args {
|
|
struct db_stmt_bind_item bind_item;
|
|
uint64_t id;
|
|
} check_args[] = {
|
|
{ .bind_item = DB_QUERY_BIND_TEXT(BOOKMARKS_ROOT_GUID, GUID_STR_LEN) },
|
|
{ .bind_item = DB_QUERY_BIND_TEXT(TAGS_ROOT_GUID, GUID_STR_LEN) },
|
|
};
|
|
size_t num_args = sizeof(check_args) / sizeof(struct store_check_args);
|
|
|
|
for (size_t idx = 0; idx < num_args; ++idx) {
|
|
struct store_check_args *args = check_args + idx;
|
|
|
|
ssize_t nrows = db_query(stmt, &args->bind_item, 1, true,
|
|
store_check_cb, &args->id);
|
|
if (nrows <= 0) {
|
|
if (nrows == 0) {
|
|
log_puts("bookmark root not found");
|
|
}
|
|
goto end;
|
|
}
|
|
}
|
|
|
|
if (!is_valid_id(check_args[0].id) || !is_valid_id(check_args[1].id)) {
|
|
goto end;
|
|
}
|
|
*bookmarks_root_id_ptr = check_args[0].id;
|
|
*tags_root_id_ptr = check_args[1].id;
|
|
status = 0;
|
|
|
|
end:
|
|
sqlite3_finalize(stmt);
|
|
return status;
|
|
}
|
|
|
|
static int
|
|
store_check_cb (
|
|
void *user_data,
|
|
sqlite3_stmt *stmt
|
|
) {
|
|
int64_t *id_ptr = user_data;
|
|
|
|
*id_ptr = sqlite3_column_int64(stmt, 0);
|
|
return 1;
|
|
}
|
|
|
|
static int
|
|
backend_create (
|
|
struct bookmarkfs_backend_conf const *conf,
|
|
struct bookmarkfs_backend_create_resp *resp
|
|
) {
|
|
bool readonly = conf->flags & BOOKMARKFS_BACKEND_READONLY;
|
|
if (!readonly) {
|
|
#ifdef BOOKMARKFS_BACKEND_FIREFOX_WRITE
|
|
struct timespec now;
|
|
xgetrealtime(&now);
|
|
if (!valid_ts_sec(now.tv_sec)) {
|
|
log_puts("bad system time");
|
|
return -1;
|
|
}
|
|
|
|
int minver = 3035000; // required for the RETURNING clause
|
|
int vernum = sqlite3_libversion_number();
|
|
if (vernum < minver) {
|
|
log_printf("sqlite version too low (%d<%d)", vernum, minver);
|
|
return -1;
|
|
}
|
|
#else /* !defined(BOOKMARKFS_BACKEND_FIREFOX_WRITE) */
|
|
log_puts("write support is not enabled on this build");
|
|
return -1;
|
|
#endif /* defined(BOOKMARKFS_BACKEND_FIREFOX_WRITE) */
|
|
}
|
|
|
|
struct parsed_mntopts opts = { 0 };
|
|
if (0 != parse_mntopts(conf->opts, conf->flags, &opts)) {
|
|
return -1;
|
|
}
|
|
|
|
sqlite3 *db = db_open(conf->store_path);
|
|
if (db == NULL) {
|
|
return -1;
|
|
}
|
|
|
|
struct db_conf_item const conf_items[] = {
|
|
{ SQLITE_DBCONFIG_DEFENSIVE, 1 },
|
|
// Trigger is required for foreign keys to work
|
|
{ SQLITE_DBCONFIG_ENABLE_TRIGGER, !readonly },
|
|
{ SQLITE_DBCONFIG_ENABLE_VIEW, 0 },
|
|
{ SQLITE_DBCONFIG_TRUSTED_SCHEMA, 0 },
|
|
};
|
|
if (0 != db_config(db, conf_items, DB_CONFIG_ITEMS_CNT(conf_items))) {
|
|
goto close_db;
|
|
}
|
|
|
|
struct db_pragma_item pragmas[5] = {
|
|
SQL_PRAGMA_ITEM("locking_mode", "normal"),
|
|
SQL_PRAGMA_ITEM("journal_mode", "wal"),
|
|
SQL_PRAGMA_ITEM("synchronous", "1"),
|
|
};
|
|
if (opts.flags & BACKEND_EXCLUSIVE_LOCK) {
|
|
pragmas[0] = SQL_PRAGMA_ITEM("locking_mode", "exclusive");
|
|
}
|
|
if (!(conf->flags & BOOKMARKFS_BACKEND_NO_SANDBOX)) {
|
|
// Cannot use file as temp store in sandbox mode,
|
|
// since SQLite does not use *at() for filesystem operations.
|
|
pragmas[3] = SQL_PRAGMA_ITEM("temp_store", "2");
|
|
}
|
|
if (!readonly) {
|
|
// moz_places_extra, moz_places_metadata, ...
|
|
pragmas[4] = SQL_PRAGMA_ITEM("foreign_keys", "1");
|
|
}
|
|
if (0 != db_pragma(db, pragmas, DB_PRAGMA_ITEMS_CNT(pragmas))) {
|
|
goto close_db;
|
|
}
|
|
|
|
uint64_t bookmarks_root_id = UINT64_MAX;
|
|
uint64_t tags_root_id = UINT64_MAX;
|
|
if (conf->flags & BOOKMARKFS_BACKEND_NO_SANDBOX) {
|
|
if (0 != db_check(db)) {
|
|
goto close_db;
|
|
}
|
|
// Defer initialization in sandbox mode, so that
|
|
// user-provided data is only read after entering sandbox.
|
|
if (0 != store_init(db, &bookmarks_root_id, &tags_root_id)) {
|
|
goto close_db;
|
|
}
|
|
} else {
|
|
// Persist -wal and -shm files in sandbox mode,
|
|
// since we're unable to unlink them.
|
|
if (0 != db_fcntl(db, SQLITE_FCNTL_PERSIST_WAL, 1)) {
|
|
goto close_db;
|
|
}
|
|
}
|
|
|
|
if (0 != db_register_safeincr(db)) {
|
|
goto close_db;
|
|
}
|
|
|
|
struct backend_ctx *ctx = xmalloc(sizeof(*ctx));
|
|
*ctx = (struct backend_ctx) {
|
|
.db = db,
|
|
.bookmarks_root_id = bookmarks_root_id,
|
|
.tags_root_id = tags_root_id,
|
|
.flags = conf->flags | opts.flags,
|
|
};
|
|
|
|
uint32_t resp_flags = BOOKMARKFS_BACKEND_HAS_KEYWORD;
|
|
if (opts.flags & BACKEND_EXCLUSIVE_LOCK) {
|
|
resp_flags |= BOOKMARKFS_BACKEND_EXCLUSIVE;
|
|
}
|
|
|
|
char const *xattr_names = "guid\0date_added\0description\0keyword\0";
|
|
if (opts.flags & BACKEND_FILENAME_GUID) {
|
|
xattr_names = "title\0date_added\0description\0keyword\0";
|
|
}
|
|
|
|
resp->name = "firefox";
|
|
resp->backend_ctx = ctx;
|
|
resp->bookmarks_root_id = bookmarks_root_id;
|
|
resp->tags_root_id = tags_root_id;
|
|
resp->xattr_names = xattr_names;
|
|
resp->flags = resp_flags;
|
|
return 0;
|
|
|
|
close_db:
|
|
sqlite3_close(db);
|
|
return -1;
|
|
}
|
|
|
|
static void
|
|
backend_destroy (
|
|
void *backend_ctx
|
|
) {
|
|
struct backend_ctx *ctx = backend_ctx;
|
|
if (ctx == NULL) {
|
|
return;
|
|
}
|
|
|
|
int end = PERSISTED_STMT_END;
|
|
#ifdef BOOKMARKFS_BACKEND_FIREFOX_WRITE
|
|
if (ctx->flags & BOOKMARKFS_BACKEND_READONLY) {
|
|
end = PERSISTED_STMT_WRITE_START;
|
|
}
|
|
#endif /* defined(BOOKMARKFS_BACKEND_FIREFOX_WRITE) */
|
|
for (int idx = 0; idx < end; ++idx) {
|
|
sqlite3_stmt *stmt = ctx->stmts[idx];
|
|
if (stmt != NULL) {
|
|
sqlite3_finalize(stmt);
|
|
}
|
|
}
|
|
|
|
#ifdef BOOKMARKFS_BACKEND_FIREFOX_WRITE
|
|
if (!(ctx->flags & BOOKMARKFS_BACKEND_READONLY)) {
|
|
store_sync(ctx->db);
|
|
}
|
|
#endif
|
|
sqlite3_close(ctx->db);
|
|
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)
|
|
&& !(flags & BOOKMARKFS_FRONTEND_MKFS)
|
|
) {
|
|
if (0 != bookmarkfs_lib_init()) {
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
// If you wish to use the backend in a multi-threaded environment,
|
|
// change the config to SQLITE_CONFIG_MULTITHREAD,
|
|
// thus it is MT-Safe as long as functions are never called concurrently
|
|
// with the same `backend_ctx`.
|
|
int status = sqlite3_config(SQLITE_CONFIG_SINGLETHREAD);
|
|
if (unlikely(status != SQLITE_OK)) {
|
|
log_printf("sqlite3_config(): %s", sqlite3_errstr(status));
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
backend_sandbox (
|
|
void *backend_ctx,
|
|
struct bookmarkfs_backend_create_resp *resp
|
|
) {
|
|
struct backend_ctx *ctx = backend_ctx;
|
|
|
|
if (ctx->flags & BOOKMARKFS_BACKEND_NO_SANDBOX) {
|
|
return 0;
|
|
}
|
|
|
|
// Currently there is no way to retrieve the file descriptors of the
|
|
// open database/-wal/-shm/... files using the SQLite3 public API,
|
|
// thus we're unable to exert fine-grained control over their capabilities.
|
|
if (unlikely(0 != sandbox_enter(-1, 0))) {
|
|
return -1;
|
|
}
|
|
|
|
// Deferred db init (see backend_create()).
|
|
// Processing untrusted data before entering sandbox is a potential
|
|
// vulnerability, and should be avoided if possible.
|
|
if (0 != store_init(ctx->db, &ctx->bookmarks_root_id,
|
|
&ctx->tags_root_id)
|
|
) {
|
|
return -1;
|
|
}
|
|
resp->bookmarks_root_id = ctx->bookmarks_root_id;
|
|
resp->tags_root_id = ctx->tags_root_id;
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
bookmark_check (
|
|
void *backend_ctx,
|
|
uint64_t parent_id,
|
|
struct bookmarkfs_fsck_data const *fsck_data,
|
|
uint32_t flags,
|
|
bookmarkfs_bookmark_check_cb *callback,
|
|
void *user_data,
|
|
void **cookie_ptr
|
|
) {
|
|
struct backend_ctx *ctx = backend_ctx;
|
|
|
|
if (ctx->flags & BACKEND_FILENAME_GUID) {
|
|
return -EPERM;
|
|
}
|
|
switch (flags & BOOKMARKFS_BOOKMARK_TYPE_MASK) {
|
|
case BOOKMARKFS_BOOKMARK_TYPE(TAG):
|
|
if (parent_id == ctx->tags_root_id) {
|
|
break;
|
|
}
|
|
// fallthrough
|
|
case BOOKMARKFS_BOOKMARK_TYPE(KEYWORD):
|
|
// TODO: support keyword fsck
|
|
return -EPERM;
|
|
}
|
|
|
|
struct hashmap *dentry_map = NULL;
|
|
struct bookmark_lcookie *cookie;
|
|
size_t idx = 0;
|
|
if (cookie_ptr != NULL) {
|
|
cookie = *cookie_ptr;
|
|
if (cookie != NULL) {
|
|
dentry_map = cookie->dentry_map;
|
|
idx = cookie->idx;
|
|
}
|
|
}
|
|
|
|
int status = 0;
|
|
if (callback == NULL) {
|
|
// fsck_rewind
|
|
free_dentmap(dentry_map);
|
|
dentry_map = NULL;
|
|
idx = 0;
|
|
goto end;
|
|
}
|
|
|
|
struct bookmark_list_ctx qctx;
|
|
qctx.dentry_map = dentry_map;
|
|
qctx.next = idx;
|
|
qctx.row_func = bookmark_check_cb;
|
|
qctx.callback.check = callback;
|
|
qctx.user_data = user_data;
|
|
if (fsck_data == NULL) {
|
|
qctx.status = 0;
|
|
status = bookmark_do_list(ctx, parent_id, idx, flags, &qctx);
|
|
} else {
|
|
debug_assert(!(ctx->flags & BOOKMARKFS_BACKEND_READONLY));
|
|
#ifdef BOOKMARKFS_BACKEND_FIREFOX_WRITE
|
|
status = fsck_apply(ctx, parent_id, fsck_data, &qctx);
|
|
#endif
|
|
}
|
|
dentry_map = qctx.dentry_map;
|
|
idx = qctx.next;
|
|
|
|
end:
|
|
if (cookie_ptr != NULL) {
|
|
if (cookie == NULL) {
|
|
cookie = xmalloc(sizeof(*cookie));
|
|
*cookie_ptr = cookie;
|
|
}
|
|
cookie->dentry_map = dentry_map;
|
|
cookie->idx = idx;
|
|
} else {
|
|
free_dentmap(dentry_map);
|
|
}
|
|
return status;
|
|
}
|
|
|
|
static int
|
|
bookmark_get (
|
|
void *backend_ctx,
|
|
uint64_t id,
|
|
char const *xattr_name,
|
|
bookmarkfs_bookmark_get_cb *callback,
|
|
void *user_data,
|
|
void **cookie_ptr
|
|
) {
|
|
struct backend_ctx *ctx = backend_ctx;
|
|
|
|
int xattr_id = get_xattr_id(xattr_name, ctx->flags);
|
|
if (xattr_id < 0) {
|
|
return -ENOATTR;
|
|
}
|
|
|
|
if (cookie_ptr == NULL) {
|
|
goto query;
|
|
}
|
|
int64_t data_version = get_data_version(ctx);
|
|
if (data_version < 0) {
|
|
return data_version;
|
|
}
|
|
struct bookmark_gcookie *cookie = *cookie_ptr;
|
|
if (cookie != NULL) {
|
|
if (cookie->data_version == data_version) {
|
|
return -EAGAIN;
|
|
}
|
|
}
|
|
|
|
query: ;
|
|
struct bookmark_get_ctx qctx;
|
|
qctx.callback = callback;
|
|
qctx.user_data = user_data;
|
|
int status = bookmark_do_get(ctx, id, xattr_id, &qctx);
|
|
if (status < 0) {
|
|
return status;
|
|
}
|
|
|
|
if (cookie_ptr != NULL) {
|
|
if (cookie == NULL) {
|
|
cookie = xmalloc(sizeof(*cookie));
|
|
*cookie_ptr = cookie;
|
|
}
|
|
cookie->data_version = data_version;
|
|
}
|
|
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;
|
|
|
|
struct hashmap *dentry_map = NULL;
|
|
struct bookmark_lcookie *cookie;
|
|
if (cookie_ptr != NULL) {
|
|
cookie = *cookie_ptr;
|
|
if (cookie != NULL) {
|
|
dentry_map = cookie->dentry_map;
|
|
}
|
|
}
|
|
|
|
int status = 0;
|
|
if (callback == NULL) {
|
|
goto end;
|
|
}
|
|
if (off == 0) {
|
|
// rewinddir()
|
|
free_dentmap(dentry_map);
|
|
dentry_map = NULL;
|
|
}
|
|
|
|
struct bookmark_list_ctx qctx;
|
|
qctx.dentry_map = dentry_map;
|
|
qctx.row_func = bookmark_list_cb;
|
|
qctx.callback.list = callback;
|
|
qctx.user_data = user_data;
|
|
status = bookmark_do_list(ctx, id, off, flags, &qctx);
|
|
dentry_map = qctx.dentry_map;
|
|
|
|
end:
|
|
if (cookie_ptr != NULL) {
|
|
if (cookie == NULL) {
|
|
cookie = xmalloc(sizeof(*cookie));
|
|
cookie->idx = 0;
|
|
*cookie_ptr = cookie;
|
|
}
|
|
cookie->dentry_map = dentry_map;
|
|
} else {
|
|
free_dentmap(dentry_map);
|
|
}
|
|
return status;
|
|
}
|
|
|
|
static int
|
|
bookmark_lookup (
|
|
void *backend_ctx,
|
|
uint64_t id,
|
|
char const *name,
|
|
uint32_t flags,
|
|
struct bookmarkfs_bookmark_stat *stat_buf
|
|
) {
|
|
struct backend_ctx *ctx = backend_ctx;
|
|
|
|
size_t name_len = 0;
|
|
if (name != NULL) {
|
|
name_len = strlen(name);
|
|
}
|
|
return bookmark_do_lookup(ctx, id, name, name_len, flags, stat_buf);
|
|
}
|
|
|
|
static void
|
|
cookie_free (
|
|
void *UNUSED_VAR(backend_ctx),
|
|
void *cookie,
|
|
enum bookmarkfs_cookie_type cookie_type
|
|
) {
|
|
if (cookie == NULL) {
|
|
return;
|
|
}
|
|
|
|
switch (cookie_type) {
|
|
case BOOKMARKFS_COOKIE_TYPE_WATCH:
|
|
free(cookie);
|
|
break;
|
|
|
|
case BOOKMARKFS_COOKIE_TYPE_LIST:
|
|
free_blcookie(cookie);
|
|
break;
|
|
|
|
default:
|
|
unreachable();
|
|
}
|
|
}
|
|
|
|
#ifdef BOOKMARKFS_BACKEND_FIREFOX_WRITE
|
|
|
|
static int
|
|
backend_mkfs (
|
|
struct bookmarkfs_backend_conf const *conf
|
|
) {
|
|
struct parsed_mkfsopts opts = {
|
|
.date_added = -1,
|
|
};
|
|
if (0 != parse_mkfsopts(conf->opts, &opts)) {
|
|
return -1;
|
|
}
|
|
if (opts.date_added < 0) {
|
|
opts.date_added = usecs_now(NULL);
|
|
if (unlikely(opts.date_added < 0)) {
|
|
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;
|
|
|
|
// XXX: SQLite does not support opening a database file by fd.
|
|
//
|
|
// If any path component changes before the file actually gets opened,
|
|
// we may be writing to a different file than the one `fd` represents.
|
|
//
|
|
// Do not bother with /dev/fd/*, since on Linux, that file is a
|
|
// symbolic link, and SQLite choose to resolve it with readlink(2)
|
|
// instead of directly open it (see unixFullPathname() in src/os_unix.c),
|
|
// which does not solve the TOCTOU problem. Also, /dev/fd is not portable
|
|
// (e.g., FreeBSD does not mount fdescfs by default).
|
|
//
|
|
// Theoretically we could implement a "VFS shim" to workaround this
|
|
// problem, but that does not seem to be worthwhile.
|
|
//
|
|
// See also:
|
|
// - <https://sqlite.org/forum/forumpost/c15bf2e7df289a5f>
|
|
// - <https://sqlite.org/forum/forumpost/680cd395b4bc97c6>
|
|
sqlite3 *db = db_open(conf->store_path);
|
|
if (db == NULL) {
|
|
goto end;
|
|
}
|
|
|
|
struct db_pragma_item const pragmas[] = {
|
|
SQL_PRAGMA_ITEM("locking_mode", "exclusive"),
|
|
SQL_PRAGMA_ITEM("journal_mode", "memory"),
|
|
SQL_PRAGMA_ITEM("synchronous", "0"),
|
|
// Schema version 74 was used in Firefox 115-117.
|
|
//
|
|
// See the `mozilla::places::Database::InitSchema()` method
|
|
// in the mozilla-central codebase.
|
|
SQL_PRAGMA_ITEM("user_version", "74"),
|
|
};
|
|
if (0 != db_pragma(db, pragmas, DB_PRAGMA_ITEMS_CNT(pragmas))) {
|
|
goto end;
|
|
}
|
|
|
|
if (0 != db_txn_begin(db, NULL)) {
|
|
goto end;
|
|
}
|
|
if (0 != store_new(db, opts.date_added)) {
|
|
goto end;
|
|
}
|
|
status = db_txn_commit(db, NULL);
|
|
|
|
end:
|
|
sqlite3_close(db);
|
|
if (status == 0) {
|
|
status = xfsync(fd);
|
|
}
|
|
close(fd);
|
|
return status;
|
|
}
|
|
|
|
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));
|
|
debug_assert(name != NULL);
|
|
|
|
int status = txn_begin(ctx);
|
|
if (unlikely(status < 0)) {
|
|
return status;
|
|
}
|
|
|
|
size_t name_len = strlen(name);
|
|
bool is_dir = flags & BOOKMARK_FLAG(CREATE_DIR);
|
|
switch (flags & BOOKMARKFS_BOOKMARK_TYPE_MASK) {
|
|
case BOOKMARKFS_BOOKMARK_TYPE(TAG):
|
|
if (!is_dir) {
|
|
status = tag_entry_add(ctx, parent_id, name, name_len, stat_buf);
|
|
break;
|
|
}
|
|
// fallthrough
|
|
case BOOKMARKFS_BOOKMARK_TYPE(BOOKMARK):
|
|
status = bookmark_do_create(ctx, parent_id, name, name_len, is_dir,
|
|
stat_buf);
|
|
break;
|
|
|
|
case BOOKMARKFS_BOOKMARK_TYPE(KEYWORD):
|
|
status = keyword_create(ctx, name, name_len, stat_buf);
|
|
break;
|
|
|
|
default:
|
|
unreachable();
|
|
}
|
|
if (status < 0) {
|
|
return txn_rollback(ctx, status);
|
|
}
|
|
|
|
return txn_end(ctx);
|
|
}
|
|
|
|
static int
|
|
bookmark_delete (
|
|
void *backend_ctx,
|
|
uint64_t parent_id,
|
|
char const *name,
|
|
uint32_t flags
|
|
) {
|
|
struct backend_ctx *ctx = backend_ctx;
|
|
debug_assert(!(ctx->flags & BOOKMARKFS_BACKEND_READONLY));
|
|
debug_assert(name != NULL);
|
|
|
|
int status = txn_begin(ctx);
|
|
if (unlikely(status < 0)) {
|
|
return status;
|
|
}
|
|
|
|
size_t name_len = strlen(name);
|
|
bool is_dir = flags & BOOKMARK_FLAG(DELETE_DIR);
|
|
switch (flags & BOOKMARKFS_BOOKMARK_TYPE_MASK) {
|
|
case BOOKMARKFS_BOOKMARK_TYPE(TAG):
|
|
if (!is_dir) {
|
|
status = tag_entry_delete(ctx, parent_id, name, name_len);
|
|
break;
|
|
}
|
|
// fallthrough
|
|
case BOOKMARKFS_BOOKMARK_TYPE(BOOKMARK):
|
|
status = bookmark_do_delete(ctx, parent_id, name, name_len, is_dir);
|
|
break;
|
|
|
|
case BOOKMARKFS_BOOKMARK_TYPE(KEYWORD):
|
|
status = mozkw_delete(ctx, name, name_len);
|
|
break;
|
|
|
|
default:
|
|
unreachable();
|
|
}
|
|
if (status < 0) {
|
|
return txn_rollback(ctx, status);
|
|
}
|
|
|
|
return txn_end(ctx);
|
|
}
|
|
|
|
static int
|
|
bookmark_permute (
|
|
void *backend_ctx,
|
|
uint64_t parent_id,
|
|
enum bookmarkfs_permd_op op,
|
|
char const *name1,
|
|
char const *name2,
|
|
uint32_t flags
|
|
) {
|
|
struct backend_ctx *ctx = backend_ctx;
|
|
debug_assert(!(ctx->flags & BOOKMARKFS_BACKEND_READONLY));
|
|
|
|
switch (flags & BOOKMARKFS_BOOKMARK_TYPE_MASK) {
|
|
case BOOKMARKFS_BOOKMARK_TYPE(TAG):
|
|
if (parent_id == ctx->tags_root_id) {
|
|
break;
|
|
}
|
|
// fallthrough
|
|
case BOOKMARKFS_BOOKMARK_TYPE(KEYWORD):
|
|
return -EPERM;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
int status = txn_begin(ctx);
|
|
if (unlikely(status < 0)) {
|
|
return status;
|
|
}
|
|
|
|
struct mozbm cols1;
|
|
status = mozbm_lookup(ctx, parent_id, name1, name1_len, false, &cols1);
|
|
if (status < 0) {
|
|
goto fail;
|
|
}
|
|
|
|
struct mozbm cols2;
|
|
status = mozbm_lookup(ctx, parent_id, name2, name2_len, false, &cols2);
|
|
if (status < 0) {
|
|
goto fail;
|
|
}
|
|
if (cols1.id == cols2.id) {
|
|
goto fail;
|
|
}
|
|
|
|
if (op == BOOKMARKFS_PERMD_OP_SWAP) {
|
|
status = mozbm_pos_update(ctx, cols2.id, cols1.pos);
|
|
} else {
|
|
status = mozbm_pos_shift(ctx, parent_id, cols1.pos, &cols2.pos, op);
|
|
}
|
|
if (status <= 0) {
|
|
goto fail;
|
|
}
|
|
|
|
status = mozbm_pos_update(ctx, cols1.id, cols2.pos);
|
|
if (status < 0) {
|
|
goto fail;
|
|
}
|
|
|
|
status = mozbm_mtime_update(ctx, parent_id, NULL);
|
|
if (status < 0) {
|
|
goto fail;
|
|
}
|
|
|
|
return txn_end(ctx);
|
|
|
|
fail:
|
|
return txn_rollback(ctx, status);
|
|
}
|
|
|
|
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));
|
|
|
|
int bookmark_type = flags & BOOKMARKFS_BOOKMARK_TYPE_MASK;
|
|
switch (bookmark_type) {
|
|
case BOOKMARKFS_BOOKMARK_TYPE(BOOKMARK):
|
|
if (old_parent_id == ctx->bookmarks_root_id
|
|
|| new_parent_id == ctx->bookmarks_root_id
|
|
) {
|
|
return -EPERM;
|
|
}
|
|
break;
|
|
|
|
case BOOKMARKFS_BOOKMARK_TYPE(TAG):
|
|
debug_assert(old_parent_id == ctx->tags_root_id);
|
|
debug_assert(old_parent_id == new_parent_id);
|
|
break;
|
|
|
|
default:
|
|
unreachable();
|
|
}
|
|
|
|
bool name_changed = true;
|
|
size_t old_name_len = strlen(old_name);
|
|
size_t new_name_len = strlen(new_name);
|
|
if (old_name_len == new_name_len
|
|
&& 0 == memcmp(old_name, new_name, old_name_len)
|
|
) {
|
|
if (old_parent_id == new_parent_id) {
|
|
return 0;
|
|
}
|
|
name_changed = false;
|
|
}
|
|
|
|
int status = txn_begin(ctx);
|
|
if (unlikely(status < 0)) {
|
|
return status;
|
|
}
|
|
|
|
switch (bookmark_type) {
|
|
case BOOKMARKFS_BOOKMARK_TYPE(KEYWORD):
|
|
if (name_changed) {
|
|
status = mozkw_rename(ctx, old_name, new_name, flags);
|
|
if (status < 0) {
|
|
goto fail;
|
|
}
|
|
}
|
|
goto end;
|
|
}
|
|
|
|
struct mozbm old_cols;
|
|
status = mozbm_lookup(ctx, old_parent_id, old_name, old_name_len, false,
|
|
&old_cols);
|
|
if (status < 0) {
|
|
goto fail;
|
|
}
|
|
|
|
struct mozbm new_cols;
|
|
new_cols.pos = -1;
|
|
status = mozbm_lookup(ctx, new_parent_id, new_name, new_name_len, true,
|
|
&new_cols);
|
|
if (status < 0) {
|
|
// move
|
|
if (status != -ENOENT) {
|
|
goto fail;
|
|
}
|
|
} else {
|
|
// replace
|
|
if (flags & BOOKMARKFS_BOOKMARK_RENAME_NOREPLACE) {
|
|
status = -EEXIST;
|
|
goto fail;
|
|
}
|
|
if ((old_cols.place_id == 0) != (new_cols.place_id == 0)) {
|
|
status = (old_cols.place_id == 0) ? -ENOTDIR : -EISDIR;
|
|
goto fail;
|
|
}
|
|
status = mozbm_delete(ctx, new_cols.id, old_cols.place_id == 0, true);
|
|
if (status < 0) {
|
|
goto fail;
|
|
}
|
|
}
|
|
|
|
status = mozbm_move(ctx, old_cols.id, new_parent_id, new_cols.pos,
|
|
name_changed ? new_name : NULL, new_name_len);
|
|
if (status < 0) {
|
|
if (status == -EEXIST) {
|
|
// duplicate GUID
|
|
status = -EPERM;
|
|
}
|
|
goto fail;
|
|
}
|
|
|
|
int64_t mtime = -1;
|
|
status = mozbm_mtime_update(ctx, old_parent_id, &mtime);
|
|
if (status < 0) {
|
|
goto fail;
|
|
}
|
|
if (old_parent_id != new_parent_id) {
|
|
status = mozbm_mtime_update(ctx, new_parent_id, &mtime);
|
|
if (status < 0) {
|
|
goto fail;
|
|
}
|
|
}
|
|
if (ctx->flags & BOOKMARKFS_BACKEND_CTIME) {
|
|
status = mozbm_mtime_update(ctx, old_cols.id, &mtime);
|
|
if (status < 0) {
|
|
goto fail;
|
|
}
|
|
}
|
|
|
|
end:
|
|
return txn_end(ctx);
|
|
|
|
fail:
|
|
return txn_rollback(ctx, status);
|
|
}
|
|
|
|
static int
|
|
bookmark_set (
|
|
void *backend_ctx,
|
|
uint64_t id,
|
|
char const *xattr_name,
|
|
uint32_t flags,
|
|
void const *val,
|
|
size_t val_len
|
|
) {
|
|
struct backend_ctx *ctx = backend_ctx;
|
|
debug_assert(!(ctx->flags & BOOKMARKFS_BACKEND_READONLY));
|
|
|
|
struct mozbm bm_cols = {
|
|
.id = id,
|
|
.place_id = -1,
|
|
.date_added = -1,
|
|
.last_modified = -1,
|
|
};
|
|
struct mozplace place_cols = {
|
|
.last_visit_date = -1,
|
|
};
|
|
|
|
int xattr_id = MOZBM_XATTR_START;
|
|
if (flags & BOOKMARK_FLAG(SET_ATIME, SET_MTIME)) {
|
|
struct timespec const *times = val;
|
|
if (flags & BOOKMARK_FLAG(SET_ATIME)) {
|
|
if (!valid_ts_sec(times[0].tv_sec)) {
|
|
return -EINVAL;
|
|
}
|
|
place_cols.last_visit_date = timespec_to_usecs(×[0]);
|
|
--xattr_id;
|
|
}
|
|
if (flags & BOOKMARK_FLAG(SET_MTIME)) {
|
|
if (!valid_ts_sec(times[1].tv_sec)) {
|
|
return -EINVAL;
|
|
}
|
|
bm_cols.last_modified = timespec_to_usecs(×[1]);
|
|
}
|
|
} else {
|
|
xattr_id = get_xattr_id(xattr_name, ctx->flags);
|
|
switch (xattr_id) {
|
|
case BM_XATTR_NULL:
|
|
place_cols.url = val;
|
|
place_cols.url_len = val_len;
|
|
break;
|
|
|
|
case BM_XATTR_DESC:
|
|
place_cols.desc = val;
|
|
place_cols.desc_len = val_len;
|
|
break;
|
|
|
|
case BM_XATTR_TITLE:
|
|
if (NULL != memchr(val, '\0', val_len)) {
|
|
return -EINVAL;
|
|
}
|
|
bm_cols.title = val;
|
|
bm_cols.title_len = val_len;
|
|
break;
|
|
|
|
case BM_XATTR_GUID:
|
|
if (!is_valid_guid(val, val_len)) {
|
|
return -EINVAL;
|
|
}
|
|
bm_cols.guid = val;
|
|
break;
|
|
|
|
case BM_XATTR_DATE_ADDED:
|
|
if (0 != parse_usecs(val, val_len, &bm_cols.date_added)) {
|
|
return -EINVAL;
|
|
}
|
|
break;
|
|
|
|
case BM_XATTR_KEYWORD:
|
|
return -EPERM;
|
|
|
|
default:
|
|
return -ENOATTR;
|
|
}
|
|
|
|
if (xattr_id != BM_XATTR_NULL
|
|
&& ctx->flags & BOOKMARKFS_BACKEND_CTIME
|
|
) {
|
|
bm_cols.last_modified = usecs_now(NULL);
|
|
}
|
|
}
|
|
|
|
int status = txn_begin(ctx);
|
|
if (unlikely(status < 0)) {
|
|
return status;
|
|
}
|
|
status = mozbm_update(ctx, &bm_cols);
|
|
if (status < 0) {
|
|
goto fail;
|
|
}
|
|
if (bm_cols.place_id == 0) {
|
|
// Attempting to update moz_places fields on a directory.
|
|
status = -EPERM;
|
|
goto fail;
|
|
}
|
|
if (xattr_id >= MOZBM_XATTR_START) {
|
|
goto end;
|
|
}
|
|
|
|
place_cols.id = bm_cols.place_id;
|
|
status = mozplace_update(ctx, &place_cols);
|
|
if (status < 0) {
|
|
goto fail;
|
|
}
|
|
if (place_cols.id == bm_cols.place_id) {
|
|
goto end;
|
|
}
|
|
|
|
bm_cols.place_id = place_cols.id;
|
|
status = mozbm_update(ctx, &bm_cols);
|
|
if (status < 0) {
|
|
goto fail;
|
|
}
|
|
|
|
end:
|
|
return txn_end(ctx);
|
|
|
|
fail:
|
|
return txn_rollback(ctx, status);
|
|
}
|
|
|
|
static int
|
|
bookmark_sync (
|
|
void *backend_ctx
|
|
) {
|
|
struct backend_ctx *ctx = backend_ctx;
|
|
debug_assert(!(ctx->flags & BOOKMARKFS_BACKEND_READONLY));
|
|
|
|
return store_sync(ctx->db);
|
|
}
|
|
|
|
#endif /* defined(BOOKMARKFS_BACKEND_FIREFOX_WRITE) */
|
|
|
|
BOOKMARKFS_API
|
|
struct bookmarkfs_backend const bookmarkfs_backend_firefox = {
|
|
.backend_create = backend_create,
|
|
.backend_destroy = backend_destroy,
|
|
.backend_info = backend_info,
|
|
.backend_init = backend_init,
|
|
.backend_sandbox = backend_sandbox,
|
|
|
|
.bookmark_check = bookmark_check,
|
|
.bookmark_get = bookmark_get,
|
|
.bookmark_list = bookmark_list,
|
|
.bookmark_lookup = bookmark_lookup,
|
|
|
|
.cookie_free = cookie_free,
|
|
|
|
#ifdef BOOKMARKFS_BACKEND_FIREFOX_WRITE
|
|
.backend_mkfs = backend_mkfs,
|
|
|
|
.bookmark_create = bookmark_create,
|
|
.bookmark_delete = bookmark_delete,
|
|
.bookmark_permute = bookmark_permute,
|
|
.bookmark_rename = bookmark_rename,
|
|
.bookmark_set = bookmark_set,
|
|
.bookmark_sync = bookmark_sync,
|
|
#endif /* defined(BOOKMARKFS_BACKEND_FIREFOX_WRITE) */
|
|
};
|