diff options
Diffstat (limited to 'src/lib-dict/dict.c')
-rw-r--r-- | src/lib-dict/dict.c | 769 |
1 files changed, 769 insertions, 0 deletions
diff --git a/src/lib-dict/dict.c b/src/lib-dict/dict.c new file mode 100644 index 0000000..bda34ae --- /dev/null +++ b/src/lib-dict/dict.c @@ -0,0 +1,769 @@ +/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "array.h" +#include "guid.h" +#include "llist.h" +#include "ioloop.h" +#include "str.h" +#include "ioloop.h" +#include "dict-private.h" + +struct dict_commit_callback_ctx { + pool_t pool; + struct dict_commit_callback_ctx *prev, *next; + struct dict *dict; + struct event *event; + dict_transaction_commit_callback_t *callback; + struct dict_op_settings_private set; + struct timeout *to; + void *context; + struct dict_commit_result result; + bool delayed_callback:1; +}; + +struct dict_lookup_callback_ctx { + struct dict *dict; + struct event *event; + dict_lookup_callback_t *callback; + void *context; +}; + +static ARRAY(struct dict *) dict_drivers; + +static void +dict_commit_async_timeout(struct dict_commit_callback_ctx *ctx); + +static struct event_category event_category_dict = { + .name = "dict", +}; + +static struct dict *dict_driver_lookup(const char *name) +{ + struct dict *dict; + + array_foreach_elem(&dict_drivers, dict) { + if (strcmp(dict->name, name) == 0) + return dict; + } + return NULL; +} + +void dict_transaction_commit_async_noop_callback( + const struct dict_commit_result *result ATTR_UNUSED, + void *context ATTR_UNUSED) +{ + /* do nothing */ +} + +void dict_driver_register(struct dict *driver) +{ + if (!array_is_created(&dict_drivers)) + i_array_init(&dict_drivers, 8); + + if (dict_driver_lookup(driver->name) != NULL) { + i_fatal("dict_driver_register(%s): Already registered", + driver->name); + } + array_push_back(&dict_drivers, &driver); +} + +void dict_driver_unregister(struct dict *driver) +{ + struct dict *const *dicts; + unsigned int idx = UINT_MAX; + + array_foreach(&dict_drivers, dicts) { + if (*dicts == driver) { + idx = array_foreach_idx(&dict_drivers, dicts); + break; + } + } + i_assert(idx != UINT_MAX); + array_delete(&dict_drivers, idx, 1); + + if (array_count(&dict_drivers) == 0) + array_free(&dict_drivers); +} + +int dict_init(const char *uri, const struct dict_settings *set, + struct dict **dict_r, const char **error_r) +{ + struct dict_settings set_dup = *set; + struct dict *dict; + const char *p, *name, *error; + + p = strchr(uri, ':'); + if (p == NULL) { + *error_r = t_strdup_printf("Dictionary URI is missing ':': %s", + uri); + return -1; + } + + name = t_strdup_until(uri, p); + dict = dict_driver_lookup(name); + if (dict == NULL) { + *error_r = t_strdup_printf("Unknown dict module: %s", name); + return -1; + } + struct event *event = event_create(set->event_parent); + event_add_category(event, &event_category_dict); + event_add_str(event, "driver", dict->name); + event_set_append_log_prefix(event, t_strdup_printf("dict(%s): ", + dict->name)); + set_dup.event_parent = event; + if (dict->v.init(dict, p+1, &set_dup, dict_r, &error) < 0) { + *error_r = t_strdup_printf("dict %s: %s", name, error); + event_unref(&event); + return -1; + } + i_assert(*dict_r != NULL); + (*dict_r)->refcount++; + (*dict_r)->event = event; + e_debug(event_create_passthrough(event)->set_name("dict_created")->event(), + "dict created (uri=%s, base_dir=%s)", uri, set->base_dir); + + return 0; +} + +static void dict_ref(struct dict *dict) +{ + i_assert(dict->refcount > 0); + + dict->refcount++; +} + +static void dict_unref(struct dict **_dict) +{ + struct dict *dict = *_dict; + *_dict = NULL; + if (dict == NULL) + return; + struct event *event = dict->event; + i_assert(dict->refcount > 0); + if (--dict->refcount == 0) { + dict->v.deinit(dict); + e_debug(event_create_passthrough(event)-> + set_name("dict_destroyed")->event(), "dict destroyed"); + event_unref(&event); + } +} + +void dict_deinit(struct dict **_dict) +{ + struct dict *dict = *_dict; + + *_dict = NULL; + + i_assert(dict->iter_count == 0); + i_assert(dict->transaction_count == 0); + i_assert(dict->transactions == NULL); + i_assert(dict->commits == NULL); + dict_unref(&dict); +} + +void dict_wait(struct dict *dict) +{ + struct dict_commit_callback_ctx *commit, *next; + + e_debug(dict->event, "Waiting for dict to finish pending operations"); + if (dict->v.wait != NULL) + dict->v.wait(dict); + for (commit = dict->commits; commit != NULL; commit = next) { + next = commit->next; + dict_commit_async_timeout(commit); + } +} + +bool dict_switch_ioloop(struct dict *dict) +{ + struct dict_commit_callback_ctx *commit; + bool ret = FALSE; + + for (commit = dict->commits; commit != NULL; commit = commit->next) { + commit->to = io_loop_move_timeout(&commit->to); + ret = TRUE; + } + if (dict->v.switch_ioloop != NULL) { + if (dict->v.switch_ioloop(dict)) + return TRUE; + } + return ret; +} + +static bool dict_key_prefix_is_valid(const char *key, const char *username) +{ + if (str_begins(key, DICT_PATH_SHARED)) + return TRUE; + if (str_begins(key, DICT_PATH_PRIVATE)) { + i_assert(username != NULL && username[0] != '\0'); + return TRUE; + } + return FALSE; + +} + +void dict_pre_api_callback(struct dict *dict) +{ + if (dict->prev_ioloop != NULL) { + /* Don't let callback see that we've created our + internal ioloop in case it wants to add some ios + or timeouts. */ + io_loop_set_current(dict->prev_ioloop); + } +} + +void dict_post_api_callback(struct dict *dict) +{ + if (dict->prev_ioloop != NULL) { + io_loop_set_current(dict->ioloop); + io_loop_stop(dict->ioloop); + } +} + +static void dict_lookup_finished(struct event *event, int ret, const char *error) +{ + i_assert(ret >= 0 || error != NULL); + const char *key = event_find_field_recursive_str(event, "key"); + if (ret < 0) + event_add_str(event, "error", error); + else if (ret == 0) + event_add_str(event, "key_not_found", "yes"); + event_set_name(event, "dict_lookup_finished"); + e_debug(event, "Lookup finished for '%s': %s", key, ret > 0 ? + "found" : + "not found"); +} + +static void dict_transaction_finished(struct event *event, enum dict_commit_ret ret, + bool rollback, const char *error) +{ + i_assert(ret > DICT_COMMIT_RET_FAILED || error != NULL); + if (ret == DICT_COMMIT_RET_FAILED || ret == DICT_COMMIT_RET_WRITE_UNCERTAIN) { + if (ret == DICT_COMMIT_RET_WRITE_UNCERTAIN) + event_add_str(event, "write_uncertain", "yes"); + event_add_str(event, "error", error); + } else if (rollback) { + event_add_str(event, "rollback", "yes"); + } else if (ret == 0) { + event_add_str(event, "key_not_found", "yes"); + } + event_set_name(event, "dict_transaction_finished"); + e_debug(event, "Dict transaction finished"); +} + +static void +dict_lookup_callback(const struct dict_lookup_result *result, + void *context) +{ + struct dict_lookup_callback_ctx *ctx = context; + + dict_pre_api_callback(ctx->dict); + ctx->callback(result, ctx->context); + dict_post_api_callback(ctx->dict); + dict_lookup_finished(ctx->event, result->ret, result->error); + event_unref(&ctx->event); + + dict_unref(&ctx->dict); + i_free(ctx); +} + +static void +dict_commit_async_timeout(struct dict_commit_callback_ctx *ctx) +{ + DLLIST_REMOVE(&ctx->dict->commits, ctx); + timeout_remove(&ctx->to); + dict_pre_api_callback(ctx->dict); + if (ctx->callback != NULL) + ctx->callback(&ctx->result, ctx->context); + else if (ctx->result.ret < 0) + e_error(ctx->event, "Commit failed: %s", ctx->result.error); + dict_post_api_callback(ctx->dict); + + dict_transaction_finished(ctx->event, ctx->result.ret, FALSE, ctx->result.error); + dict_op_settings_private_free(&ctx->set); + event_unref(&ctx->event); + dict_unref(&ctx->dict); + pool_unref(&ctx->pool); +} + +static void dict_commit_callback(const struct dict_commit_result *result, + void *context) +{ + struct dict_commit_callback_ctx *ctx = context; + + i_assert(result->ret >= 0 || result->error != NULL); + ctx->result = *result; + if (ctx->delayed_callback) { + ctx->result.error = p_strdup(ctx->pool, ctx->result.error); + ctx->to = timeout_add_short(0, dict_commit_async_timeout, ctx); + } else { + dict_commit_async_timeout(ctx); + } +} + +static struct event * +dict_event_create(struct dict *dict, const struct dict_op_settings *set) +{ + struct event *event = event_create(dict->event); + if (set->username != NULL) + event_add_str(event, "user", set->username); + return event; +} + +int 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 event *event = dict_event_create(dict, set); + int ret; + i_assert(dict_key_prefix_is_valid(key, set->username)); + + e_debug(event, "Looking up '%s'", key); + event_add_str(event, "key", key); + ret = dict->v.lookup(dict, set, pool, key, value_r, error_r); + dict_lookup_finished(event, ret, *error_r); + event_unref(&event); + return ret; +} + +#undef dict_lookup_async +void dict_lookup_async(struct dict *dict, const struct dict_op_settings *set, + const char *key, dict_lookup_callback_t *callback, + void *context) +{ + i_assert(dict_key_prefix_is_valid(key, set->username)); + if (dict->v.lookup_async == NULL) { + struct dict_lookup_result result; + + i_zero(&result); + /* event is going to be sent by dict_lookup */ + result.ret = dict_lookup(dict, set, pool_datastack_create(), + key, &result.value, &result.error); + const char *const values[] = { result.value, NULL }; + result.values = values; + callback(&result, context); + return; + } + struct dict_lookup_callback_ctx *lctx = + i_new(struct dict_lookup_callback_ctx, 1); + lctx->dict = dict; + dict_ref(lctx->dict); + lctx->callback = callback; + lctx->context = context; + lctx->event = dict_event_create(dict, set); + event_add_str(lctx->event, "key", key); + e_debug(lctx->event, "Looking up (async) '%s'", key); + dict->v.lookup_async(dict, set, key, dict_lookup_callback, lctx); +} + +struct dict_iterate_context * +dict_iterate_init(struct dict *dict, const struct dict_op_settings *set, + const char *path, enum dict_iterate_flags flags) +{ + struct dict_iterate_context *ctx; + + i_assert(path != NULL); + i_assert(dict_key_prefix_is_valid(path, set->username)); + + if (dict->v.iterate_init == NULL) { + /* not supported by backend */ + ctx = &dict_iter_unsupported; + } else { + ctx = dict->v.iterate_init(dict, set, path, flags); + } + /* the dict in context can differ from the dict + passed as parameter, e.g. it can be dict-fail when + iteration is not supported. */ + ctx->event = dict_event_create(dict, set); + ctx->flags = flags; + dict_op_settings_dup(set, &ctx->set); + + event_add_str(ctx->event, "key", path); + event_set_name(ctx->event, "dict_iteration_started"); + e_debug(ctx->event, "Iterating prefix %s", path); + ctx->dict->iter_count++; + return ctx; +} + +bool dict_iterate(struct dict_iterate_context *ctx, + const char **key_r, const char **value_r) +{ + const char *const *values; + + if (!dict_iterate_values(ctx, key_r, &values)) + return FALSE; + if ((ctx->flags & DICT_ITERATE_FLAG_NO_VALUE) == 0) + *value_r = values[0]; + return TRUE; +} + +bool dict_iterate_values(struct dict_iterate_context *ctx, + const char **key_r, const char *const **values_r) +{ + + if (ctx->max_rows > 0 && ctx->row_count >= ctx->max_rows) { + e_debug(ctx->event, "Maximum row count (%"PRIu64") reached", + ctx->max_rows); + /* row count was limited */ + ctx->has_more = FALSE; + return FALSE; + } + if (!ctx->dict->v.iterate(ctx, key_r, values_r)) + return FALSE; + if ((ctx->flags & DICT_ITERATE_FLAG_NO_VALUE) != 0) { + /* always return value as NULL to be consistent across + drivers */ + *values_r = NULL; + } else { + i_assert(values_r[0] != NULL); + } + ctx->row_count++; + return TRUE; +} + +#undef dict_iterate_set_async_callback +void dict_iterate_set_async_callback(struct dict_iterate_context *ctx, + dict_iterate_callback_t *callback, + void *context) +{ + ctx->async_callback = callback; + ctx->async_context = context; +} + +void dict_iterate_set_limit(struct dict_iterate_context *ctx, + uint64_t max_rows) +{ + ctx->max_rows = max_rows; +} + +bool dict_iterate_has_more(struct dict_iterate_context *ctx) +{ + return ctx->has_more; +} + +int dict_iterate_deinit(struct dict_iterate_context **_ctx, + const char **error_r) +{ + struct dict_iterate_context *ctx = *_ctx; + + if (ctx == NULL) + return 0; + + struct event *event = ctx->event; + int ret; + uint64_t rows; + + i_assert(ctx->dict->iter_count > 0); + ctx->dict->iter_count--; + + *_ctx = NULL; + rows = ctx->row_count; + struct dict_op_settings_private set_copy = ctx->set; + ret = ctx->dict->v.iterate_deinit(ctx, error_r); + dict_op_settings_private_free(&set_copy); + + event_add_int(event, "rows", rows); + event_set_name(event, "dict_iteration_finished"); + + if (ret < 0) { + event_add_str(event, "error", *error_r); + e_debug(event, "Iteration finished: %s", *error_r); + } else { + if (rows == 0) + event_add_str(event, "key_not_found", "yes"); + e_debug(event, "Iteration finished, got %"PRIu64" rows", rows); + } + + event_unref(&event); + return ret; +} + +struct dict_transaction_context * +dict_transaction_begin(struct dict *dict, const struct dict_op_settings *set) +{ + struct dict_transaction_context *ctx; + guid_128_t guid; + if (dict->v.transaction_init == NULL) + ctx = &dict_transaction_unsupported; + else + ctx = dict->v.transaction_init(dict); + /* the dict in context can differ from the dict + passed as parameter, e.g. it can be dict-fail when + transactions are not supported. */ + ctx->dict->transaction_count++; + DLLIST_PREPEND(&ctx->dict->transactions, ctx); + ctx->event = dict_event_create(dict, set); + dict_op_settings_dup(set, &ctx->set); + guid_128_generate(guid); + event_add_str(ctx->event, "txid", guid_128_to_string(guid)); + event_set_name(ctx->event, "dict_transaction_started"); + e_debug(ctx->event, "Starting transaction"); + return ctx; +} + +void dict_transaction_no_slowness_warning(struct dict_transaction_context *ctx) +{ + ctx->no_slowness_warning = TRUE; +} + +void dict_transaction_set_hide_log_values(struct dict_transaction_context *ctx, + bool hide_log_values) +{ + /* Apply hide_log_values to the current transactions dict op settings */ + ctx->set.hide_log_values = hide_log_values; + if (ctx->dict->v.set_hide_log_values != NULL) + ctx->dict->v.set_hide_log_values(ctx, hide_log_values); +} + +void dict_transaction_set_timestamp(struct dict_transaction_context *ctx, + const struct timespec *ts) +{ + /* These asserts are mainly here to guarantee a possibility in future + to change the API to support multiple timestamps within the same + transaction, so this call would apply only to the following + changes. */ + i_assert(!ctx->changed); + i_assert(ctx->timestamp.tv_sec == 0); + i_assert(ts->tv_sec > 0); + + ctx->timestamp = *ts; + struct event_passthrough *e = event_create_passthrough(ctx->event)-> + set_name("dict_set_timestamp"); + + e_debug(e->event(), "Setting timestamp on transaction to (%"PRIdTIME_T", %ld)", + ts->tv_sec, ts->tv_nsec); + if (ctx->dict->v.set_timestamp != NULL) + ctx->dict->v.set_timestamp(ctx, ts); +} + +struct dict_commit_sync_result { + int ret; + char *error; +}; + +static void +dict_transaction_commit_sync_callback(const struct dict_commit_result *result, + void *context) +{ + struct dict_commit_sync_result *sync_result = context; + + sync_result->ret = result->ret; + sync_result->error = i_strdup(result->error); +} + +int dict_transaction_commit(struct dict_transaction_context **_ctx, + const char **error_r) +{ + pool_t pool = pool_alloconly_create("dict_commit_callback_ctx", 64); + struct dict_commit_callback_ctx *cctx = + p_new(pool, struct dict_commit_callback_ctx, 1); + struct dict_transaction_context *ctx = *_ctx; + struct dict_commit_sync_result result; + + *_ctx = NULL; + cctx->pool = pool; + i_zero(&result); + i_assert(ctx->dict->transaction_count > 0); + ctx->dict->transaction_count--; + DLLIST_REMOVE(&ctx->dict->transactions, ctx); + DLLIST_PREPEND(&ctx->dict->commits, cctx); + cctx->dict = ctx->dict; + dict_ref(cctx->dict); + cctx->callback = dict_transaction_commit_sync_callback; + cctx->context = &result; + cctx->event = ctx->event; + cctx->set = ctx->set; + + ctx->dict->v.transaction_commit(ctx, FALSE, dict_commit_callback, cctx); + *error_r = t_strdup(result.error); + i_free(result.error); + return result.ret; +} + +#undef dict_transaction_commit_async +void dict_transaction_commit_async(struct dict_transaction_context **_ctx, + dict_transaction_commit_callback_t *callback, + void *context) +{ + pool_t pool = pool_alloconly_create("dict_commit_callback_ctx", 64); + struct dict_commit_callback_ctx *cctx = + p_new(pool, struct dict_commit_callback_ctx, 1); + struct dict_transaction_context *ctx = *_ctx; + + *_ctx = NULL; + i_assert(ctx->dict->transaction_count > 0); + ctx->dict->transaction_count--; + DLLIST_REMOVE(&ctx->dict->transactions, ctx); + DLLIST_PREPEND(&ctx->dict->commits, cctx); + if (callback == NULL) + callback = dict_transaction_commit_async_noop_callback; + cctx->pool = pool; + cctx->dict = ctx->dict; + dict_ref(cctx->dict); + cctx->callback = callback; + cctx->context = context; + cctx->event = ctx->event; + cctx->set = ctx->set; + cctx->delayed_callback = TRUE; + ctx->dict->v.transaction_commit(ctx, TRUE, dict_commit_callback, cctx); + cctx->delayed_callback = FALSE; +} + +void dict_transaction_commit_async_nocallback( + struct dict_transaction_context **ctx) +{ + dict_transaction_commit_async(ctx, NULL, NULL); +} + +void dict_transaction_rollback(struct dict_transaction_context **_ctx) +{ + struct dict_transaction_context *ctx = *_ctx; + + if (ctx == NULL) + return; + + struct event *event = ctx->event; + + *_ctx = NULL; + i_assert(ctx->dict->transaction_count > 0); + ctx->dict->transaction_count--; + DLLIST_REMOVE(&ctx->dict->transactions, ctx); + struct dict_op_settings_private set_copy = ctx->set; + ctx->dict->v.transaction_rollback(ctx); + dict_transaction_finished(event, DICT_COMMIT_RET_OK, TRUE, NULL); + dict_op_settings_private_free(&set_copy); + event_unref(&event); +} + +void dict_set(struct dict_transaction_context *ctx, + const char *key, const char *value) +{ + i_assert(dict_key_prefix_is_valid(key, ctx->set.username)); + struct event_passthrough *e = event_create_passthrough(ctx->event)-> + set_name("dict_set_key")-> + add_str("key", key); + + e_debug(e->event(), "Setting '%s' to '%s'", key, value); + + T_BEGIN { + ctx->dict->v.set(ctx, key, value); + } T_END; + ctx->changed = TRUE; +} + +void dict_unset(struct dict_transaction_context *ctx, + const char *key) +{ + i_assert(dict_key_prefix_is_valid(key, ctx->set.username)); + struct event_passthrough *e = event_create_passthrough(ctx->event)-> + set_name("dict_unset_key")-> + add_str("key", key); + + e_debug(e->event(), "Unsetting '%s'", key); + + T_BEGIN { + ctx->dict->v.unset(ctx, key); + } T_END; + ctx->changed = TRUE; +} + +void dict_atomic_inc(struct dict_transaction_context *ctx, + const char *key, long long diff) +{ + i_assert(dict_key_prefix_is_valid(key, ctx->set.username)); + struct event_passthrough *e = event_create_passthrough(ctx->event)-> + set_name("dict_increment_key")-> + add_str("key", key); + + e_debug(e->event(), "Incrementing '%s' with %lld", key, diff); + + if (diff != 0) T_BEGIN { + ctx->dict->v.atomic_inc(ctx, key, diff); + ctx->changed = TRUE; + } T_END; +} + +const char *dict_escape_string(const char *str) +{ + const char *p; + string_t *ret; + + /* see if we need to escape it */ + for (p = str; *p != '\0'; p++) { + if (*p == '/' || *p == '\\') + break; + } + + if (*p == '\0') + return str; + + /* escape */ + ret = t_str_new((size_t) (p - str) + 128); + str_append_data(ret, str, (size_t) (p - str)); + + for (; *p != '\0'; p++) { + switch (*p) { + case '/': + str_append_c(ret, '\\'); + str_append_c(ret, '|'); + break; + case '\\': + str_append_c(ret, '\\'); + str_append_c(ret, '\\'); + break; + default: + str_append_c(ret, *p); + break; + } + } + return str_c(ret); +} + +const char *dict_unescape_string(const char *str) +{ + const char *p; + string_t *ret; + + /* see if we need to unescape it */ + for (p = str; *p != '\0'; p++) { + if (*p == '\\') + break; + } + + if (*p == '\0') + return str; + + /* unescape */ + ret = t_str_new((size_t) (p - str) + strlen(p) + 1); + str_append_data(ret, str, (size_t) (p - str)); + + for (; *p != '\0'; p++) { + if (*p != '\\') + str_append_c(ret, *p); + else { + if (*++p == '|') + str_append_c(ret, '/'); + else if (*p == '\0') + break; + else + str_append_c(ret, *p); + } + } + return str_c(ret); +} + +void dict_op_settings_dup(const struct dict_op_settings *source, + struct dict_op_settings_private *dest_r) +{ + i_zero(dest_r); + dest_r->username = i_strdup(source->username); + dest_r->home_dir = i_strdup(source->home_dir); + dest_r->hide_log_values = source->hide_log_values; +} + +void dict_op_settings_private_free(struct dict_op_settings_private *set) +{ + i_free(set->username); + i_free(set->home_dir); +} |