summaryrefslogtreecommitdiffstats
path: root/src/lib/var-expand.c
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 09:51:24 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 09:51:24 +0000
commitf7548d6d28c313cf80e6f3ef89aed16a19815df1 (patch)
treea3f6f2a3f247293bee59ecd28e8cd8ceb6ca064a /src/lib/var-expand.c
parentInitial commit. (diff)
downloaddovecot-upstream.tar.xz
dovecot-upstream.zip
Adding upstream version 1:2.3.19.1+dfsg1.upstream/1%2.3.19.1+dfsg1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/lib/var-expand.c')
-rw-r--r--src/lib/var-expand.c833
1 files changed, 833 insertions, 0 deletions
diff --git a/src/lib/var-expand.c b/src/lib/var-expand.c
new file mode 100644
index 0000000..7ddaec0
--- /dev/null
+++ b/src/lib/var-expand.c
@@ -0,0 +1,833 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "md5.h"
+#include "hash.h"
+#include "hex-binary.h"
+#include "base64.h"
+#include "hostpid.h"
+#include "hmac.h"
+#include "pkcs5.h"
+#include "hash-method.h"
+#include "str.h"
+#include "strescape.h"
+#include "var-expand.h"
+#include "var-expand-private.h"
+
+#include <unistd.h>
+#include <ctype.h>
+
+#define TABLE_LAST(t) \
+ ((t)->key == '\0' && (t)->long_key == NULL)
+
+struct var_expand_modifier {
+ char key;
+ const char *(*func)(const char *, struct var_expand_context *);
+};
+
+static ARRAY(struct var_expand_extension_func_table) var_expand_extensions;
+
+static const char *
+m_str_lcase(const char *str, struct var_expand_context *ctx ATTR_UNUSED)
+{
+ return t_str_lcase(str);
+}
+
+static const char *
+m_str_ucase(const char *str, struct var_expand_context *ctx ATTR_UNUSED)
+{
+ return t_str_ucase(str);
+}
+
+static const char *
+m_str_escape(const char *str, struct var_expand_context *ctx ATTR_UNUSED)
+{
+ return str_escape(str);
+}
+
+static const char *
+m_str_hex(const char *str, struct var_expand_context *ctx ATTR_UNUSED)
+{
+ unsigned long long l;
+
+ if (str_to_ullong(str, &l) < 0)
+ l = 0;
+ return t_strdup_printf("%llx", l);
+}
+
+static const char *
+m_str_reverse(const char *str, struct var_expand_context *ctx ATTR_UNUSED)
+{
+ size_t len = strlen(str);
+ char *p, *rev;
+
+ rev = t_malloc_no0(len + 1);
+ rev[len] = '\0';
+
+ for (p = rev + len - 1; *str != '\0'; str++)
+ *p-- = *str;
+ return rev;
+}
+
+static const char *m_str_hash(const char *str, struct var_expand_context *ctx)
+{
+ unsigned int value = str_hash(str);
+ string_t *hash = t_str_new(20);
+
+ if (ctx->width != 0) {
+ value %= ctx->width;
+ ctx->width = 0;
+ }
+
+ str_printfa(hash, "%x", value);
+ while ((int)str_len(hash) < ctx->offset)
+ str_insert(hash, 0, "0");
+ ctx->offset = 0;
+
+ return str_c(hash);
+}
+
+static const char *
+m_str_newhash(const char *str, struct var_expand_context *ctx)
+{
+ string_t *hash = t_str_new(20);
+ unsigned char result[MD5_RESULTLEN];
+ unsigned int i;
+ uint64_t value = 0;
+
+ md5_get_digest(str, strlen(str), result);
+ for (i = 0; i < sizeof(value); i++) {
+ value <<= 8;
+ value |= result[i];
+ }
+
+ if (ctx->width != 0) {
+ value %= ctx->width;
+ ctx->width = 0;
+ }
+
+ str_printfa(hash, "%x", (unsigned int)value);
+ while ((int)str_len(hash) < ctx->offset)
+ str_insert(hash, 0, "0");
+ ctx->offset = 0;
+
+ return str_c(hash);
+}
+
+static const char *
+m_str_md5(const char *str, struct var_expand_context *ctx ATTR_UNUSED)
+{
+ unsigned char digest[16];
+
+ md5_get_digest(str, strlen(str), digest);
+
+ return binary_to_hex(digest, sizeof(digest));
+}
+
+static const char *
+m_str_ldap_dn(const char *str, struct var_expand_context *ctx ATTR_UNUSED)
+{
+ string_t *ret = t_str_new(256);
+
+ while (*str != '\0') {
+ if (*str == '.')
+ str_append(ret, ",dc=");
+ else
+ str_append_c(ret, *str);
+ str++;
+ }
+
+ return str_free_without_data(&ret);
+}
+
+static const char *
+m_str_trim(const char *str, struct var_expand_context *ctx ATTR_UNUSED)
+{
+ size_t len;
+
+ len = strlen(str);
+ while (len > 0 && i_isspace(str[len-1]))
+ len--;
+ return t_strndup(str, len);
+}
+
+#define MAX_MODIFIER_COUNT 10
+static const struct var_expand_modifier modifiers[] = {
+ { 'L', m_str_lcase },
+ { 'U', m_str_ucase },
+ { 'E', m_str_escape },
+ { 'X', m_str_hex },
+ { 'R', m_str_reverse },
+ { 'H', m_str_hash },
+ { 'N', m_str_newhash },
+ { 'M', m_str_md5 },
+ { 'D', m_str_ldap_dn },
+ { 'T', m_str_trim },
+ { '\0', NULL }
+};
+
+static int
+var_expand_short(const struct var_expand_table *table, char key,
+ const char **var_r, const char **error_r)
+{
+ const struct var_expand_table *t;
+
+ if (table != NULL) {
+ for (t = table; !TABLE_LAST(t); t++) {
+ if (t->key == key) {
+ *var_r = t->value != NULL ? t->value : "";
+ return 1;
+ }
+ }
+ }
+
+ /* not found */
+ if (key == '%') {
+ *var_r = "%";
+ return 1;
+ }
+ if (*error_r == NULL)
+ *error_r = t_strdup_printf("Unknown variable '%%%c'", key);
+ *var_r = t_strdup_printf("UNSUPPORTED_VARIABLE_%c", key);
+ return 0;
+}
+
+static int
+var_expand_hash(struct var_expand_context *ctx,
+ const char *key, const char *field,
+ const char **result_r, const char **error_r)
+{
+ enum {
+ FORMAT_HEX,
+ FORMAT_HEX_UC,
+ FORMAT_BASE64
+ } format = FORMAT_HEX;
+
+ const char *p = strchr(key, ';');
+ const char *const *args = NULL;
+ const char *algo = key;
+ const char *value;
+ int ret;
+
+ if (p != NULL) {
+ algo = t_strcut(key, ';');
+ args = t_strsplit(p+1, ",");
+ }
+
+ const struct hash_method *method;
+ if (strcmp(algo, "pkcs5") == 0) {
+ method = hash_method_lookup("sha256");
+ } else if ((method = hash_method_lookup(algo)) == NULL) {
+ return 0;
+ }
+
+ string_t *field_value = t_str_new(64);
+ string_t *salt = t_str_new(64);
+ string_t *tmp = t_str_new(method->digest_size);
+
+ if ((ret = var_expand_long(ctx, field, strlen(field),
+ &value, error_r)) < 1) {
+ return ret;
+ }
+
+ str_append(field_value, value);
+
+ /* default values */
+ unsigned int rounds = 1;
+ unsigned int truncbits = 0;
+
+ if (strcmp(algo, "pkcs5") == 0) {
+ rounds = 2048;
+ str_append(salt, field);
+ }
+
+ while(args != NULL && *args != NULL) {
+ const char *k = t_strcut(*args, '=');
+ const char *value = strchr(*args, '=');
+ if (value == NULL) {
+ args++;
+ continue;
+ } else {
+ value++;
+ }
+ if (strcmp(k, "rounds") == 0) {
+ if (str_to_uint(value, &rounds)<0) {
+ *error_r = t_strdup_printf(
+ "Cannot parse hash arguments:"
+ "'%s' is not number for rounds",
+ value);
+ return -1;
+ }
+ if (rounds < 1) {
+ *error_r = t_strdup_printf(
+ "Cannot parse hash arguments:"
+ "rounds must be at least 1");
+ return -1;
+ }
+ } else if (strcmp(k, "truncate") == 0) {
+ if (str_to_uint(value, &truncbits)<0) {
+ *error_r = t_strdup_printf(
+ "Cannot parse hash arguments:"
+ "'%s' is not number for truncbits",
+ value);
+ return -1;
+ }
+ truncbits = I_MIN(truncbits, method->digest_size*8);
+ } else if (strcmp(k, "salt") == 0) {
+ str_truncate(salt, 0);
+ if (var_expand_with_funcs(salt, value, ctx->table,
+ ctx->func_table, ctx->context,
+ error_r) < 0) {
+ return -1;
+ }
+ break;
+ } else if (strcmp(k, "format") == 0) {
+ if (strcmp(value, "hex") == 0) {
+ format = FORMAT_HEX;
+ } else if (strcmp(value, "hexuc") == 0){
+ format = FORMAT_HEX_UC;
+ } else if (strcmp(value, "base64") == 0) {
+ format = FORMAT_BASE64;
+ } else {
+ *error_r = t_strdup_printf(
+ "Cannot parse hash arguments:"
+ "'%s' is not supported format",
+ value);
+ return -1;
+ }
+ }
+ args++;
+ }
+
+ str_truncate(tmp, 0);
+
+ if (strcmp(algo, "pkcs5") == 0) {
+ if (pkcs5_pbkdf(PKCS5_PBKDF2, method,
+ field_value->data, field_value->used,
+ salt->data, salt->used,
+ rounds, HMAC_MAX_CONTEXT_SIZE, tmp) != 0) {
+ *error_r = "Cannot hash: PKCS5_PBKDF2 failed";
+ return -1;
+ }
+ } else {
+ void *context = t_malloc_no0(method->context_size);
+
+ str_append_str(tmp, field_value);
+
+ for(;rounds>0;rounds--) {
+ method->init(context);
+ if (salt->used > 0)
+ method->loop(context, salt->data, salt->used);
+ method->loop(context, tmp->data, tmp->used);
+ unsigned char *digest =
+ buffer_get_modifiable_data(tmp, NULL);
+ method->result(context, digest);
+ if (tmp->used != method->digest_size)
+ buffer_set_used_size(tmp, method->digest_size);
+ }
+ }
+
+ if (truncbits > 0)
+ buffer_truncate_rshift_bits(tmp, truncbits);
+
+ switch(format) {
+ case FORMAT_HEX:
+ *result_r = binary_to_hex(tmp->data, tmp->used);
+ return 1;
+ case FORMAT_HEX_UC:
+ *result_r = binary_to_hex(tmp->data, tmp->used);
+ return 1;
+ case FORMAT_BASE64: {
+ string_t *dest = t_str_new(64);
+ base64_encode(tmp->data, tmp->used, dest);
+ *result_r = str_c(dest);
+ return 1;
+ }
+ }
+
+ i_unreached();
+}
+
+static int
+var_expand_func(const struct var_expand_func_table *func_table,
+ const char *key, const char *data, void *context,
+ const char **var_r, const char **error_r)
+{
+ const char *value = NULL;
+ int ret;
+
+ if (strcmp(key, "env") == 0) {
+ value = getenv(data);
+ *var_r = value != NULL ? value : "";
+ return 1;
+ }
+ if (func_table != NULL) {
+ for (; func_table->key != NULL; func_table++) {
+ if (strcmp(func_table->key, key) == 0) {
+ ret = func_table->func(data, context, &value, error_r);
+ *var_r = value != NULL ? value : "";
+ return ret;
+ }
+ }
+ }
+ if (*error_r == NULL)
+ *error_r = t_strdup_printf("Unknown variable '%%%s'", key);
+ *var_r = t_strdup_printf("UNSUPPORTED_VARIABLE_%s", key);
+ return 0;
+}
+
+static int
+var_expand_try_extension(struct var_expand_context *ctx,
+ const char *key, const char *data,
+ const char **var_r, const char **error_r)
+{
+ int ret;
+ const char *sep = strchr(key, ';');
+
+ if (sep == NULL) sep = key + strlen(key);
+
+ /* try with extensions */
+ const struct var_expand_extension_func_table *f;
+ array_foreach(&var_expand_extensions, f) {
+ /* ensure we won't match abbreviations */
+ size_t len = sep-key;
+ if (strncasecmp(key, f->key, len) == 0 && f->key[len] == '\0')
+ return f->func(ctx, key, data, var_r, error_r);
+ }
+ if ((ret = var_expand_func(ctx->func_table, key, data,
+ ctx->context, var_r, error_r)) == 0) {
+ *error_r = t_strdup_printf("Unknown variable '%%%s'", key);
+ }
+ return ret;
+}
+
+
+int
+var_expand_long(struct var_expand_context *ctx,
+ const void *key_start, size_t key_len,
+ const char **var_r, const char **error_r)
+{
+ const struct var_expand_table *t;
+ const char *key, *value = NULL;
+ int ret = 1;
+
+ if (ctx->table != NULL) {
+ for (t = ctx->table; !TABLE_LAST(t); t++) {
+ if (t->long_key != NULL &&
+ strncmp(t->long_key, key_start, key_len) == 0 &&
+ t->long_key[key_len] == '\0') {
+ *var_r = t->value != NULL ? t->value : "";
+ return 1;
+ }
+ }
+ }
+ key = t_strndup(key_start, key_len);
+
+ /* built-in variables: */
+ switch (key_len) {
+ case 3:
+ if (strcmp(key, "pid") == 0)
+ value = my_pid;
+ else if (strcmp(key, "uid") == 0)
+ value = dec2str(geteuid());
+ else if (strcmp(key, "gid") == 0)
+ value = dec2str(getegid());
+ break;
+ case 8:
+ if (strcmp(key, "hostname") == 0)
+ value = my_hostname;
+ break;
+ }
+
+ if (value == NULL) {
+ const char *data = strchr(key, ':');
+
+ if (data != NULL)
+ key = t_strdup_until(key, data++);
+ else
+ data = "";
+
+ ret = var_expand_try_extension(ctx, key, data, &value, error_r);
+
+ if (ret <= 0 && value == NULL) {
+ value = "";
+ }
+ }
+ *var_r = value;
+ return ret;
+}
+
+int var_expand_with_funcs(string_t *dest, const char *str,
+ const struct var_expand_table *table,
+ const struct var_expand_func_table *func_table,
+ void *context, const char **error_r)
+{
+ const struct var_expand_modifier *m;
+ const char *var;
+ struct var_expand_context ctx;
+ const char *(*modifier[MAX_MODIFIER_COUNT])
+ (const char *, struct var_expand_context *);
+ const char *end;
+ unsigned int i, modifier_count;
+ size_t len;
+ int ret, final_ret = 1;
+
+ *error_r = NULL;
+
+ i_zero(&ctx);
+ ctx.table = table;
+ ctx.func_table = func_table;
+ ctx.context = context;
+
+ for (; *str != '\0'; str++) {
+ if (*str != '%')
+ str_append_c(dest, *str);
+ else {
+ int sign = 1;
+
+ str++;
+
+ /* reset per-field modifiers */
+ ctx.offset = 0;
+ ctx.width = 0;
+ ctx.zero_padding = FALSE;
+
+ /* [<offset>.]<width>[<modifiers>]<variable> */
+ if (*str == '-') {
+ sign = -1;
+ str++;
+ }
+ if (*str == '0') {
+ ctx.zero_padding = TRUE;
+ str++;
+ }
+ while (*str >= '0' && *str <= '9') {
+ ctx.width = ctx.width*10 + (*str - '0');
+ str++;
+ }
+
+ if (*str == '.') {
+ ctx.offset = sign * ctx.width;
+ sign = 1;
+ ctx.width = 0;
+ str++;
+
+ /* if offset was prefixed with zero (or it was
+ plain zero), just ignore that. zero padding
+ is done with the width. */
+ ctx.zero_padding = FALSE;
+ if (*str == '0') {
+ ctx.zero_padding = TRUE;
+ str++;
+ }
+ if (*str == '-') {
+ sign = -1;
+ str++;
+ }
+
+ while (*str >= '0' && *str <= '9') {
+ ctx.width = ctx.width*10 + (*str - '0');
+ str++;
+ }
+ ctx.width = sign * ctx.width;
+ }
+
+ modifier_count = 0;
+ while (modifier_count < MAX_MODIFIER_COUNT) {
+ modifier[modifier_count] = NULL;
+ for (m = modifiers; m->key != '\0'; m++) {
+ if (m->key == *str) {
+ /* @UNSAFE */
+ modifier[modifier_count] =
+ m->func;
+ str++;
+ break;
+ }
+ }
+ if (modifier[modifier_count] == NULL)
+ break;
+ modifier_count++;
+ }
+
+ if (*str == '\0')
+ break;
+
+ var = NULL;
+ if (*str == '{' && strchr(str, '}') != NULL) {
+ /* %{long_key} */
+ unsigned int ctr = 1;
+ bool escape = FALSE;
+ end = str;
+ while(*++end != '\0' && ctr > 0) {
+ if (!escape && *end == '\\') {
+ escape = TRUE;
+ continue;
+ }
+ if (escape) {
+ escape = FALSE;
+ continue;
+ }
+ if (*end == '{') ctr++;
+ if (*end == '}') ctr--;
+ }
+ if (ctr == 0)
+ /* it needs to come back a bit */
+ end--;
+ /* if there is no } it will consume rest of the
+ string */
+ len = end - (str + 1);
+ ret = var_expand_long(&ctx, str+1, len,
+ &var, error_r);
+ str = end;
+ } else {
+ ret = var_expand_short(ctx.table, *str,
+ &var, error_r);
+ }
+ i_assert(var != NULL);
+
+ if (final_ret > ret)
+ final_ret = ret;
+
+ if (ret <= 0)
+ str_append(dest, var);
+ else {
+ for (i = 0; i < modifier_count; i++)
+ var = modifier[i](var, &ctx);
+
+ if (ctx.offset < 0) {
+ /* if offset is < 0 then we want to
+ start at the end */
+ size_t len = strlen(var);
+ size_t offset_from_end = -ctx.offset;
+
+ if (len > offset_from_end)
+ var += len - offset_from_end;
+ } else {
+ while (*var != '\0' && ctx.offset > 0) {
+ ctx.offset--;
+ var++;
+ }
+ }
+ if (ctx.width == 0)
+ str_append(dest, var);
+ else if (!ctx.zero_padding) {
+ if (ctx.width < 0)
+ ctx.width = strlen(var) - (-ctx.width);
+ str_append_max(dest, var, ctx.width);
+ } else {
+ /* %05d -like padding. no truncation. */
+ ssize_t len = strlen(var);
+ while (len < ctx.width) {
+ str_append_c(dest, '0');
+ ctx.width--;
+ }
+ str_append(dest, var);
+ }
+ }
+ }
+ }
+ return final_ret;
+}
+
+int var_expand(string_t *dest, const char *str,
+ const struct var_expand_table *table, const char **error_r)
+{
+ return var_expand_with_funcs(dest, str, table, NULL, NULL, error_r);
+}
+
+static bool
+var_get_key_range_full(const char *str, unsigned int *idx_r,
+ unsigned int *size_r)
+{
+ const struct var_expand_modifier *m;
+ unsigned int i = 0;
+
+ /* [<offset>.]<width>[<modifiers>]<variable> */
+ while ((str[i] >= '0' && str[i] <= '9') || str[i] == '-')
+ i++;
+
+ if (str[i] == '.') {
+ i++;
+ while ((str[i] >= '0' && str[i] <= '9') || str[i] == '-')
+ i++;
+ }
+
+ do {
+ for (m = modifiers; m->key != '\0'; m++) {
+ if (m->key == str[i]) {
+ i++;
+ break;
+ }
+ }
+ } while (m->key != '\0');
+
+ if (str[i] != '{') {
+ /* short key */
+ *idx_r = i;
+ *size_r = str[i] == '\0' ? 0 : 1;
+ return FALSE;
+ } else {
+ unsigned int depth = 1;
+ bool escape = FALSE;
+ /* long key */
+ *idx_r = ++i;
+ for (; str[i] != '\0'; i++) {
+ if (!escape && str[i] == '\\') {
+ escape = TRUE;
+ continue;
+ }
+ if (escape) {
+ escape = FALSE;
+ continue;
+ }
+ if (str[i] == '{')
+ depth++;
+ if (str[i] == '}') {
+ if (--depth==0)
+ break;
+ }
+ }
+ *size_r = i - *idx_r;
+ return TRUE;
+ }
+}
+
+char var_get_key(const char *str)
+{
+ unsigned int idx, size;
+
+ if (var_get_key_range_full(str, &idx, &size))
+ return '{';
+ return str[idx];
+}
+
+void var_get_key_range(const char *str, unsigned int *idx_r,
+ unsigned int *size_r)
+{
+ (void)var_get_key_range_full(str, idx_r, size_r);
+}
+
+static bool var_has_long_key(const char **str, const char *long_key)
+{
+ const char *start, *end;
+
+ start = strchr(*str, '{');
+ i_assert(start != NULL);
+
+ end = strchr(++start, '}');
+ if (end == NULL)
+ return FALSE;
+
+ if (strncmp(start, long_key, end-start) == 0 &&
+ long_key[end-start] == '\0')
+ return TRUE;
+
+ *str = end;
+ return FALSE;
+}
+
+bool var_has_key(const char *str, char key, const char *long_key)
+{
+ char c;
+
+ for (; *str != '\0'; str++) {
+ if (*str == '%' && str[1] != '\0') {
+ str++;
+ c = var_get_key(str);
+ if (c == key && key != '\0')
+ return TRUE;
+
+ if (c == '{' && long_key != NULL) {
+ if (var_has_long_key(&str, long_key))
+ return TRUE;
+ }
+ }
+ }
+ return FALSE;
+}
+
+void var_expand_extensions_deinit(void)
+{
+ array_free(&var_expand_extensions);
+}
+
+void var_expand_extensions_init(void)
+{
+ i_array_init(&var_expand_extensions, 32);
+
+ /* put all hash methods there */
+ for(const struct hash_method **meth = hash_methods;
+ *meth != NULL;
+ meth++) {
+ struct var_expand_extension_func_table *func =
+ array_append_space(&var_expand_extensions);
+ func->key = (*meth)->name;
+ func->func = var_expand_hash;
+ }
+
+ /* pkcs5 */
+ struct var_expand_extension_func_table *func =
+ array_append_space(&var_expand_extensions);
+ func->key = "pkcs5";
+ func->func = var_expand_hash;
+
+ /* if */
+ func = array_append_space(&var_expand_extensions);
+ func->key = "if";
+ func->func = var_expand_if;
+}
+
+void
+var_expand_register_func_array(const struct var_expand_extension_func_table *funcs)
+{
+ for(const struct var_expand_extension_func_table *ptr = funcs;
+ ptr->key != NULL;
+ ptr++) {
+ i_assert(*ptr->key != '\0');
+ array_push_front(&var_expand_extensions, ptr);
+ }
+}
+
+void
+var_expand_unregister_func_array(const struct var_expand_extension_func_table *funcs)
+{
+ for(const struct var_expand_extension_func_table *ptr = funcs;
+ ptr->key != NULL;
+ ptr++) {
+ i_assert(ptr->func != NULL);
+ for(unsigned int i = 0; i < array_count(&var_expand_extensions); i++) {
+ const struct var_expand_extension_func_table *func =
+ array_idx(&var_expand_extensions, i);
+ if (strcasecmp(func->key, ptr->key) == 0) {
+ array_delete(&var_expand_extensions, i, 1);
+ }
+ }
+ }
+}
+
+struct var_expand_table *
+var_expand_merge_tables(pool_t pool, const struct var_expand_table *a,
+ const struct var_expand_table *b)
+{
+ ARRAY(struct var_expand_table) table;
+ size_t a_size = var_expand_table_size(a);
+ size_t b_size = var_expand_table_size(b);
+ p_array_init(&table, pool, a_size + b_size + 1);
+ for(size_t i=0; i<a_size; i++) {
+ struct var_expand_table *entry =
+ array_append_space(&table);
+ entry->key = a[i].key;
+ entry->value = p_strdup(pool, a[i].value);
+ entry->long_key = p_strdup(pool, a[i].long_key);
+ }
+ for(size_t i=0; i<b_size; i++) {
+ struct var_expand_table *entry =
+ array_append_space(&table);
+ entry->key = b[i].key;
+ entry->value = p_strdup(pool, b[i].value);
+ entry->long_key = p_strdup(pool, b[i].long_key);
+ }
+ array_append_zero(&table);
+ return array_front_modifiable(&table);
+}