/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */ #include "lib.h" #include "array.h" #include "fs-api.h" #include "istream.h" #include "str.h" #include "dict-transaction-memory.h" #include "dict-private.h" struct fs_dict { struct dict dict; struct fs *fs; }; struct fs_dict_iterate_context { struct dict_iterate_context ctx; char *path; enum dict_iterate_flags flags; pool_t value_pool; struct fs_iter *fs_iter; const char *values[2]; char *error; }; static int fs_dict_init(struct dict *driver, const char *uri, const struct dict_settings *set, struct dict **dict_r, const char **error_r) { struct fs_settings fs_set; struct fs *fs; struct fs_dict *dict; const char *p, *fs_driver, *fs_args; p = strchr(uri, ':'); if (p == NULL) { fs_driver = uri; fs_args = ""; } else { fs_driver = t_strdup_until(uri, p); fs_args = p+1; } i_zero(&fs_set); fs_set.base_dir = set->base_dir; if (fs_init(fs_driver, fs_args, &fs_set, &fs, error_r) < 0) return -1; dict = i_new(struct fs_dict, 1); dict->dict = *driver; dict->fs = fs; *dict_r = &dict->dict; return 0; } static void fs_dict_deinit(struct dict *_dict) { struct fs_dict *dict = (struct fs_dict *)_dict; fs_deinit(&dict->fs); i_free(dict); } /* Remove unsafe paths */ static const char *fs_dict_escape_key(const char *key) { const char *ptr; string_t *new_key = NULL; /* we take the slow path always if we see potential need for escaping */ while ((ptr = strstr(key, "/.")) != NULL) { /* move to the first dot */ const char *ptr2 = ptr + 1; /* find position of non-dot */ while (*ptr2 == '.') ptr2++; if (new_key == NULL) new_key = t_str_new(strlen(key)); str_append_data(new_key, key, ptr - key); /* if ptr2 is / or end of string, escape */ if (*ptr2 == '/' || *ptr2 == '\0') str_append(new_key, "/..."); else str_append(new_key, "/."); key = ptr + 2; } if (new_key == NULL) return key; str_append(new_key, key); return str_c(new_key); } static const char *fs_dict_get_full_key(const char *username, const char *key) { key = fs_dict_escape_key(key); if (str_begins(key, DICT_PATH_SHARED)) return key + strlen(DICT_PATH_SHARED); else if (str_begins(key, DICT_PATH_PRIVATE)) { return t_strdup_printf("%s/%s", username, key + strlen(DICT_PATH_PRIVATE)); } else { i_unreached(); } } static int fs_dict_lookup(struct dict *_dict, const struct dict_op_settings *set, pool_t pool, const char *key, const char **value_r, const char **error_r) { struct fs_dict *dict = (struct fs_dict *)_dict; struct fs_file *file; struct istream *input; const unsigned char *data; size_t size; const char *path; string_t *str; int ret; path = fs_dict_get_full_key(set->username, key); file = fs_file_init(dict->fs, path, FS_OPEN_MODE_READONLY); input = fs_read_stream(file, IO_BLOCK_SIZE); (void)i_stream_read(input); str = str_new(pool, i_stream_get_data_size(input)+1); while ((ret = i_stream_read_more(input, &data, &size)) > 0) { str_append_data(str, data, size); i_stream_skip(input, size); } i_assert(ret == -1); if (input->stream_errno == 0) { *value_r = str_c(str); ret = 1; } else { *value_r = NULL; if (input->stream_errno == ENOENT) ret = 0; else { *error_r = t_strdup_printf("read(%s) failed: %s", path, i_stream_get_error(input)); } } i_stream_unref(&input); fs_file_deinit(&file); return ret; } static struct dict_iterate_context * fs_dict_iterate_init(struct dict *_dict, const struct dict_op_settings *set, const char *path, enum dict_iterate_flags flags) { struct fs_dict *dict = (struct fs_dict *)_dict; struct fs_dict_iterate_context *iter; /* these flags are not supported for now */ i_assert((flags & DICT_ITERATE_FLAG_RECURSE) == 0); i_assert((flags & DICT_ITERATE_FLAG_EXACT_KEY) == 0); i_assert((flags & (DICT_ITERATE_FLAG_SORT_BY_KEY | DICT_ITERATE_FLAG_SORT_BY_VALUE)) == 0); iter = i_new(struct fs_dict_iterate_context, 1); iter->ctx.dict = _dict; iter->path = i_strdup(path); iter->flags = flags; iter->value_pool = pool_alloconly_create("iterate value pool", 128); iter->fs_iter = fs_iter_init(dict->fs, fs_dict_get_full_key(set->username, path), 0); return &iter->ctx; } static bool fs_dict_iterate(struct dict_iterate_context *ctx, const char **key_r, const char *const **values_r) { struct fs_dict_iterate_context *iter = (struct fs_dict_iterate_context *)ctx; struct fs_dict *dict = (struct fs_dict *)ctx->dict; const char *path, *error; int ret; if (iter->error != NULL) return FALSE; *key_r = fs_iter_next(iter->fs_iter); if (*key_r == NULL) { if (fs_iter_deinit(&iter->fs_iter, &error) < 0) { iter->error = i_strdup(error); return FALSE; } if (iter->path == NULL) return FALSE; path = fs_dict_get_full_key(ctx->set.username, iter->path); iter->fs_iter = fs_iter_init(dict->fs, path, 0); return fs_dict_iterate(ctx, key_r, values_r); } path = t_strconcat(iter->path, *key_r, NULL); if ((iter->flags & DICT_ITERATE_FLAG_NO_VALUE) != 0) { iter->values[0] = NULL; *key_r = path; return TRUE; } p_clear(iter->value_pool); struct dict_op_settings set = { .username = ctx->set.username, }; ret = fs_dict_lookup(ctx->dict, &set, iter->value_pool, path, &iter->values[0], &error); if (ret < 0) { /* I/O error */ iter->error = i_strdup(error); return FALSE; } else if (ret == 0) { /* file was just deleted, just skip to next one */ return fs_dict_iterate(ctx, key_r, values_r); } *key_r = path; *values_r = iter->values; return TRUE; } static int fs_dict_iterate_deinit(struct dict_iterate_context *ctx, const char **error_r) { struct fs_dict_iterate_context *iter = (struct fs_dict_iterate_context *)ctx; const char *error; int ret; if (fs_iter_deinit(&iter->fs_iter, &error) < 0 && iter->error == NULL) iter->error = i_strdup(error); ret = iter->error != NULL ? -1 : 0; *error_r = t_strdup(iter->error); pool_unref(&iter->value_pool); i_free(iter->path); i_free(iter->error); i_free(iter); return ret; } static struct dict_transaction_context * fs_dict_transaction_init(struct dict *_dict) { struct dict_transaction_memory_context *ctx; pool_t pool; pool = pool_alloconly_create("file dict transaction", 2048); ctx = p_new(pool, struct dict_transaction_memory_context, 1); dict_transaction_memory_init(ctx, _dict, pool); return &ctx->ctx; } static int fs_dict_write_changes(struct dict_transaction_memory_context *ctx, const char **error_r) { struct fs_dict *dict = (struct fs_dict *)ctx->ctx.dict; struct fs_file *file; const struct dict_transaction_memory_change *change; const char *key; int ret = 0; array_foreach(&ctx->changes, change) { key = fs_dict_get_full_key(ctx->ctx.set.username, change->key); switch (change->type) { case DICT_CHANGE_TYPE_SET: file = fs_file_init(dict->fs, key, FS_OPEN_MODE_REPLACE); if (fs_write(file, change->value.str, strlen(change->value.str)) < 0) { *error_r = t_strdup_printf( "fs_write(%s) failed: %s", key, fs_file_last_error(file)); ret = -1; } fs_file_deinit(&file); break; case DICT_CHANGE_TYPE_UNSET: file = fs_file_init(dict->fs, key, FS_OPEN_MODE_READONLY); if (fs_delete(file) < 0) { *error_r = t_strdup_printf( "fs_delete(%s) failed: %s", key, fs_file_last_error(file)); ret = -1; } fs_file_deinit(&file); break; case DICT_CHANGE_TYPE_INC: i_unreached(); } if (ret < 0) return -1; } return 0; } static void fs_dict_transaction_commit(struct dict_transaction_context *_ctx, bool async ATTR_UNUSED, dict_transaction_commit_callback_t *callback, void *context) { struct dict_transaction_memory_context *ctx = (struct dict_transaction_memory_context *)_ctx; struct dict_commit_result result = { .ret = 1 }; if (fs_dict_write_changes(ctx, &result.error) < 0) result.ret = -1; pool_unref(&ctx->pool); callback(&result, context); } struct dict dict_driver_fs = { .name = "fs", { .init = fs_dict_init, .deinit = fs_dict_deinit, .lookup = fs_dict_lookup, .iterate_init = fs_dict_iterate_init, .iterate = fs_dict_iterate, .iterate_deinit = fs_dict_iterate_deinit, .transaction_init = fs_dict_transaction_init, .transaction_commit = fs_dict_transaction_commit, .transaction_rollback = dict_transaction_memory_rollback, .set = dict_transaction_memory_set, .unset = dict_transaction_memory_unset, } };