hash table FEATURE make hash table public
diff --git a/src/dict.c b/src/dict.c
new file mode 100644
index 0000000..07631f6
--- /dev/null
+++ b/src/dict.c
@@ -0,0 +1,265 @@
+/**
+ * @file dict.c
+ * @author Radek Krejci <rkrejci@cesnet.cz>
+ * @author Michal Vasko <mvasko@cesnet.cz>
+ * @brief libyang dictionary for storing strings
+ *
+ * Copyright (c) 2015 - 2023 CESNET, z.s.p.o.
+ *
+ * This source code is licensed under BSD 3-Clause License (the "License").
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://opensource.org/licenses/BSD-3-Clause
+ */
+
+#include "dict.h"
+
+#include <assert.h>
+#include <pthread.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "common.h"
+#include "compat.h"
+#include "log.h"
+
+/* starting size of the dictionary */
+#define LYDICT_MIN_SIZE 1024
+
+/**
+ * @brief Comparison callback for dictionary's hash table
+ *
+ * Implementation of ::lyht_value_equal_cb.
+ */
+static ly_bool
+lydict_val_eq(void *val1_p, void *val2_p, ly_bool UNUSED(mod), void *cb_data)
+{
+    LY_CHECK_ARG_RET(NULL, val1_p, val2_p, cb_data, 0);
+
+    const char *str1 = ((struct ly_dict_rec *)val1_p)->value;
+    const char *str2 = ((struct ly_dict_rec *)val2_p)->value;
+
+    LY_CHECK_ERR_RET(!str1, LOGARG(NULL, val1_p), 0);
+    LY_CHECK_ERR_RET(!str2, LOGARG(NULL, val2_p), 0);
+
+    if (strncmp(str1, str2, *(size_t *)cb_data) == 0) {
+        return 1;
+    }
+
+    return 0;
+}
+
+void
+lydict_init(struct ly_dict *dict)
+{
+    LY_CHECK_ARG_RET(NULL, dict, );
+
+    dict->hash_tab = lyht_new(LYDICT_MIN_SIZE, sizeof(struct ly_dict_rec), lydict_val_eq, NULL, 1);
+    LY_CHECK_ERR_RET(!dict->hash_tab, LOGINT(NULL), );
+    pthread_mutex_init(&dict->lock, NULL);
+}
+
+void
+lydict_clean(struct ly_dict *dict)
+{
+    struct ly_dict_rec *dict_rec = NULL;
+    struct ly_ht_rec *rec = NULL;
+
+    LY_CHECK_ARG_RET(NULL, dict, );
+
+    for (uint32_t i = 0; i < dict->hash_tab->size; i++) {
+        /* get ith record */
+        rec = (struct ly_ht_rec *)&dict->hash_tab->recs[i * dict->hash_tab->rec_size];
+        if (rec->hits == 1) {
+            /*
+             * this should not happen, all records inserted into
+             * dictionary are supposed to be removed using lydict_remove()
+             * before calling lydict_clean()
+             */
+            dict_rec = (struct ly_dict_rec *)rec->val;
+            LOGWRN(NULL, "String \"%s\" not freed from the dictionary, refcount %d", dict_rec->value, dict_rec->refcount);
+            /* if record wasn't removed before free string allocated for that record */
+#ifdef NDEBUG
+            free(dict_rec->value);
+#endif
+        }
+    }
+
+    /* free table and destroy mutex */
+    lyht_free(dict->hash_tab, NULL);
+    pthread_mutex_destroy(&dict->lock);
+}
+
+static ly_bool
+lydict_resize_val_eq(void *val1_p, void *val2_p, ly_bool mod, void *cb_data)
+{
+    LY_CHECK_ARG_RET(NULL, val1_p, val2_p, 0);
+
+    const char *str1 = ((struct ly_dict_rec *)val1_p)->value;
+    const char *str2 = ((struct ly_dict_rec *)val2_p)->value;
+
+    LY_CHECK_ERR_RET(!str1, LOGARG(NULL, val1_p), 0);
+    LY_CHECK_ERR_RET(!str2, LOGARG(NULL, val2_p), 0);
+
+    if (mod) {
+        /* used when inserting new values */
+        if (strcmp(str1, str2) == 0) {
+            return 1;
+        }
+    } else {
+        /* used when finding the original value again in the resized table */
+        return lydict_val_eq(val1_p, val2_p, mod, cb_data);
+    }
+
+    return 0;
+}
+
+LIBYANG_API_DEF LY_ERR
+lydict_remove(const struct ly_ctx *ctx, const char *value)
+{
+    LY_ERR ret = LY_SUCCESS;
+    size_t len;
+    uint32_t hash;
+    struct ly_dict_rec rec, *match = NULL;
+    char *val_p;
+
+    if (!ctx || !value) {
+        return LY_SUCCESS;
+    }
+
+    LOGDBG(LY_LDGDICT, "removing \"%s\"", value);
+
+    len = strlen(value);
+    hash = lyht_hash(value, len);
+
+    /* create record for lyht_find call */
+    rec.value = (char *)value;
+    rec.refcount = 0;
+
+    pthread_mutex_lock((pthread_mutex_t *)&ctx->dict.lock);
+    /* set len as data for compare callback */
+    lyht_set_cb_data(ctx->dict.hash_tab, (void *)&len);
+    /* check if value is already inserted */
+    ret = lyht_find(ctx->dict.hash_tab, &rec, hash, (void **)&match);
+
+    if (ret == LY_SUCCESS) {
+        LY_CHECK_ERR_GOTO(!match, LOGINT(ctx), finish);
+
+        /* if value is already in dictionary, decrement reference counter */
+        match->refcount--;
+        if (match->refcount == 0) {
+            /*
+             * remove record
+             * save pointer to stored string before lyht_remove to
+             * free it after it is removed from hash table
+             */
+            val_p = match->value;
+            ret = lyht_remove_with_resize_cb(ctx->dict.hash_tab, &rec, hash, lydict_resize_val_eq);
+            free(val_p);
+            LY_CHECK_ERR_GOTO(ret, LOGINT(ctx), finish);
+        }
+    } else if (ret == LY_ENOTFOUND) {
+        LOGERR(ctx, LY_ENOTFOUND, "Value \"%s\" was not found in the dictionary.", value);
+    } else {
+        LOGINT(ctx);
+    }
+
+finish:
+    pthread_mutex_unlock((pthread_mutex_t *)&ctx->dict.lock);
+    return ret;
+}
+
+LY_ERR
+dict_insert(const struct ly_ctx *ctx, char *value, size_t len, ly_bool zerocopy, const char **str_p)
+{
+    LY_ERR ret = LY_SUCCESS;
+    struct ly_dict_rec *match = NULL, rec;
+    uint32_t hash;
+
+    LOGDBG(LY_LDGDICT, "inserting \"%.*s\"", (int)len, value);
+
+    hash = lyht_hash(value, len);
+    /* set len as data for compare callback */
+    lyht_set_cb_data(ctx->dict.hash_tab, (void *)&len);
+    /* create record for lyht_insert */
+    rec.value = value;
+    rec.refcount = 1;
+
+    ret = lyht_insert_with_resize_cb(ctx->dict.hash_tab, (void *)&rec, hash, lydict_resize_val_eq, (void **)&match);
+    if (ret == LY_EEXIST) {
+        match->refcount++;
+        if (zerocopy) {
+            free(value);
+        }
+        ret = LY_SUCCESS;
+    } else if (ret == LY_SUCCESS) {
+        if (!zerocopy) {
+            /*
+             * allocate string for new record
+             * record is already inserted in hash table
+             */
+            match->value = malloc(sizeof *match->value * (len + 1));
+            LY_CHECK_ERR_RET(!match->value, LOGMEM(ctx), LY_EMEM);
+            if (len) {
+                memcpy(match->value, value, len);
+            }
+            match->value[len] = '\0';
+        }
+    } else {
+        /* lyht_insert returned error */
+        if (zerocopy) {
+            free(value);
+        }
+        return ret;
+    }
+
+    if (str_p) {
+        *str_p = match->value;
+    }
+
+    return ret;
+}
+
+LIBYANG_API_DEF LY_ERR
+lydict_insert(const struct ly_ctx *ctx, const char *value, size_t len, const char **str_p)
+{
+    LY_ERR result;
+
+    LY_CHECK_ARG_RET(ctx, ctx, str_p, LY_EINVAL);
+
+    if (!value) {
+        *str_p = NULL;
+        return LY_SUCCESS;
+    }
+
+    if (!len) {
+        len = strlen(value);
+    }
+
+    pthread_mutex_lock((pthread_mutex_t *)&ctx->dict.lock);
+    result = dict_insert(ctx, (char *)value, len, 0, str_p);
+    pthread_mutex_unlock((pthread_mutex_t *)&ctx->dict.lock);
+
+    return result;
+}
+
+LIBYANG_API_DEF LY_ERR
+lydict_insert_zc(const struct ly_ctx *ctx, char *value, const char **str_p)
+{
+    LY_ERR result;
+
+    LY_CHECK_ARG_RET(ctx, ctx, str_p, LY_EINVAL);
+
+    if (!value) {
+        *str_p = NULL;
+        return LY_SUCCESS;
+    }
+
+    pthread_mutex_lock((pthread_mutex_t *)&ctx->dict.lock);
+    result = dict_insert(ctx, value, strlen(value), 1, str_p);
+    pthread_mutex_unlock((pthread_mutex_t *)&ctx->dict.lock);
+
+    return result;
+}