]> git.ipfire.org Git - thirdparty/dovecot/core.git/commitdiff
Add lib-var-expand
authorAki Tuomi <aki.tuomi@open-xchange.com>
Fri, 16 Aug 2024 09:20:40 +0000 (12:20 +0300)
committerAki Tuomi <aki.tuomi@open-xchange.com>
Fri, 17 Jan 2025 08:40:00 +0000 (10:40 +0200)
18 files changed:
.gitignore
configure.ac
src/Makefile.am
src/lib-imap-client/imapc-settings.c
src/lib-var-expand/Makefile.am [new file with mode: 0644]
src/lib-var-expand/expansion-filter-if.c [new file with mode: 0644]
src/lib-var-expand/expansion-filter.c [new file with mode: 0644]
src/lib-var-expand/expansion-parameter.c [new file with mode: 0644]
src/lib-var-expand/expansion-program.c [new file with mode: 0644]
src/lib-var-expand/expansion-statement.c [new file with mode: 0644]
src/lib-var-expand/expansion.h [new file with mode: 0644]
src/lib-var-expand/test-var-expand.c [new file with mode: 0644]
src/lib-var-expand/var-expand-lexer.l [new file with mode: 0644]
src/lib-var-expand/var-expand-new.h [new file with mode: 0644]
src/lib-var-expand/var-expand-parser-private.h [new file with mode: 0644]
src/lib-var-expand/var-expand-parser.y [new file with mode: 0644]
src/lib-var-expand/var-expand-private.h [new file with mode: 0644]
src/lib-var-expand/var-expand.c [new file with mode: 0644]

index 3d37e31817493d96a2228a1d3923fa3938dc263d..e7115b32cc23ab55eb735ebd721b2aed1b1344e1 100644 (file)
@@ -131,3 +131,6 @@ src/lib-imap/fuzz-imap-utf7
 src/lib-imap/fuzz-imap-bodystructure
 src/lib-mail/fuzz-message-parser
 src/lib-smtp/fuzz-smtp-server
+src/lib-var-expand/var-expand-parser.c
+src/lib-var-expand/var-expand-parser.h
+src/lib-var-expand/var-expand-lexer.c
index ced65542bc846716e42bbc7e43f0f327be48fd71..4ce0419f973108a2ddc3538d123f973554dafc34 100644 (file)
@@ -554,6 +554,7 @@ LIBDOVECOT_LA_LIBS='\
        $(top_builddir)/src/lib-oauth2/liboauth2.la \
        $(top_builddir)/src/lib-smtp/libsmtp.la \
        $(top_builddir)/src/lib-program-client/libprogram_client.la \
+       $(top_builddir)/src/lib-var-expand/libvar_expand.la \
        $(top_builddir)/src/lib-master/libmaster.la \
        $(top_builddir)/src/lib-login/liblogin.la \
        $(top_builddir)/src/lib-settings/libsettings.la \
@@ -841,6 +842,7 @@ src/lib-storage/index/dbox-multi/Makefile
 src/lib-storage/index/dbox-single/Makefile
 src/lib-storage/index/raw/Makefile
 src/lib-storage/index/shared/Makefile
+src/lib-var-expand/Makefile
 src/anvil/Makefile
 src/auth/Makefile
 src/config/Makefile
index f4999c50a4ea0b6e8b495f3bfa9d0d92a39c1fab..79897f51d0139e370f2e9c0f480321d31f66cb46 100644 (file)
@@ -11,6 +11,7 @@ endif
 LIBDOVECOT_SUBDIRS = \
        lib-test \
        lib \
+       lib-var-expand \
        lib-settings \
        lib-otp \
        lib-auth \
index a4a9b41591c5575f0ba9a0134207957d03d9a91f..544b3e6ee67b72e70e3f7119aaa8444a84942286 100644 (file)
@@ -79,7 +79,7 @@ static const struct setting_keyvalue imapc_default_settings_keyvalue[] = {
        /* We want to have all imapc mailboxes accessible, so escape them if
           necessary. */
        { "layout_imapc/mailbox_list_visible_escape_char", "~" },
-       { "layout_imapc/mailbox_list_storage_escape_char", "%%" },
+       { "layout_imapc/mailbox_list_storage_escape_char", "%" },
        { NULL, NULL }
 };
 
diff --git a/src/lib-var-expand/Makefile.am b/src/lib-var-expand/Makefile.am
new file mode 100644 (file)
index 0000000..3dbfaeb
--- /dev/null
@@ -0,0 +1,71 @@
+noinst_LTLIBRARIES = libvar_expand.la
+
+# Squelch autoconf error about using .[ly] sources but not defining $(LEX)
+# and $(YACC).  Using false here avoids accidental use.
+LEX=/bin/false
+YACC=/bin/false
+
+# We use custom rules here because we want to use flex and bison instead
+# of lex and yacc (or bison in yacc-compatibility mode).  Both flex and
+# bison can handle properly naming the generated files, and it is simpler
+# and cleaner to make this rule ourselves instead of working around ylwrap
+# and yywrap's antiquated notion of what is hapenning.
+.l.c:
+       $(AM_V_GEN)$(FLEX) -o $@ $<
+
+.y.c:
+       $(AM_V_GEN)$(BISON) -Wcounterexamples -o $@ $<
+
+AM_CPPFLAGS = \
+       -I$(top_srcdir)/src/lib \
+       -I$(top_srcdir)/src/lib-test \
+       -I$(top_srcdir)/src/lib-dcrypt \
+       -Wno-error=unused-function
+
+var-expand-parser.h: var-expand-parser.c
+
+libvar_expand_la_SOURCES = \
+       expansion-parameter.c \
+       expansion-statement.c \
+       expansion-filter.c \
+       expansion-filter-if.c \
+       expansion-program.c \
+       var-expand.c \
+       var-expand-parser.y \
+       var-expand-lexer.l
+
+BUILT_SOURCES = \
+       var-expand-parser.c \
+       var-expand-parser.h \
+       var-expand-lexer.c
+
+noinst_HEADERS = \
+       var-expand-parser-private.h \
+       var-expand-parser.h \
+       expansion.h
+
+headers = \
+       var-expand-new.h \
+       var-expand-private.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+
+test_programs = \
+       test-var-expand
+
+noinst_PROGRAMS = $(test_programs)
+
+test_libs = \
+       libvar_expand.la \
+       ../lib-test/libtest.la \
+       ../lib/liblib.la \
+       $(MODULE_LIBS)
+
+test_var_expand_SOURCE = test-var-expand.c
+test_var_expand_LDADD = $(test_libs)
+
+check-local:
+       for bin in $(test_programs); do \
+         if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+       done
diff --git a/src/lib-var-expand/expansion-filter-if.c b/src/lib-var-expand/expansion-filter-if.c
new file mode 100644 (file)
index 0000000..cb70840
--- /dev/null
@@ -0,0 +1,266 @@
+/* Copyright (c) 2024 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "var-expand-private.h"
+#include "expansion.h"
+#include "wildcard-match.h"
+
+#include <regex.h>
+
+enum var_expand_if_op {
+       OP_UNKNOWN,
+       OP_NUM_EQ,
+       OP_NUM_LT,
+       OP_NUM_LE,
+       OP_NUM_GT,
+       OP_NUM_GE,
+       OP_NUM_NE,
+/* put all numeric comparisons before this line */
+       OP_STR_EQ,
+       OP_STR_LT,
+       OP_STR_LE,
+       OP_STR_GT,
+       OP_STR_GE,
+       OP_STR_NE,
+       OP_STR_LIKE,
+       OP_STR_NOT_LIKE,
+       OP_STR_REGEXP,
+       OP_STR_NOT_REGEXP,
+/* keep this as last */
+       OP_COUNT
+};
+
+static enum var_expand_if_op var_expand_if_str_to_comp(const char *op)
+{
+       const char *ops[] = {
+               NULL,
+               "==",
+               "<",
+               "<=",
+               ">",
+               ">=",
+               "!=",
+               "eq",
+               "lt",
+               "le",
+               "gt",
+               "ge",
+               "ne",
+               "*",
+               "!*",
+               "~",
+               "!~",
+       };
+       static_assert_array_size(ops, OP_COUNT);
+       for (enum var_expand_if_op i = 1; i < OP_COUNT; i++) {
+               i_assert(ops[i] != NULL);
+               if (strcmp(op, ops[i]) == 0)
+                       return i;
+       }
+       return OP_UNKNOWN;
+}
+
+static int
+fn_if_cmp(struct var_expand_state *state, const struct var_expand_parameter *p_lhs,
+         enum var_expand_if_op op, const struct var_expand_parameter *p_rhs,
+         bool *result_r, const char **error_r)
+{
+       bool neg = FALSE;
+       if (op < OP_STR_EQ) {
+               intmax_t a;
+               intmax_t b;
+               if (var_expand_parameter_number_or_var(state, p_lhs, &a, error_r) < 0) {
+                       *error_r = t_strdup_printf("Left-hand side: %s", *error_r);
+                       return -1;
+               } else if (var_expand_parameter_number_or_var(state, p_rhs, &b, error_r) < 0) {
+                       *error_r = t_strdup_printf("Right-hand side: %s", *error_r);
+                       return -1;
+               }
+               switch (op) {
+               case OP_NUM_EQ:
+                       *result_r = a == b;
+                       return 0;
+               case OP_NUM_LT:
+                       *result_r = a < b;
+                       return 0;
+               case OP_NUM_LE:
+                       *result_r = a <= b;
+                       return 0;
+               case OP_NUM_GT:
+                       *result_r = a > b;
+                       return 0;
+               case OP_NUM_GE:
+                       *result_r = a >= b;
+                       return 0;
+               case OP_NUM_NE:
+                       *result_r = a != b;
+                       return 0;
+               default:
+                       i_panic("Missing numeric comparator %u", op);
+               }
+       }
+
+       const char *lhs, *rhs;
+       if (var_expand_parameter_string_or_var(state, p_lhs, &lhs, error_r) < 0) {
+               *error_r = t_strdup_printf("Left-hand side %s", *error_r);
+               return -1;
+       } else if (var_expand_parameter_string_or_var(state, p_rhs, &rhs, error_r) < 0) {
+               *error_r = t_strdup_printf("Right-hand side %s", *error_r);
+               return -1;
+       }
+
+       switch (op) {
+       case OP_STR_EQ:
+               *result_r = strcmp(lhs,rhs) == 0;
+               return 0;
+       case OP_STR_LT:
+               *result_r = strcmp(lhs,rhs) < 0;
+               return 0;
+       case OP_STR_LE:
+               *result_r = strcmp(lhs,rhs) <= 0;
+               return 0;
+       case OP_STR_GT:
+               *result_r = strcmp(lhs,rhs) > 0;
+               return 0;
+       case OP_STR_GE:
+               *result_r = strcmp(lhs,rhs) >= 0;
+               return 0;
+       case OP_STR_NE:
+               *result_r = strcmp(lhs,rhs) != 0;
+               return 0;
+       case OP_STR_LIKE:
+               *result_r = wildcard_match(lhs, rhs);
+               return 0;
+       case OP_STR_NOT_LIKE:
+               *result_r = !wildcard_match(lhs, rhs);
+               return 0;
+       case OP_STR_NOT_REGEXP:
+               neg = TRUE;
+               /* fall through */
+       case OP_STR_REGEXP: {
+               int ec;
+               bool res;
+               regex_t reg;
+               if ((ec = regcomp(&reg, rhs, REG_EXTENDED)) != 0) {
+                       size_t size;
+                       char *errbuf;
+                       size = regerror(ec, &reg, NULL, 0);
+                       errbuf = t_malloc_no0(size);
+                       (void)regerror(ec, &reg, errbuf, size);
+                       *error_r = t_strdup_printf("regexp() failed: %s",
+                                                  errbuf);
+                       return -1;
+               }
+               if ((ec = regexec(&reg, lhs, 0, 0, 0)) != 0) {
+                       i_assert(ec == REG_NOMATCH);
+                       res = FALSE;
+               } else {
+                       res = TRUE;
+               }
+               regfree(&reg);
+               /* this should be same as neg.
+                  if NOT_REGEXP, neg == TRUE and res should be FALSE
+                  if REGEXP, ned == FALSE, and res should be TRUE
+                */
+               *result_r = res != neg;
+               return 0;
+       }
+       default:
+               i_panic("Missing generic comparator %u", op);
+       }
+}
+
+int expansion_filter_if(const struct var_expand_statement *stmt,
+                       struct var_expand_state *state,
+                       const char **error_r)
+{
+       const char *_op;
+       bool use_first_as_lhs = !state->transfer_set;
+
+       const struct var_expand_parameter *p_lhs = NULL;
+       const struct var_expand_parameter *p_rhs = NULL;
+       const struct var_expand_parameter *p_true = NULL;
+       const struct var_expand_parameter *p_false = NULL;
+
+       enum {
+               STATE_LHS = 0,
+               STATE_OP,
+               STATE_RHS,
+               STATE_TRUE,
+               STATE_FALSE,
+               STATE_DONE,
+       } parse_state;
+
+       if (use_first_as_lhs)
+               parse_state = STATE_LHS;
+       else
+               parse_state = STATE_OP;
+
+       struct var_expand_parameter_iter_context *ctx =
+               var_expand_parameter_iter_init(stmt);
+       while (var_expand_parameter_iter_more(ctx)) {
+               const struct var_expand_parameter *par =
+                       var_expand_parameter_iter_next(ctx);
+               const char *key = var_expand_parameter_key(par);
+               if (key != NULL) {
+                       ERROR_UNSUPPORTED_KEY(key);
+               }
+               switch (parse_state) {
+               case STATE_LHS: p_lhs = par; break;
+               case STATE_OP:
+                       if (var_expand_parameter_string_or_var(state, par, &_op, error_r) < 0) {
+                               *error_r = t_strdup_printf("Comparator: %s", *error_r);
+                               return -1;
+                       }
+                       break;
+               case STATE_RHS: p_rhs = par; break;
+               case STATE_TRUE: p_true = par; break;
+               case STATE_FALSE: p_false = par; break;
+               case STATE_DONE: ERROR_TOO_MANY_UNNAMED_PARAMETERS;
+               }
+               parse_state++;
+       }
+
+       if (parse_state != STATE_DONE) {
+               *error_r = "Missing parameters";
+               return -1;
+       }
+
+       enum var_expand_if_op op = var_expand_if_str_to_comp(_op);
+
+       if (op == OP_UNKNOWN) {
+               *error_r = t_strdup_printf("Unsupported comparator '%s'", _op);
+               return -1;
+       }
+
+       if (!use_first_as_lhs) {
+               if (var_expand_parameter_from_state(state, op < OP_STR_EQ,
+                                                   &p_lhs) < 0) {
+                       if (op < OP_STR_EQ) {
+                               *error_r = "Input is not a number";
+                       } else  {
+                               *error_r = "No value to use as left-hand in if";
+                       }
+                       return -1;
+               }
+       }
+
+       i_assert(p_lhs != NULL);
+
+       bool result;
+       if (fn_if_cmp(state, p_lhs, op, p_rhs, &result, error_r) < 0)
+               return -1;
+       const struct var_expand_parameter *res = result ? p_true : p_false;
+       const char *value;
+
+       if (var_expand_parameter_any_or_var(state, res, &value, error_r) < 0) {
+               *error_r = t_strdup_printf("%s: %s",
+                                          result ? "True branch" : "False branch",
+                                          *error_r);
+               return -1;
+       }
+
+       var_expand_state_set_transfer(state, value);
+
+       return 0;
+}
diff --git a/src/lib-var-expand/expansion-filter.c b/src/lib-var-expand/expansion-filter.c
new file mode 100644 (file)
index 0000000..7ae0722
--- /dev/null
@@ -0,0 +1,1233 @@
+/* Copyright (c) 2024 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "base64.h"
+#include "hash-method.h"
+#include "hex-binary.h"
+#include "str.h"
+#include "strescape.h"
+#include "str-sanitize.h"
+#include "var-expand-private.h"
+#include "expansion.h"
+
+#include <ctype.h>
+#include <regex.h>
+
+ARRAY_DEFINE_TYPE(var_expand_filter, struct var_expand_filter);
+static ARRAY_TYPE(var_expand_filter) dyn_filters = ARRAY_INIT;
+
+static int fn_lookup(const struct var_expand_statement *stmt,
+                    struct var_expand_state *state, const char **error_r)
+{
+       const struct var_expand_parameter *par = stmt->params;
+       if (par == NULL) {
+               *error_r = "Missing name to lookup";
+               return -1;
+       }
+
+       const char *key = var_expand_parameter_key(par);
+       if (key != NULL)
+               ERROR_UNSUPPORTED_KEY(key);
+
+       var_expand_state_unset_transfer(state);
+
+       const char *value;
+       if (var_expand_parameter_string_or_var(state, par, &key, error_r) < 0)
+               return -1;
+
+       if (var_expand_state_lookup_variable(state, key, &value, error_r) == 0) {
+               var_expand_state_set_transfer(state, value);
+               return 0;
+       } else {
+               return -1;
+       }
+}
+
+static int fn_lower(const struct var_expand_statement *stmt,
+                   struct var_expand_state *state, const char **error_r)
+{
+       ERROR_IF_ANY_PARAMETERS;
+       ERROR_IF_NO_TRANSFER_TO("lower");
+
+       char *value = str_c_modifiable(state->transfer);
+       str_lcase(value);
+       return 0;
+}
+
+static int fn_upper(const struct var_expand_statement *stmt,
+                   struct var_expand_state *state, const char **error_r)
+{
+       ERROR_IF_ANY_PARAMETERS;
+       ERROR_IF_NO_TRANSFER_TO("upper");
+
+       char *value = str_c_modifiable(state->transfer);
+       str_ucase(value);
+       return 0;
+}
+
+static int fn_default(const struct var_expand_statement *stmt,
+                     struct var_expand_state *state, const char **error_r)
+{
+       if (state->transfer_set && state->transfer->used > 0)
+               return 0;
+
+       /* allow default without parameters to expand into literal empty */
+       const char *value;
+       const char *key;
+       if (stmt->params == NULL)
+               value = "";
+       else if ((key = var_expand_parameter_key(stmt->params)) != NULL)
+               ERROR_UNSUPPORTED_KEY(key);
+       else if (var_expand_parameter_any_or_var(state, stmt->params, &value,
+                                                  error_r) < 0)
+               return -1;
+       else if (stmt->params->next != NULL)
+               ERROR_TOO_MANY_UNNAMED_PARAMETERS;
+
+       var_expand_state_set_transfer(state, value);
+
+       return 0;
+}
+
+static int fn_literal(const struct var_expand_statement *stmt,
+                     struct var_expand_state *state,
+                     const char **error_r)
+{
+       const char *value;
+
+       ERROR_IF_NO_PARAMETERS;
+       const char *key = var_expand_parameter_key(stmt->params);
+       if (key != NULL)
+               ERROR_UNSUPPORTED_KEY(key);
+
+       if (var_expand_parameter_any_or_var(state, stmt->params, &value, error_r) < 0)
+               return -1;
+       var_expand_state_set_transfer(state, value);
+       return 0;
+}
+
+static int fn_calculate(const struct var_expand_statement *stmt,
+                       struct var_expand_state *state, const char **error_r)
+{
+       ERROR_IF_NO_PARAMETERS;
+
+       struct var_expand_parameter_iter_context *ctx =
+               var_expand_parameter_iter_init(stmt);
+
+       enum var_expand_statement_operator oper =
+               VAR_EXPAND_STATEMENT_OPER_COUNT;
+       intmax_t right;
+
+       while (var_expand_parameter_iter_more(ctx)) {
+               const struct var_expand_parameter *par =
+                       var_expand_parameter_iter_next(ctx);
+               const char *key = var_expand_parameter_key(par);
+               if (key != NULL)
+                       ERROR_UNSUPPORTED_KEY(key);
+
+               switch (var_expand_parameter_idx(par)) {
+               case 0:
+                       if (var_expand_parameter_number(par, FALSE, &right) < 0) {
+                               *error_r = "Missing operator";
+                               return -1;
+                       }
+                       oper = right;
+                       break;
+               case 1:
+                       if (var_expand_parameter_number_or_var(state, par, &right,
+                                                              error_r) < 0)
+                               return -1;
+                       break;
+               default:
+                       ERROR_TOO_MANY_UNNAMED_PARAMETERS;
+               }
+       }
+
+       ERROR_IF_NO_TRANSFER_TO("calculate");
+       intmax_t value;
+
+       /* special case for the society of config file prettification:
+          binary input can be treated as 64 bit unsigned integer
+          for modulo operations only. */
+       if (state->transfer_binary && oper == VAR_EXPAND_STATEMENT_OPER_MODULO) {
+               if (right < 0) {
+                       *error_r = "Binary modulo must be positive integer";
+                       return -1;
+               }
+               uintmax_t tmp = 0;
+               const unsigned char *input = state->transfer->data;
+               for (size_t i = state->transfer->used - I_MIN(state->transfer->used, sizeof(tmp));
+                    i < state->transfer->used; i++) {
+                       tmp <<= 8;
+                       tmp |= input[i];
+               }
+               tmp %= right;
+               var_expand_state_set_transfer(state, dec2str(tmp));
+               return 0;
+       }
+
+       /* transfer must be number */
+       if (str_to_intmax(str_c(state->transfer), &value) < 0) {
+               *error_r = "Input is not a number";
+               return -1;
+       }
+
+       switch (oper) {
+       case VAR_EXPAND_STATEMENT_OPER_PLUS:
+               value += right;
+               break;
+       case VAR_EXPAND_STATEMENT_OPER_MINUS:
+               value -= right;
+               break;
+       case VAR_EXPAND_STATEMENT_OPER_STAR:
+               value *= right;
+               break;
+       case VAR_EXPAND_STATEMENT_OPER_SLASH:
+               if (right == 0) {
+                       *error_r = "Division by zero";
+                       return -1;
+               }
+               value /= right;
+               break;
+       case VAR_EXPAND_STATEMENT_OPER_MODULO:
+               if (right == 0) {
+                       *error_r = "Modulo by zero";
+                       return -1;
+               }
+               value %= right;
+               break;
+       case VAR_EXPAND_STATEMENT_OPER_COUNT:
+       default:
+               i_unreached();
+       }
+
+       /* Should usually use var_expand_state_set_transfer
+          but this way it avoids a t_strdup_printf round. */
+       var_expand_state_unset_transfer(state);
+       str_printfa(state->transfer, "%jd", value);
+       state->transfer_set = TRUE;
+       return 0;
+}
+
+static int fn_concat(const struct var_expand_statement *stmt,
+                    struct var_expand_state *state, const char **error_r)
+{
+       ERROR_IF_NO_PARAMETERS;
+
+       string_t *result = t_str_new(32);
+
+       /* start with transfer */
+       if (state->transfer_set)
+               str_append_data(result, state->transfer->data, state->transfer->used);
+
+       struct var_expand_parameter_iter_context *ctx =
+               var_expand_parameter_iter_init(stmt);
+
+       while (var_expand_parameter_iter_more(ctx)) {
+               const struct var_expand_parameter *par =
+                       var_expand_parameter_iter_next(ctx);
+               const char *value;
+               const char *key = var_expand_parameter_key(par);
+               if (key != NULL)
+                       ERROR_UNSUPPORTED_KEY(key);
+               if (var_expand_parameter_any_or_var(state, par, &value, error_r) < 0)
+                       return -1;
+               str_append(result, value);
+       }
+
+       var_expand_state_set_transfer_data(state, result->data, result->used);
+       return 0;
+}
+
+static int fn_hash_algo(const struct var_expand_statement *stmt, const char *algo,
+                       bool algo_from_param, struct var_expand_state *state,
+                       const char **error_r)
+{
+       const struct hash_method *method;
+
+       method = hash_method_lookup(algo);
+       if (method == NULL) {
+               *error_r = t_strdup_printf("Unsupported algorithm '%s'",
+                                          algo);
+               return -1;
+       }
+
+       intmax_t rounds = 1;
+       const char *salt = "";
+
+       struct var_expand_parameter_iter_context * ctx =
+               var_expand_parameter_iter_init(stmt);
+       while (var_expand_parameter_iter_more(ctx)) {
+               const struct var_expand_parameter *par =
+                       var_expand_parameter_iter_next(ctx);
+               const char *key = var_expand_parameter_key(par);
+               /* if called as hash(), allow algorithm as idx 0 */
+               if (key == NULL) {
+                       if (!algo_from_param || var_expand_parameter_idx(par) > 0)
+                               ERROR_TOO_MANY_UNNAMED_PARAMETERS;
+                       if (var_expand_parameter_idx(par) == 0)
+                               continue;
+               }
+               if (strcmp(key, "rounds") == 0) {
+                       if (var_expand_parameter_number_or_var(state, par,
+                                                              &rounds, error_r) < 0)
+                               return -1;
+               } else if (strcmp(key, "salt") == 0) {
+                       if (var_expand_parameter_string_or_var(state, par,
+                                                              &salt, error_r) < 0)
+                               return -1;
+               } else
+                       ERROR_UNSUPPORTED_KEY(key);
+       }
+
+       ERROR_IF_NO_TRANSFER_TO(algo_from_param ? "hash" : algo);
+
+       buffer_t *input = t_buffer_create(state->transfer->used);
+       buffer_append(input, state->transfer->data, state->transfer->used);
+
+       for (int i = 0; i < rounds; i++) {
+               unsigned char ctx[method->context_size];
+               unsigned char result[method->digest_size];
+
+               method->init(ctx);
+               if (salt != NULL)
+                       method->loop(ctx, salt, strlen(salt));
+               method->loop(ctx, input->data, input->used);
+               method->result(ctx, result);
+               buffer_set_used_size(input, 0);
+               buffer_append(input, result, sizeof(result));
+       }
+
+       var_expand_state_set_transfer_binary(state, input->data, input->used);
+
+       return 0;
+}
+
+static int fn_hash(const struct var_expand_statement *stmt,
+                  struct var_expand_state *state, const char **error_r)
+{
+       ERROR_IF_NO_PARAMETERS;
+
+       const struct var_expand_parameter *par = stmt->params;
+       const char *algo;
+
+       if (var_expand_parameter_idx(par) == -1) {
+               *error_r = "No algorithm as first parameter";
+               return -1;
+       } else if (var_expand_parameter_string_or_var(state, par, &algo, error_r) < 0)
+               return -1;
+
+       return fn_hash_algo(stmt, algo, TRUE, state, error_r);
+}
+
+
+static int fn_md5(const struct var_expand_statement *stmt,
+                 struct var_expand_state *state, const char **error_r)
+{
+       return fn_hash_algo(stmt, "md5", FALSE, state, error_r);
+}
+
+static int fn_sha1(const struct var_expand_statement *stmt,
+                  struct var_expand_state *state, const char **error_r)
+{
+       return fn_hash_algo(stmt, "sha1", FALSE, state, error_r);
+}
+
+static int fn_sha256(const struct var_expand_statement *stmt,
+                    struct var_expand_state *state, const char **error_r)
+{
+       return fn_hash_algo(stmt, "sha256", FALSE, state, error_r);
+}
+
+static int fn_sha384(const struct var_expand_statement *stmt,
+                    struct var_expand_state *state, const char **error_r)
+{
+       return fn_hash_algo(stmt, "sha384", FALSE, state, error_r);
+}
+
+static int fn_sha512(const struct var_expand_statement *stmt,
+                    struct var_expand_state *state, const char **error_r)
+{
+       return fn_hash_algo(stmt, "sha512", FALSE, state, error_r);
+}
+
+static int fn_base64(const struct var_expand_statement *stmt,
+                    struct var_expand_state *state, const char **error_r)
+{
+       enum base64_encode_flags flags = 0;
+       const struct base64_scheme *scheme = &base64_scheme;
+       struct var_expand_parameter_iter_context *ctx =
+               var_expand_parameter_iter_init(stmt);
+       while (var_expand_parameter_iter_more(ctx)) {
+               const struct var_expand_parameter *par =
+                       var_expand_parameter_iter_next(ctx);
+               const char *key = var_expand_parameter_key(par);
+               if (key == NULL)
+                       ERROR_TOO_MANY_UNNAMED_PARAMETERS;
+               bool value;
+               if (strcmp(key, "pad") == 0) {
+                       if (var_expand_parameter_bool_or_var(state, par,
+                                                            &value, error_r) < 0)
+                               return -1;
+                       if (!value)
+                               flags |= BASE64_ENCODE_FLAG_NO_PADDING;
+               } else if (strcmp(key, "url") == 0) {
+                       if (var_expand_parameter_bool_or_var(state, par,
+                                                            &value, error_r) < 0)
+                               return -1;
+                       if (value)
+                               scheme = &base64url_scheme;
+               } else
+                       ERROR_UNSUPPORTED_KEY(key);
+       }
+
+       ERROR_IF_NO_TRANSFER_TO("base64");
+
+       buffer_t *result =
+               t_base64_scheme_encode(scheme, flags, UINT_MAX,
+                                      state->transfer->data, state->transfer->used);
+       var_expand_state_set_transfer(state, str_c(result));
+       return 0;
+}
+
+static int fn_unbase64(const struct var_expand_statement *stmt,
+                      struct var_expand_state *state, const char **error_r)
+{
+       enum base64_decode_flags flags = 0;
+       const struct base64_scheme *scheme = &base64_scheme;
+       struct var_expand_parameter_iter_context *ctx =
+               var_expand_parameter_iter_init(stmt);
+       while (var_expand_parameter_iter_more(ctx)) {
+               const struct var_expand_parameter *par =
+                       var_expand_parameter_iter_next(ctx);
+               const char *key = var_expand_parameter_key(par);
+               if (key == NULL)
+                       ERROR_TOO_MANY_UNNAMED_PARAMETERS;
+               intmax_t value;
+               if (strcmp(key, "pad") == 0) {
+                       if (var_expand_parameter_number_or_var(state, par,
+                                                              &value, error_r) < 0)
+                               return -1;
+                       if (value == 0)
+                               flags |= BASE64_ENCODE_FLAG_NO_PADDING;
+                       else if (value == 1)
+                               ; /* do nothing */
+                       else {
+                               *error_r = "Supported values for pad are 0 or 1";
+                               return -1;
+                       }
+               } else if (strcmp(key, "url") == 0) {
+                       if (var_expand_parameter_number_or_var(state, par,
+                                                              &value, error_r) < 0)
+                               return -1;
+                       if (value == 0)
+                               ; /* do nothing */
+                       else if (value == 1)
+                               scheme = &base64url_scheme;
+                       else {
+                               *error_r = "Supported values for url are 0 or 1";
+                               return -1;
+                       }
+               } else
+                       ERROR_UNSUPPORTED_KEY(key);
+       }
+
+       ERROR_IF_NO_TRANSFER_TO("unbase64");
+
+       buffer_t *result =
+               t_base64_scheme_decode(scheme, flags, state->transfer->data,
+                                      state->transfer->used);
+       var_expand_state_set_transfer_binary(state, result->data, result->used);
+       return 0;
+}
+
+static int fn_hex(const struct var_expand_statement *stmt,
+                 struct var_expand_state *state, const char **error_r)
+{
+       uintmax_t number;
+       intmax_t width = 0;
+
+       struct var_expand_parameter_iter_context *iter =
+               var_expand_parameter_iter_init(stmt);
+       while (var_expand_parameter_iter_more(iter)) {
+               const struct var_expand_parameter *par =
+                       var_expand_parameter_iter_next(iter);
+               const char *key = var_expand_parameter_key(par);
+               if (key != NULL)
+                       ERROR_UNSUPPORTED_KEY(key);
+               if (var_expand_parameter_idx(par) != 0)
+                       ERROR_TOO_MANY_UNNAMED_PARAMETERS;
+               if (var_expand_parameter_number_or_var(state, par, &width, error_r) < 0)
+                       return -1;
+       }
+
+       ERROR_IF_NO_TRANSFER_TO("hex");
+
+       if (str_to_uintmax(str_c(state->transfer), &number) < 0) {
+               *error_r = "Input is not a number";
+               return -1;
+       }
+
+       str_truncate(state->transfer, 0);
+       str_printfa(state->transfer, "%jx", number);
+
+       if (width < 0) {
+               width = -width;
+               while (str_len(state->transfer) < (size_t)width)
+                       str_append_c(state->transfer, '0');
+               str_truncate(state->transfer, width);
+       } else if (width > 0) {
+               while (str_len(state->transfer) < (size_t)width)
+                       str_insert(state->transfer, 0, "0");
+               str_delete(state->transfer, 0, str_len(state->transfer) - width);
+       }
+
+       return 0;
+}
+
+static int fn_unhex(const struct var_expand_statement *stmt,
+                   struct var_expand_state *state, const char **error_r)
+{
+       ERROR_IF_ANY_PARAMETERS;
+       ERROR_IF_NO_TRANSFER_TO("unhex");
+
+       uintmax_t number;
+
+       if (str_to_uintmax_hex(str_c(state->transfer), &number) < 0) {
+               *error_r = "Input is not a hex number";
+               return -1;
+       }
+
+       var_expand_state_set_transfer(state, dec2str(number));
+
+       return 0;
+}
+
+
+static int fn_hexlify(const struct var_expand_statement *stmt,
+                     struct var_expand_state *state, const char **error_r)
+{
+       intmax_t width = 0;
+
+       if (stmt->params != NULL) {
+               const char *key = var_expand_parameter_key(stmt->params);
+               if (key != NULL)
+                       ERROR_UNSUPPORTED_KEY(key);
+               if (var_expand_parameter_number_or_var(state, stmt->params,
+                                                      &width, error_r) < 0)
+                       return -1;
+               if (width < 0) {
+                       *error_r = "Width must be positive";
+                       return -1;
+               }
+       }
+
+       ERROR_IF_NO_TRANSFER_TO("hexlify");
+
+       const char *result =
+               binary_to_hex(state->transfer->data, state->transfer->used);
+       size_t rlen = strlen(result);
+       if (width == 0) {
+               /* pass */
+       } else if (rlen < (uintmax_t)width) {
+               string_t *tmp = t_str_new(width);
+               width -= strlen(result);
+               for (; width > 0; width--)
+                       str_append_c(tmp, '0');
+               str_append(tmp, result);
+               result = str_c(tmp);
+       } else if (rlen > (uintmax_t)width)
+               result = t_strndup(result, width);
+       var_expand_state_set_transfer(state, result);
+
+       return 0;
+}
+
+static int fn_unhexlify(const struct var_expand_statement *stmt,
+                       struct var_expand_state *state, const char **error_r)
+{
+       ERROR_IF_ANY_PARAMETERS;
+       ERROR_IF_NO_TRANSFER_TO("unhexlify");
+
+       if (state->transfer->used % 2 != 0) {
+               *error_r = "Not a hex value";
+               return -1;
+       }
+
+       buffer_t *dest = t_buffer_create(state->transfer->used / 2);
+       if (hex_to_binary(str_c(state->transfer), dest) == 0)
+               var_expand_state_set_transfer_binary(state, dest->data, dest->used);
+       else {
+               *error_r = "Not a hex value";
+               return -1;
+       }
+       return 0;
+}
+
+static int fn_reverse(const struct var_expand_statement *stmt,
+                     struct var_expand_state *state, const char **error_r)
+{
+       ERROR_IF_ANY_PARAMETERS;
+       ERROR_IF_NO_TRANSFER_TO("reverse");
+
+       buffer_t *new_value = t_buffer_create(state->transfer->used);
+       const unsigned char *tmp = state->transfer->data;
+       for (size_t i = 1; i <= state->transfer->used; i++)
+               buffer_append_c(new_value, tmp[state->transfer->used - i]);
+       var_expand_state_set_transfer_data(state, new_value->data, new_value->used);
+
+       return 0;
+}
+
+static int fn_truncate(const struct var_expand_statement *stmt,
+                      struct var_expand_state *state, const char **error_r)
+{
+       ERROR_IF_NO_PARAMETERS;
+
+       size_t len = SIZE_MAX;
+       bool bits = FALSE;
+
+       struct var_expand_parameter_iter_context *ctx =
+               var_expand_parameter_iter_init(stmt);
+       while (var_expand_parameter_iter_more(ctx)) {
+               const struct var_expand_parameter *par =
+                       var_expand_parameter_iter_next(ctx);
+               const char *key = var_expand_parameter_key(par);
+               if (null_strcmp(key, "bits") == 0) {
+                       intmax_t value;
+                       if (var_expand_parameter_number_or_var(state, par,
+                                                              &value, error_r) < 0)
+                               return -1;
+                       if (value < 0 || value > SSIZE_MAX) {
+                               *error_r = "Value of out of bounds";
+                               return -1;
+                       }
+                       len = (size_t)value;
+                       bits = TRUE;
+               } else if (var_expand_parameter_idx(par) == 0) {
+                       intmax_t value;
+                       if (var_expand_parameter_number_or_var(state, par,
+                                                              &value, error_r) < 0)
+                               return -1;
+                       if (value < 0 || value > SSIZE_MAX) {
+                               *error_r = "Value of out of bounds";
+                               return -1;
+                       }
+                       len = (size_t)value;
+               } else if (key != NULL)
+                       ERROR_UNSUPPORTED_KEY(key);
+               else
+                       ERROR_TOO_MANY_UNNAMED_PARAMETERS;
+       }
+
+       if (len == SIZE_MAX) {
+               *error_r = "Missing truncation length";
+               return -1;
+       }
+
+       ERROR_IF_NO_TRANSFER_TO("truncate");
+
+       buffer_t *new_value = t_buffer_create(state->transfer->used);
+       buffer_append_buf(new_value, state->transfer, 0, state->transfer->used);
+
+       if (bits)
+               buffer_truncate_rshift_bits(new_value, len);
+       else
+               buffer_set_used_size(new_value, len);
+
+       var_expand_state_set_transfer_data(state, new_value->data, new_value->used);
+
+       return 0;
+}
+
+static int fn_substr(const struct var_expand_statement *stmt,
+                    struct var_expand_state *state, const char **error_r)
+{
+       intmax_t off = -INT_MAX, len = state->transfer->used;
+       bool got_off = FALSE, got_len = FALSE;
+
+       ERROR_IF_NO_PARAMETERS;
+
+       struct var_expand_parameter_iter_context *ctx =
+               var_expand_parameter_iter_init(stmt);
+       while (var_expand_parameter_iter_more(ctx)) {
+               intmax_t value;
+               const struct var_expand_parameter *par =
+                       var_expand_parameter_iter_next(ctx);
+               const char *key = var_expand_parameter_key(par);
+               if (key != NULL)
+                       ERROR_UNSUPPORTED_KEY(key);
+               if (var_expand_parameter_number_or_var(state, par, &value,
+                                                      error_r) < 0)
+                       return -1;
+               if (var_expand_parameter_idx(par) == 0) {
+                       off = value;
+                       got_off = TRUE;
+               } else if (var_expand_parameter_idx(par) == 1) {
+                       len = value;
+                       got_len = TRUE;
+               } else
+                       ERROR_TOO_MANY_UNNAMED_PARAMETERS;
+       }
+
+       if (!got_off) {
+               *error_r = "Missing offset parameter";
+               return -1;
+       }
+
+       ERROR_IF_NO_TRANSFER_TO("substring");
+
+       if (off < -(intmax_t)state->transfer->used || off > (intmax_t)state->transfer->used) {
+               *error_r = "Offset out of bounds";
+               return -1;
+       }
+
+       if (len < -(intmax_t)state->transfer->used || len > (intmax_t)state->transfer->used) {
+               *error_r = "Length out of bounds";
+               return -1;
+       }
+
+       if (len == 0) {
+               var_expand_state_set_transfer_data(state, "", 0);
+               return 0;
+       }
+
+       if (off < 0)
+               off = (intmax_t)state->transfer->used + off;
+
+       /* from offset to end */
+       if (!got_len)
+               len = (intmax_t)state->transfer->used - off;
+       else if (len < 0) {
+               /* negative offset leaves that many characters from end */
+               len = (intmax_t)state->transfer->used + len;
+               if (len < off) {
+                       *error_r = "Length out of bounds";
+                       return -1;
+               }
+               len -= off;
+       }
+
+       if (off < 0 || off > (intmax_t)state->transfer->used) {
+               *error_r = "Offset out of bounds";
+               return -1;
+       }
+
+       if (len < 0 || len + off > (intmax_t)state->transfer->used) {
+               *error_r = "Length out of bounds";
+               return -1;
+       } else if (len == 0) {
+               var_expand_state_set_transfer_data(state, "", 0);
+       } else {
+               const unsigned char *data =
+                       p_memdup(pool_datastack_create(),
+                                CONST_PTR_OFFSET(state->transfer->data, off), len);
+               var_expand_state_set_transfer_data(state, data, len);
+       }
+       return 0;
+}
+
+static int fn_ldap_dn(const struct var_expand_statement *stmt,
+                     struct var_expand_state *state, const char **error_r)
+{
+       ERROR_IF_ANY_PARAMETERS;
+       ERROR_IF_NO_TRANSFER_TO("convert to ldap_dn");
+
+       string_t *ret = t_str_new(256);
+       const char *str = str_c(state->transfer);
+
+       while (*str != '\0') {
+               if (*str == '.')
+                       str_append(ret, ",dc=");
+               else
+                       str_append_c(ret, *str);
+               str++;
+       }
+
+       var_expand_state_set_transfer_data(state, ret->data, ret->used);
+       return 0;
+}
+
+static int fn_regexp(const struct var_expand_statement *stmt,
+                    struct var_expand_state *state, const char **error_r)
+{
+       ERROR_IF_NO_PARAMETERS;
+
+       /* pattern and replacement */
+       const char *pat = NULL;
+       const char *rep = NULL;
+       const char *error;
+       struct var_expand_parameter_iter_context *ctx =
+               var_expand_parameter_iter_init(stmt);
+       while (var_expand_parameter_iter_more(ctx)) {
+               const struct var_expand_parameter *par =
+                       var_expand_parameter_iter_next(ctx);
+               const char *key = var_expand_parameter_key(par);
+               if (key != NULL)
+                       ERROR_UNSUPPORTED_KEY(key);
+               const char *value;
+               if (var_expand_parameter_string_or_var(state, par, &value,
+                                                      &error) < 0)
+                       return -1;
+               switch (var_expand_parameter_idx(par)) {
+               case 0:
+                       pat = value;
+                       break;
+               case 1:
+                       rep = value;
+                       break;
+               default:
+                       ERROR_TOO_MANY_UNNAMED_PARAMETERS;
+               }
+       }
+
+       if (pat == NULL) {
+               *error_r = "Missing pattern and replacement parameters";
+               return -1;
+       }
+
+       if (rep == NULL) {
+               *error_r = "Missing replacement parameter";
+               return -1;
+       }
+
+       ERROR_IF_NO_TRANSFER_TO("regexp");
+
+       int ret;
+       regex_t reg;
+       regmatch_t matches[10];
+       const char *input = str_c(state->transfer);
+       i_zero(&reg);
+       i_zero(&matches);
+       if ((ret = regcomp(&reg, pat, REG_EXTENDED)) != 0) {
+               char errbuf[1024] = {0};
+               (void)regerror(ret, &reg, errbuf, sizeof(errbuf));
+               regfree(&reg);
+               *error_r = t_strdup(errbuf);
+               return -1;
+       }
+
+       ret = regexec(&reg, input, N_ELEMENTS(matches), matches, 0);
+       if (ret == REG_NOMATCH) {
+               /* no match, do not modify */
+               regfree(&reg);
+               return 0;
+       }
+
+       /* perform replacement */
+       string_t *dest = t_str_new(strlen(rep));
+       const char *p0 = rep;
+       const char *p1;
+       ret = 0;
+
+       /* Supports up to 9 capture groups,
+        * if we need more, then this code should
+        * be refactored to see how many we really need
+        * and create a proper template from this. */
+       while ((p1 = strchr(p0, '\\')) != NULL) {
+               if (i_isdigit(p1[1])) {
+                       /* looks like a placeholder */
+                       str_append_data(dest, p0, p1 - p0);
+                       unsigned int g = p1[1] - '0';
+                       if (g >= sizeof(matches) ||
+                           matches[g].rm_so == -1) {
+                               *error_r = "Invalid capture group";
+                               ret = -1;
+                               break;
+                       }
+                       i_assert(matches[g].rm_eo >= matches[g].rm_so);
+                       str_append_data(dest, input + matches[g].rm_so,
+                                       matches[g].rm_eo - matches[g].rm_so);
+                       p0 = p1 + 2;
+               } else {
+                       str_append_c(dest, *p1);
+                       p1++;
+               }
+       }
+
+       regfree(&reg);
+
+       if (ret == 0) {
+               str_append(dest, p0);
+               var_expand_state_set_transfer_data(state, dest->data, dest->used);
+       }
+
+       return ret == 0 ? 0 : -1;
+}
+
+static int fn_number(const struct var_expand_statement *stmt, bool be,
+                    struct var_expand_state *state, const char **error_r)
+{
+       ERROR_IF_ANY_PARAMETERS;
+       ERROR_IF_NO_TRANSFER_TO("convert to number");
+       const unsigned char *data = state->transfer->data;
+       size_t len = state->transfer->used;
+       uintmax_t result;
+
+       /* see if we can convert input bytes to a number */
+       if (len == sizeof(uint8_t)) {
+               if (be)
+                       result = be8_to_cpu_unaligned(data);
+               else
+                       result = le8_to_cpu_unaligned(data);
+       } else if (len == sizeof(uint16_t)) {
+               if (be)
+                       result = be16_to_cpu_unaligned(data);
+               else
+                       result = le16_to_cpu_unaligned(data);
+       } else if (len == sizeof(uint32_t)) {
+               if (be)
+                       result = be32_to_cpu_unaligned(data);
+               else
+                       result = le32_to_cpu_unaligned(data);
+       } else if (len == sizeof(uint64_t)) {
+               if (be)
+                       result = be64_to_cpu_unaligned(data);
+               else
+                       result = le64_to_cpu_unaligned(data);
+       } else {
+               *error_r = t_strdup_printf("Cannot convert '%zu' bytes to number",
+                                          len);
+               return -1;
+       }
+
+       var_expand_state_set_transfer(state, dec2str(result));
+       return 0;
+}
+
+static int fn_be_number(const struct var_expand_statement *stmt,
+                       struct var_expand_state *state, const char **error_r)
+{
+       return fn_number(stmt, TRUE, state ,error_r);
+}
+
+static int fn_le_number(const struct var_expand_statement *stmt,
+                       struct var_expand_state *state, const char **error_r)
+{
+       return fn_number(stmt, FALSE, state, error_r);
+}
+
+static int fn_index_common(struct var_expand_state *state, int index,
+                          const char *separator, const char **error_r)
+{
+       const char *p;
+       const char *token;
+       const char *input = str_c(state->transfer);
+       const char *end = CONST_PTR_OFFSET(input, str_len(state->transfer));
+       ARRAY_TYPE(const_string) tokens;
+       t_array_init(&tokens, 2);
+
+       while ((p = strstr(input, separator)) != NULL) {
+               token = t_strdup_until(input, p);
+               array_push_back(&tokens, &token);
+               input = p + strlen(separator);
+               i_assert(input <= end);
+       }
+       token = t_strdup(input);
+       array_push_back(&tokens, &token);
+
+       if (index < 0)
+               index = (int)array_count(&tokens) + index;
+
+       if (index < 0 || (unsigned int)index >= array_count(&tokens)) {
+               *error_r = "Position out of bounds";
+               return -1;
+       }
+
+       token = array_idx_elem(&tokens, index);
+
+       var_expand_state_set_transfer(state, token);
+       return 0;
+}
+
+static int fn_index(const struct var_expand_statement *stmt,
+                   struct var_expand_state *state, const char **error_r)
+{
+       ERROR_IF_NO_PARAMETERS;
+
+       const char *separator = NULL;
+       int idx = 0;
+       bool got_idx;
+
+       struct var_expand_parameter_iter_context *ctx =
+               var_expand_parameter_iter_init(stmt);
+       while (var_expand_parameter_iter_more(ctx)) {
+               const struct var_expand_parameter *par =
+                       var_expand_parameter_iter_next(ctx);
+               const char *key = var_expand_parameter_key(par);
+               intmax_t value;
+               if (key != NULL)
+                       ERROR_UNSUPPORTED_KEY(key);
+               switch (var_expand_parameter_idx(par)) {
+               case 0:
+                       if (var_expand_parameter_string_or_var(state, par,
+                                                              &separator, error_r) < 0)
+                               return -1;
+                       break;
+               case 1:
+                       if (var_expand_parameter_number_or_var(state, par,
+                                                              &value, error_r) < 0)
+                               return -1;
+                       else if (value < INT_MIN || value > INT_MAX) {
+                               *error_r = "Position out of bounds";
+                               return -1;
+                       }
+                       idx = (int)value;
+                       got_idx = TRUE;
+                       break;
+               default:
+                       ERROR_TOO_MANY_UNNAMED_PARAMETERS;
+               }
+       }
+
+       if (separator == NULL) {
+               *error_r = "Missing separator and index parameters";
+               return -1;
+       }
+
+       if (!got_idx) {
+               *error_r = "Missing index parameter";
+               return -1;
+       }
+
+       ERROR_IF_NO_TRANSFER_TO("index");
+
+       return fn_index_common(state, idx, separator, error_r);
+}
+
+static int fn_username(const struct var_expand_statement *stmt,
+                      struct var_expand_state *state, const char **error_r)
+{
+       ERROR_IF_ANY_PARAMETERS;
+       ERROR_IF_NO_TRANSFER_TO("get username from");
+
+       return fn_index_common(state, 0, "@", error_r);
+}
+
+static int fn_domain(const struct var_expand_statement *stmt,
+                    struct var_expand_state *state, const char **error_r)
+{
+       ERROR_IF_ANY_PARAMETERS;
+       ERROR_IF_NO_TRANSFER_TO("get domain from");
+
+       /* This function needs to return the whole string after @ character
+          even if it contains @ characters. */
+       const char *input = str_c(state->transfer);
+       var_expand_state_set_transfer(state, i_strchr_to_next(input, '@'));
+       return 0;
+}
+
+static int fn_list(const struct var_expand_statement *stmt,
+                  struct var_expand_state *state, const char **error_r)
+{
+       /* allow optionally specifying separator */
+       const char *sep = ",";
+       struct var_expand_parameter_iter_context *iter =
+               var_expand_parameter_iter_init(stmt);
+       while (var_expand_parameter_iter_more(iter)) {
+               const struct var_expand_parameter *par =
+                       var_expand_parameter_iter_next(iter);
+               const char *key = var_expand_parameter_key(par);
+               if (key != NULL)
+                       ERROR_UNSUPPORTED_KEY(key);
+               if (var_expand_parameter_idx(par) > 0)
+                       ERROR_TOO_MANY_UNNAMED_PARAMETERS;
+               if (var_expand_parameter_string_or_var(state, stmt->params, &sep,
+                                                      error_r) < 0)
+                       return -1;
+       }
+
+       ERROR_IF_NO_TRANSFER_TO("generate list from");
+
+       /* split tabescaped */
+       const char *const *values = t_strsplit_tabescaped(str_c(state->transfer));
+       /* join values */
+       var_expand_state_set_transfer(state, t_strarray_join(values, sep));
+
+       return 0;
+}
+
+static int fn_fill(const struct var_expand_statement *stmt, bool left,
+                  struct var_expand_state *state, const char **error_r)
+{
+       size_t amount = 0;
+       const char *filler = "0";
+
+       ERROR_IF_NO_PARAMETERS;
+
+       struct var_expand_parameter_iter_context *ctx =
+               var_expand_parameter_iter_init(stmt);
+       while (var_expand_parameter_iter_more(ctx)) {
+               const struct var_expand_parameter *par =
+                       var_expand_parameter_iter_next(ctx);
+               const char *key = var_expand_parameter_key(par);
+               intmax_t value;
+
+               if (key != NULL)
+                       ERROR_UNSUPPORTED_KEY(key);
+
+               switch (var_expand_parameter_idx(par)) {
+               case 0:
+                       if (var_expand_parameter_number_or_var(state, par,
+                                                              &value, error_r) < 0)
+                               return -1;
+                       else if (value < 1 || value > INT_MAX) {
+                               *error_r = "Fill length out of bounds";
+                               return -1;
+                       }
+                       amount = (size_t)value;
+                       break;
+               case 1:
+                       if (var_expand_parameter_string_or_var(state, par,
+                                                              &filler, error_r) < 0)
+                               return -1;
+                       break;
+               default:
+                       ERROR_TOO_MANY_UNNAMED_PARAMETERS;
+               }
+       }
+
+       if (amount < 1) {
+               *error_r = "Missing amount";
+               return -1;
+       }
+
+       ERROR_IF_NO_TRANSFER_TO("fill");
+
+       /* do nothing if it's already long enough */
+       while (str_len(state->transfer) < (size_t)amount) {
+               if (left)
+                       str_insert(state->transfer, 0, filler);
+               else
+                       str_append(state->transfer, filler);
+       }
+
+       return 0;
+}
+
+static int fn_rfill(const struct var_expand_statement *stmt,
+                   struct var_expand_state *state, const char **error_r)
+{
+       return fn_fill(stmt, FALSE, state, error_r);
+}
+
+static int fn_lfill(const struct var_expand_statement *stmt,
+                   struct var_expand_state *state, const char **error_r)
+{
+       return fn_fill(stmt, TRUE, state, error_r);
+}
+
+static int fn_text(const struct var_expand_statement *stmt,
+                  struct var_expand_state *state, const char **error_r)
+{
+       ERROR_IF_ANY_PARAMETERS;
+       ERROR_IF_NO_TRANSFER_TO("text");
+       string_t *result = t_str_new(state->transfer->used);
+       str_sanitize_append_utf8(result, str_c(state->transfer), SIZE_MAX);
+       var_expand_state_set_transfer(state, str_c(result));
+       return 0;
+}
+
+static const struct var_expand_filter var_expand_builtin_filters[] = {
+       { .name = "lookup", .filter = fn_lookup },
+       { .name = "literal", .filter = fn_literal },
+       { .name = "calculate", .filter = fn_calculate },
+       { .name = "concat", .filter = fn_concat },
+       { .name = "upper", .filter = fn_upper },
+       { .name = "lower", .filter = fn_lower },
+       { .name = "hash", .filter = fn_hash },
+       { .name = "md5", .filter = fn_md5 },
+       { .name = "sha1", .filter = fn_sha1 },
+       { .name = "sha256", .filter = fn_sha256 },
+       { .name = "sha384", .filter = fn_sha384 },
+       { .name = "sha512", .filter = fn_sha512 },
+       { .name = "base64", .filter = fn_base64 } ,
+       { .name = "unbase64", .filter = fn_unbase64 },
+       { .name = "hex", .filter = fn_hex },
+       { .name = "unhex", .filter = fn_unhex },
+       { .name = "hexlify", .filter = fn_hexlify },
+       { .name = "unhexlify", .filter = fn_unhexlify },
+       { .name = "default", .filter = fn_default },
+       { .name = "reverse", .filter = fn_reverse },
+       { .name = "truncate", .filter = fn_truncate },
+       { .name = "substr", .filter = fn_substr },
+       { .name = "ldap_dn", .filter = fn_ldap_dn },
+       { .name = "if", .filter = expansion_filter_if },
+       { .name = "regexp", .filter = fn_regexp },
+       { .name = "lenumber", .filter = fn_le_number },
+       { .name = "benumber", .filter = fn_be_number },
+       { .name = "index", .filter = fn_index },
+       { .name = "username", .filter = fn_username },
+       { .name = "domain", .filter = fn_domain },
+       { .name = "list", .filter = fn_list },
+       { .name = "lfill", .filter = fn_lfill },
+       { .name = "rfill", .filter = fn_rfill },
+       { .name = "text", .filter = fn_text },
+       { .name = NULL }
+};
+
+static void var_expand_free_filters(void)
+{
+       array_free(&dyn_filters);
+}
+
+void var_expand_register_filter(const char *name, var_expand_filter_func_t *const filter)
+{
+       if (!array_is_created(&dyn_filters)) {
+               i_array_init(&dyn_filters, 8);
+               lib_atexit(var_expand_free_filters);
+       }
+       bool is_filter = var_expand_is_filter(name);
+       i_assert(!is_filter);
+
+       struct var_expand_filter f = {
+               .name = name,
+               .filter = filter,
+       };
+       array_push_back(&dyn_filters, &f);
+}
+
+bool var_expand_is_filter(const char *name)
+{
+       var_expand_filter_func_t *fn ATTR_UNUSED;
+       return var_expand_find_filter(name, &fn) == 0;
+}
+
+void var_expand_unregister_filter(const char *name)
+{
+       i_assert(array_is_created(&dyn_filters));
+
+       const struct var_expand_filter *filter;
+       array_foreach(&dyn_filters, filter) {
+               unsigned int i = array_foreach_idx(&dyn_filters, filter);
+               if (strcmp(filter->name, name) == 0) {
+                       array_delete(&dyn_filters, i, 1);
+                       return;
+               }
+       }
+       i_unreached();
+}
+
+int var_expand_find_filter(const char *name, var_expand_filter_func_t **fn_r)
+{
+       for (size_t i = 0; var_expand_builtin_filters[i].name != NULL; i++) {
+               if (strcmp(var_expand_builtin_filters[i].name, name) == 0) {
+                       *fn_r = var_expand_builtin_filters[i].filter;
+                       return 0;
+               }
+       }
+
+       if (array_is_created(&dyn_filters)) {
+               const struct var_expand_filter *filter;
+               /* see if we can find from dyn_filters */
+               array_foreach(&dyn_filters, filter) {
+                       if (strcmp(filter->name, name) == 0) {
+                               *fn_r = filter->filter;
+                               return 0;
+                       }
+               }
+       }
+
+       return -1;
+}
diff --git a/src/lib-var-expand/expansion-parameter.c b/src/lib-var-expand/expansion-parameter.c
new file mode 100644 (file)
index 0000000..1e95e15
--- /dev/null
@@ -0,0 +1,200 @@
+/* Copyright (c) 2024 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "strnum.h"
+#include "var-expand-private.h"
+#include "expansion.h"
+
+struct var_expand_parameter_iter_context {
+       const struct var_expand_parameter *ptr;
+};
+
+const char *var_expand_parameter_key(const struct var_expand_parameter *param)
+{
+       return param->key;
+}
+
+int var_expand_parameter_idx(const struct var_expand_parameter *param)
+{
+       return param->idx;
+}
+
+int var_expand_parameter_number_or_var(const struct var_expand_state *state,
+                                      const struct var_expand_parameter *param,
+                                      intmax_t *value_r, const char **error_r)
+{
+       if (param == NULL) {
+               *error_r = "Missing parameter";
+               return -1;
+       }
+
+       if (param->value_type == VAR_EXPAND_PARAMETER_VALUE_TYPE_VARIABLE) {
+               const char *result;
+               if (var_expand_state_lookup_variable(state, param->value.str,
+                                                    &result, error_r) < 0)
+                       return -1;
+               else if (str_to_intmax(result, value_r) < 0) {
+                       *error_r = t_strdup_printf("'%s' (in %s) is not a number",
+                                                  result, param->value.str);
+                       return -1;
+               }
+       } else if (param->value_type != VAR_EXPAND_PARAMETER_VALUE_TYPE_INT) {
+               *error_r = t_strdup_printf("'%s' is not a number", param->value.str);
+               return -1;
+       } else {
+               *value_r = param->value.num;
+       }
+       return 0;
+}
+
+int var_expand_parameter_bool_or_var(const struct var_expand_state *state,
+                                    const struct var_expand_parameter *param,
+                                    bool *value_r, const char **error_r)
+{
+       intmax_t value;
+       if (var_expand_parameter_number_or_var(state, param, &value, error_r) < 0)
+               return -1;
+       if (value == 0) {
+               *value_r = FALSE;
+       } else if (value == 1) {
+               *value_r = TRUE;
+       } else {
+               *error_r = t_strdup_printf("'%s' is not 0 or 1", param->value.str);
+               return -1;
+       }
+       return 0;
+}
+
+int var_expand_parameter_string_or_var(const struct var_expand_state *state,
+                                      const struct var_expand_parameter *param,
+                                      const char **value_r, const char **error_r)
+{
+       if (param == NULL) {
+               *error_r = "Missing parameter";
+               return -1;
+       }
+       if (param->value_type == VAR_EXPAND_PARAMETER_VALUE_TYPE_VARIABLE) {
+               if (var_expand_state_lookup_variable(state, param->value.str,
+                                                    value_r, error_r) < 0)
+                       return -1;
+       } else if (param->value_type == VAR_EXPAND_PARAMETER_VALUE_TYPE_INT) {
+               *error_r = t_strdup_printf("%jd is not a string",
+                                          param->value.num);
+               return -1;
+       } else {
+               *value_r = param->value.str;
+       }
+       return 0;
+}
+
+int var_expand_parameter_any_or_var(const struct var_expand_state *state,
+                                   const struct var_expand_parameter *param,
+                                   const char **value_r, const char **error_r)
+{
+       if (param == NULL) {
+               *error_r = "Missing parameter";
+               return -1;
+       }
+       if (param->value_type == VAR_EXPAND_PARAMETER_VALUE_TYPE_VARIABLE) {
+               if (var_expand_state_lookup_variable(state, param->value.str,
+                                                    value_r, error_r) < 0)
+                       return -1;
+       } else if (param->value_type == VAR_EXPAND_PARAMETER_VALUE_TYPE_INT) {
+               *value_r = t_strdup_printf("%jd", param->value.num);
+       } else {
+               *value_r = param->value.str;
+       }
+       return 0;
+}
+
+struct var_expand_parameter_iter_context *
+var_expand_parameter_iter_init(const struct var_expand_statement *stmt)
+{
+       struct var_expand_parameter_iter_context *ctx =
+               t_new(struct var_expand_parameter_iter_context, 1);
+       ctx->ptr = stmt->params;
+       return ctx;
+}
+
+bool var_expand_parameter_iter_more(struct var_expand_parameter_iter_context *ctx)
+{
+       return ctx->ptr != NULL;
+}
+
+const struct var_expand_parameter *
+var_expand_parameter_iter_next(struct var_expand_parameter_iter_context *ctx)
+{
+       i_assert(ctx->ptr != NULL);
+       const struct var_expand_parameter *par = ctx->ptr;
+       ctx->ptr = ctx->ptr->next;
+       return par;
+}
+
+void var_expand_parameter_dump(string_t *dest, const struct var_expand_parameter *par)
+{
+       if (par->idx > -1)
+               str_printfa(dest, "\t - %d ", par->idx);
+       else
+               str_printfa(dest, "\t - %s ", par->key);
+       switch (par->value_type) {
+       case VAR_EXPAND_PARAMETER_VALUE_TYPE_STRING:
+               str_printfa(dest, "'%s'", par->value.str);
+               break;
+       case VAR_EXPAND_PARAMETER_VALUE_TYPE_INT:
+               str_printfa(dest, "%ld", par->value.num);
+               break;
+       case VAR_EXPAND_PARAMETER_VALUE_TYPE_VARIABLE:
+               str_append(dest, par->value.str);
+               break;
+       case VAR_EXPAND_PARAMETER_VALUE_TYPE_COUNT:
+               i_unreached();
+       }
+       str_append_c(dest, '\n');
+}
+
+
+int var_expand_parameter_number(const struct var_expand_parameter *param,
+                               bool convert, intmax_t *value_r)
+{
+       if (param->value_type == VAR_EXPAND_PARAMETER_VALUE_TYPE_INT) {
+               *value_r = param->value.num;
+               return 0;
+       } else if (convert && param->value_type == VAR_EXPAND_PARAMETER_VALUE_TYPE_STRING)
+               return str_to_intmax(param->value.str, value_r);
+
+       return -1;
+}
+
+int var_expand_parameter_string(const struct var_expand_parameter *param,
+                               bool convert, const char **value_r)
+{
+       if (param->value_type == VAR_EXPAND_PARAMETER_VALUE_TYPE_STRING) {
+               *value_r = param->value.str;
+               return 0;
+       } else if (convert && param->value_type == VAR_EXPAND_PARAMETER_VALUE_TYPE_INT) {
+               *value_r = t_strdup_printf("%jd", param->value.num);
+               return 0;
+       }
+       return -1;
+}
+
+int
+var_expand_parameter_from_state(struct var_expand_state *state, bool number,
+                               const struct var_expand_parameter **param_r)
+{
+       if (!state->transfer_set)
+               return -1;
+       struct var_expand_parameter *par = t_new(struct var_expand_parameter, 1);
+       par->idx = -1;
+       if (number) {
+               par->value_type = VAR_EXPAND_PARAMETER_VALUE_TYPE_INT;
+               if (str_to_intmax(str_c(state->transfer), &par->value.num) < 0)
+                       return -1;
+       } else {
+               par->value_type = VAR_EXPAND_PARAMETER_VALUE_TYPE_STRING;
+               par->value.str = t_strdup(str_c(state->transfer));
+       }
+       *param_r = par;
+       return 0;
+}
diff --git a/src/lib-var-expand/expansion-program.c b/src/lib-var-expand/expansion-program.c
new file mode 100644 (file)
index 0000000..3f1c36e
--- /dev/null
@@ -0,0 +1,181 @@
+/* Copyright (c) 2024 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "hex-binary.h"
+#include "var-expand-private.h"
+#include "var-expand-parser-private.h"
+#include "var-expand-parser.h"
+#include "expansion.h"
+
+extern void var_expand_parser_lex_init_extra(void*, void*);
+
+static const struct var_expand_params empty_params = {
+};
+
+int var_expand_program_create(const char *str,
+                             struct var_expand_program **program_r,
+                             const char **error_r)
+{
+       int ret;
+       struct var_expand_parser_state state;
+       i_zero(&state);
+       pool_t pool =
+               pool_alloconly_create(MEMPOOL_GROWING"var expand program", 1024);
+       state.p = state.plist = p_new(pool, struct var_expand_program, 1);
+       state.p->pool = pool;
+       p_array_init(&state.variables, pool, 1);
+
+       T_BEGIN {
+               state.str = NULL;
+               state.pool =
+                       pool_alloconly_create(MEMPOOL_GROWING"var expand parser", 32768);
+               p_array_init(&state.variables, pool, 1);
+               state.input = str;
+               state.left = strlen(str);
+               var_expand_parser_lex_init_extra(&state, &state.scanner);
+               /* 0 = OK, everything else = something went wrong */
+               ret = var_expand_parser_parse(&state);
+               state.error = t_strdup(state.error);
+       } T_END_PASS_STR_IF(ret != 0, &state.error);
+
+       array_append_space(&state.variables);
+       state.plist->variables = array_front(&state.variables);
+       i_assert(state.plist->variables != NULL);
+       pool_unref(&state.pool);
+
+       if (ret != 0) {
+               *error_r = state.error;
+               var_expand_program_free(&state.plist);
+       } else {
+               *program_r = state.plist;
+       }
+       i_assert(ret == 0 || *error_r != NULL);
+
+       return ret == 0 ? 0 : -1;
+}
+
+void var_expand_program_dump(const struct var_expand_program *prog, string_t *dest)
+{
+       while (prog != NULL) {
+               struct var_expand_statement *stmt = prog->first;
+               while (stmt != NULL) {
+                       const char *or_var = "";
+                       if (stmt == prog->first && !prog->only_literal)
+                               or_var = " or variable";
+                       str_printfa(dest, "function%s %s\n", or_var, stmt->function);
+                       struct var_expand_parameter_iter_context *ctx =
+                               var_expand_parameter_iter_init(stmt);
+                       while (var_expand_parameter_iter_more(ctx)) {
+                               const struct var_expand_parameter *par =
+                                       var_expand_parameter_iter_next(ctx);
+                               var_expand_parameter_dump(dest, par);
+                       }
+                       stmt = stmt->next;
+               }
+               prog = prog->next;
+       }
+}
+
+int var_expand_program_execute(string_t *dest, const struct var_expand_program *program,
+                              const struct var_expand_params *params, const char **error_r)
+ {
+       int ret = 0;
+       struct var_expand_state state;
+       i_zero(&state);
+
+       if (params == NULL)
+               params = &empty_params;
+
+       i_assert((params->table == NULL && params->tables_arr == NULL) ||
+                (params->table != NULL && params->tables_arr == NULL) ||
+                (params->table == NULL && params->tables_arr != NULL));
+
+       i_assert((params->providers == NULL && params->providers_arr == NULL) ||
+                (params->providers != NULL && params->providers_arr == NULL) ||
+                (params->providers == NULL && params->providers_arr != NULL));
+
+       size_t num_tables = 0;
+       if (params->tables_arr != NULL)
+               while (params->tables_arr[num_tables] != NULL)
+                       num_tables++;
+       size_t num_providers = 0;
+       if (params->providers_arr != NULL)
+               while (params->providers_arr[num_providers] != NULL)
+                    num_providers++;
+       size_t num_contexts = I_MAX(num_tables, num_providers);
+
+       /* ensure contexts are properly terminated. */
+       i_assert(params->contexts == NULL ||
+                params->contexts[num_contexts] == var_expand_contexts_end);
+
+       state.params = params;
+       state.result = str_new(default_pool, 32);
+       state.transfer = str_new(default_pool, 32);
+
+       *error_r = NULL;
+
+       while (program != NULL) {
+               const struct var_expand_statement *stmt = program->first;
+               if (stmt == NULL) {
+                       /* skip empty programs */
+                       program = program->next;
+                       continue;
+               }
+               T_BEGIN {
+                       while (stmt != NULL) {
+                               bool first = stmt == program->first;
+                               if (!var_expand_execute_stmt(&state, stmt,
+                                                            first, error_r)) {
+                                       ret = -1;
+                                       break;
+                               }
+                               stmt = stmt->next;
+                       }
+               } T_END_PASS_STR_IF(ret < 0, error_r);
+               if (ret < 0)
+                       break;
+               if (state.transfer_binary)
+                       var_expand_state_set_transfer(&state, binary_to_hex(state.transfer->data, state.transfer->used));
+               if (state.transfer_set) {
+                       if (!program->only_literal && params->escape_func != NULL) {
+                               str_append(state.result,
+                                          params->escape_func(str_c(state.transfer),
+                                                              params->escape_context));
+                       } else
+                               str_append_str(state.result, state.transfer);
+               } else {
+                       *error_r = t_strdup(state.delayed_error);
+                       ret = -1;
+                       break;
+               }
+               var_expand_state_unset_transfer(&state);
+               program = program->next;
+       };
+       str_free(&state.transfer);
+       i_free(state.delayed_error);
+       /* only write to dest on success */
+       if (ret == 0)
+               str_append_str(dest, state.result);
+       str_free(&state.result);
+       i_assert(ret == 0 || *error_r != NULL);
+
+       return ret;
+}
+
+const char *const *
+var_expand_program_variables(const struct var_expand_program *program)
+{
+       return program->variables;
+}
+
+void var_expand_program_free(struct var_expand_program **_program)
+{
+       struct var_expand_program *program = *_program;
+       if (program == NULL)
+               return;
+       *_program = NULL;
+
+       pool_unref(&program->pool);
+}
diff --git a/src/lib-var-expand/expansion-statement.c b/src/lib-var-expand/expansion-statement.c
new file mode 100644 (file)
index 0000000..9d2f4d9
--- /dev/null
@@ -0,0 +1,65 @@
+/* Copyright (c) 2024 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "var-expand-private.h"
+#include "expansion.h"
+
+bool var_expand_execute_stmt(struct var_expand_state *state,
+                            const struct var_expand_statement *stmt,
+                            bool first, const char **error_r)
+{
+       const char *error;
+       char *delayed_error = NULL;
+       var_expand_filter_func_t *fn;
+
+       /* We allow first function to be either variable or function,
+          so that you can do simple lookups, like %{variable}.
+          Also we prefer variables first, to avoid cumbersome things like
+          having to write lookup('domain') every time you wanted domain.
+       */
+       if (first) {
+               const char *value = NULL;
+               if (var_expand_state_lookup_variable(state, stmt->function,
+                                                    &value, &error) < 0) {
+                       /* ignore this error now, but leave transfer unset. */
+                       /* allows default to pick this up */
+                       var_expand_state_unset_transfer(state);
+                       i_free(delayed_error);
+                       delayed_error = i_strdup(error);
+               } else {
+                       i_assert(value != NULL);
+                       var_expand_state_set_transfer(state, value);
+                       return TRUE;
+               }
+       }
+
+       if (var_expand_find_filter(stmt->function, &fn) == 0) {
+               int ret;
+               T_BEGIN {
+                       ret = (*fn)(stmt, state, &error);
+               } T_END_PASS_STR_IF(ret < 0, &error);
+               /* this is to allow e.g. defaut to work correctly */
+               if (ret < 0) {
+                       var_expand_state_unset_transfer(state);
+                       if (state->delayed_error != NULL) {
+                               *error_r = t_strdup(state->delayed_error);
+                               return FALSE;
+                       }
+                       i_free(delayed_error);
+                       delayed_error =
+                               i_strdup_printf("%s: %s", stmt->function, error);
+               } else {
+                       i_free(delayed_error);
+               }
+               /* this was already handled in the first branch, so just ignore
+                  the error here */
+       } else if (!first) {
+               i_free(delayed_error);
+               *error_r = t_strdup_printf("No such function '%s'", stmt->function);
+               return FALSE;
+       }
+
+       i_free(state->delayed_error);
+       state->delayed_error = delayed_error;
+       return TRUE;
+}
diff --git a/src/lib-var-expand/expansion.h b/src/lib-var-expand/expansion.h
new file mode 100644 (file)
index 0000000..04d6ea3
--- /dev/null
@@ -0,0 +1,37 @@
+#ifndef EXPANSION_H
+#define EXPANSION_H 1
+
+enum var_expand_parameter_value_type {
+       VAR_EXPAND_PARAMETER_VALUE_TYPE_STRING,
+       VAR_EXPAND_PARAMETER_VALUE_TYPE_INT,
+       VAR_EXPAND_PARAMETER_VALUE_TYPE_VARIABLE,
+       VAR_EXPAND_PARAMETER_VALUE_TYPE_COUNT
+};
+
+union var_expand_parameter_value {
+       const char *str;
+       intmax_t num;
+};
+
+struct var_expand_parameter {
+       struct var_expand_parameter *next;
+       const char *key;
+       int idx;
+       enum var_expand_parameter_value_type value_type;
+       union var_expand_parameter_value value;
+};
+
+struct var_expand_filter {
+       const char *name;
+       var_expand_filter_func_t *const filter;
+};
+
+bool var_expand_execute_stmt(struct var_expand_state *state,
+                        const struct var_expand_statement *stmt,
+                        bool first, const char **error_r);
+int var_expand_find_filter(const char *name, var_expand_filter_func_t **fn_r);
+
+int expansion_filter_if(const struct var_expand_statement *stmt, struct var_expand_state *state,
+                       const char **error_r);
+
+#endif
diff --git a/src/lib-var-expand/test-var-expand.c b/src/lib-var-expand/test-var-expand.c
new file mode 100644 (file)
index 0000000..671731f
--- /dev/null
@@ -0,0 +1,948 @@
+/* Copyright (c) 2024 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "test-common.h"
+#include "cpu-count.h"
+#include "str.h"
+#include "hostpid.h"
+#include "var-expand-private.h"
+#include "expansion.h"
+#include "dovecot-version.h"
+#include "time-util.h"
+
+#ifdef HAVE_SYS_UTSNAME_H
+#  include <sys/utsname.h>
+#endif
+
+#include <time.h>
+
+struct var_expand_test {
+       const char *in;
+       const char *out;
+       int ret;
+};
+
+static void run_var_expand_tests(const struct var_expand_params *params,
+                                const struct var_expand_test tests[],
+                                size_t test_count)
+{
+       string_t *dest = str_new(default_pool, 128);
+
+       for (size_t i = 0; i < test_count; i++) {
+               const struct var_expand_test *test = &tests[i];
+               const char *error = NULL;
+
+               str_truncate(dest, 0);
+               int ret = var_expand_new(dest, test->in, params, &error);
+               test_assert_cmp_idx(test->ret, ==, ret, i);
+
+               if (ret < 0) {
+                       test_assert(error != NULL);
+                       test_assert(dest->used == 0);
+                       if (test->ret < 0) {
+                               i_assert(test->out != NULL && *test->out != '\0');
+                               const char *match = strstr(error, test->out);
+                               test_assert(match != NULL);
+                               if (match == NULL) {
+                                       i_debug("error '%s' does not contain '%s'",
+                                               error, test->out);
+                               }
+                       }
+                       if (test->ret != ret) {
+                               i_debug("%s", test->in);
+                               i_error("<%zu> %s", i, error);
+                               continue;
+                       }
+               } else if (ret == 0) {
+                       if (test->ret != ret) {
+                               i_debug("%s", test->in);
+                               i_error("<%zu> Unexpected success", i);
+                               continue;
+                       }
+                       test_assert_strcmp_idx(str_c(dest), test->out, i);
+                       if (strcmp(str_c(dest), test->out) != 0)
+                               i_debug("%s", test->in);
+               }
+       }
+
+       str_free(&dest);
+
+}
+
+static void test_var_expand_new_builtin_filters(void) {
+       test_begin("var_expand(buildin filters)");
+
+       const struct var_expand_table table[] = {
+               { .key = "first", .value = "hello", },
+               { .key = "second", .value = "world", },
+               { .key = "third", .value = "pointer" },
+               { .key = "pointer", .value = "portal" },
+               { .key = "port", .value = "143", },
+               { .key = "three", .value = "3", },
+               { .key = "encoded", .value = "68656c6c6f" },
+               { .key = "domain", .value = "test.dovecot.org" },
+               { .key = "user", .value ="user@test@domain" },
+               { .key = "multivalue", .value = "one\ttwo\tthree" },
+               { .key = "uidvalidity", .value = "1727121943" },
+               { .key = "empty", .value = "" },
+               { .key = "null", .value = NULL },
+               VAR_EXPAND_TABLE_END
+       };
+
+       const struct var_expand_test tests[] = {
+               /* basic lookup */
+               { .in = "%{first}", .out = "hello", .ret = 0 },
+               { .in = "%{lookup('first')}", .out = "hello", .ret = 0 },
+               { .in = "%{literal('hello')}", .out = "hello", .ret = 0 },
+               { .in = "%{lookup}", .out = "lookup: Missing name to lookup", .ret = -1 },
+               { .in = "%{literal}", .out = "literal: Missing parameters", .ret = -1 },
+               /* lookup via variable */
+               { .in = "%{third}", .out = "pointer", .ret = 0 },
+               { .in = "%{lookup(third)}", .out = "portal", .ret = 0 },
+               { .in = "%{lookup(missing)}", .out = "lookup: Unknown variable 'missing'", .ret = -1 },
+               /* default values */
+               { .in = "%{missing | default}", .out = "", .ret = 0 },
+               { .in = "%{missing | default(first)}", .out = "hello", .ret = 0 },
+               { .in = "%{missing | default('hello')}", .out = "hello", .ret = 0 },
+               /* preserves first error */
+               { .in = "%{missing | default(missing)}", .out = "Unknown variable 'missing'", .ret = -1 },
+               { .in = "%{first | default(second)}", .out = "hello", .ret = 0 },
+               { .in = "%{first | default('world')}", .out = "hello", .ret = 0 },
+               { .in = "%{default(first)}", .out = "hello", .ret = 0 },
+               { .in = "%{default(missing)}", .out = "default: Unknown variable 'missing'", .ret = -1 },
+               { .in = "%{empty | default('nonempty')}", .out = "nonempty", .ret = 0 },
+               { .in = "%{null}", .out = "", .ret = 0 },
+               /* fail without default */
+               { .in = "%{missing}", .out = "Unknown variable 'missing'", .ret = -1 },
+               /* casing */
+               { .in = "%{first | upper}", .out = "HELLO", .ret = 0 },
+               { .in = "%{first | upper | lower}", .out = "hello", .ret = 0 },
+               /* substring */
+               { .in = "%{first | substr(0)}", .out = "hello", .ret = 0 },
+               { .in = "%{first | substr(5, 0)}", .out = "", .ret = 0 },
+               { .in = "%{first | substr(1, 2)}", .out = "el", .ret = 0 },
+               { .in = "%{first | substr(-2, 2)}", .out = "lo", .ret = 0 },
+               { .in = "%{first | substr(2, -2)}", .out = "l", .ret = 0 },
+               { .in = "%{first | substr(-1, -1)}", .out = "", .ret = 0 },
+               { .in = "%{first | substr(0, -1)}", .out = "hell", .ret = 0 },
+               { .in = "%{first | substr(-1)}", .out = "o", .ret = 0 },
+               { .in = "%{first | substr(-5)}", .out = "hello", .ret = 0 },
+               { .in = "%{first | substr(6)}", .out = "substr: Offset out of bounds", .ret = -1 },
+               { .in = "%{first | substr(-6)}", .out = "substr: Offset out of bounds", .ret = -1 },
+               { .in = "%{first | substr(1, 5)}", .out = "substr: Length out of bounds", .ret = -1 },
+               { .in = "%{first | substr(1, -5)}", .out = "substr: Length out of bounds", .ret = -1 },
+               { .in = "%{first | substr(-1, 5)}", .out = "substr: Length out of bounds", .ret = -1 },
+               { .in = "%{first | substr(-1, -5)}", .out = "substr: Length out of bounds", .ret = -1 },
+               { .in = "%{first | substr(5, 1)}", .out = "substr: Length out of bounds", .ret = -1 },
+               { .in = "%{first | substr(5, -1)}", .out = "substr: Length out of bounds", .ret = -1 },
+               { .in = "%{first | substr(-5, 1)}", .out = "h", .ret = 0 },
+               { .in = "%{first | substr(-5, -1)}", .out = "hell", .ret = 0 },
+               { .in = "%{first | substr(-6, 1)}", .out = "substr: Offset out of bounds", .ret = -1 },
+               { .in = "%{first | substr(-6, -1)}", .out = "substr: Offset out of bounds", .ret = -1 },
+               { .in = "%{substr}", .out = "substr: Missing parameters", .ret = -1 },
+               { .in = "%{substr(0,0)}", .out = "substr: No value to substring", .ret = -1 },
+               /* reverse */
+               { .in = "%{first | reverse}", .out = "olleh", .ret = 0 },
+               { .in = "%{reverse}", .out = "reverse: No value to reverse", .ret = -1 },
+               /* concatenate */
+               { .in = "%{first | concat(' ',second)}", .out = "hello world", .ret = 0 },
+               { .in = "%{concat(first,' ',second)}", .out = "hello world", .ret = 0 },
+               { .in = "%{concat}", .out = "Missing parameters", .ret = -1 },
+               /* hash */
+               { .in = "%{first | sha1}", .out = "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d", .ret = 0 },
+               { .in = "%{first | sha1(rounds=1000)}", .out = "c0ddea212ee1af8d6401947d29c8bfab31f8ad93", .ret = 0 },
+               { .in = "%{first | sha1(salt='world')}", .out = "5715790a892990382d98858c4aa38d0617151575", .ret = 0 },
+               { .in = "%{first | sha1(rounds=1000,salt='world')}", .out = "a314ec3ef5103223a4c5bd285dbe4ea5534d4334", .ret = 0 },
+               { .in = "%{literal('1724925643') | hex}", .out = "66d046cb" },
+               { .in = "%{hash}", .out = "hash: Missing parameters", .ret = -1 },
+               { .in = "%{hash(rounds=1000)}", .out = "hash: No algorithm as first parameter", .ret = -1 },
+               { .in = "%{hash('md5',fail=1)}", .out = "hash: Unsupported key 'fail'", .ret = -1 },
+               { .in = "%{md5(fail=1)}", .out = "md5: Unsupported key 'fail'", .ret = -1 },
+               /* hexlify */
+               { .in = "%{encoded | unhexlify | text}", .out = "hello", .ret = 0 },
+               { .in = "%{encoded | unhexlify}", .out = "68656c6c6f", .ret = 0 },
+               { .in = "%{three | hexlify(4)}", .out = "0033", .ret = 0 },
+               { .in = "%{hexlify(fail=1)}", .out = "hexlify: Unsupported key 'fail'", .ret = -1 },
+               /* hex */
+               { .in = "%{three | hex | unhex}", .out = "3", .ret = 0 },
+               { .in = "%{uidvalidity | hex | unhex}", .out = "1727121943", .ret = 0 },
+               { .in = "%{three | hex(2)}", .out = "03", .ret = 0 },
+               { .in = "%{uidvalidity | hex(2)}",  .out = "17", .ret = 0 },
+               { .in = "%{uidvalidity | hex(-2)}",  .out = "66", .ret = 0 },
+               { .in = "%{uidvalidity | hex }", .out = "66f1ca17", .ret = 0 },
+               /* base64 */
+               { .in = "%{first | base64}", .out = "aGVsbG8=", .ret = 0 },
+               { .in = "%{first | base64 | unbase64 | text}", .out = "hello", .ret = 0 },
+               { .in = "%{base64(0)}", .out = "base64: Too many positional parameters", .ret = -1 },
+               { .in = "%{base64(fail=1)}", .out = "base64: Unsupported key 'fail'", .ret = -1 },
+               { .in = "%{unbase64(0)}", .out = "unbase64: Too many positional parameters", .ret = -1 },
+               { .in = "%{unbase64(fail=1)}", .out = "unbase64: Unsupported key 'fail'", .ret = -1 },
+               { .in = "%{first | base64(pad=0)}", .out = "aGVsbG8", .ret = 0 },
+               /* weird syntax to avoid trigraph ignored */
+               { .in = "%{literal('<<?""?""?>>') | base64(url=1)}", .out = "PDw_Pz8-Pg==", .ret = 0 },
+               { .in = "%{literal('<<?""?""?>>') | base64(pad=0,url=1)}", .out = "PDw_Pz8-Pg", .ret = 0 },
+               /* truncate */
+               { .in = "%{first | truncate(3)}", .out = "hel", .ret = 0 },
+               { .in = "%{first | truncate(three)}", .out = "hel", .ret = 0 },
+               { .in = "%{first | truncate(bits=7)}", .out = "4", .ret = 0 },
+               { .in = "%{truncate}", .out = "truncate: Missing parameter", .ret = -1 },
+               { .in = "%{truncate('hello')}", .out = "truncate: 'hello' is not a number", .ret = -1 },
+               { .in = "%{truncate(first)}", .out = "truncate: 'hello' (in first) is not a number", .ret = -1 },
+               { .in = "%{truncate(3)}", .out = "truncate: No value to truncate", .ret = -1 },
+               /* ldap dn */
+               { .in = "cn=%{first},ou=%{domain | ldap_dn}", .out = "cn=hello,ou=test,dc=dovecot,dc=org", .ret = 0 },
+               /* regexp */
+               { .in = "%{literal('hello world') | regexp('(.*) (.*)', '\\\\2 \\\\1')}", .out = "world hello" },
+               /* index */
+               { .in = "%{user | index('@',0)}", .out = "user", .ret = 0 },
+               { .in = "%{user | username}", .out = "user", .ret = 0 },
+               { .in = "%{user | domain}", .out = "test@domain", .ret = 0 },
+               { .in = "%{user | domain | domain}", .out = "domain", .ret = 0 },
+               { .in = "%{user | index('@',1)}", .out = "test", .ret = 0 },
+               { .in = "%{user | index('@',2)}", .out = "domain", .ret = 0 },
+               { .in = "%{user | index('@',3)}", .out = "index: Position out of bounds", .ret = -1 },
+               { .in = "%{user | index('@',-4)}", .out = "index: Position out of bounds", .ret = -1 },
+               { .in = "%{user | index('@',-5)}", .out = "index: Position out of bounds", .ret = -1 },
+               { .in = "%{user | index('@',-4) | default('hello')}", .out = "hello", .ret = 0 },
+               { .in = "%{user | index('@',-3)}", .out = "user", .ret = 0 },
+               { .in = "%{user | index('@',-2)}", .out = "test", .ret = 0 },
+               { .in = "%{user | index('@',-1)}", .out = "domain", .ret = 0 },
+               { .in = "%{user | username(0)}", .out = "username: Too many positional parameters", .ret = -1 },
+               { .in = "%{user | domain(0)}", .out = "domain: Too many positional parameters", .ret = -1 },
+               { .in = "%{literal('hello@') | domain }", .out = "", .ret = 0 },
+               { .in = "%{literal('@hello') | username }", .out = "", .ret = 0 },
+               { .in = "%{literal('@') | domain }", .out = "", .ret = 0 },
+               { .in = "%{literal('@') | username }", .out = "", .ret = 0 },
+               { .in = "%{literal('') | username }", .out = "", .ret = 0 },
+               { .in = "%{literal('') | domain }", .out = "", .ret = 0 },
+               { .in = "%{literal('username') | username }", .out = "username", .ret = 0 },
+               { .in = "%{literal('username') | domain }", .out = "", .ret = 0 },
+               /* list */
+               { .in = "%{multivalue | list(',')}", .out = "one,two,three", .ret = 0 },
+               /* fill */
+               { .in = "%{literal('1') | rfill(3)}", .out = "100", .ret = 0 },
+               { .in = "%{literal('1') | lfill(3)}", .out = "001", .ret = 0 },
+               { .in = "%{literal('1') | lfill(3, ' ')}", .out = "  1", .ret = 0 },
+               /* %8Mu */
+               { .in = "%{first | md5 | hexlify(8)}", .out = "5d41402a", .ret = 0 },
+               /* %N */
+               { .in = "%{first | md5 | substr(4,4) }", .out = "bc4b2a76", .ret = 0 },
+               /* %2.256N */
+               { .in = "%{first | md5 | substr(0,8) % 256 | hex}", .out = "76", .ret = 0 },
+               { .in = "%{first | md5 | substr(0,8) % 30 | hex }", .out = "c", .ret = 0 },
+               /* Modulo special case */
+               { .in = "%{first | md5 % 256 | hex}", .out = "92", .ret = 0 },
+               { .in = "%{first | sha512 % 256 | hex }", .out = "43", .ret = 0 },
+               /* %N30 */
+               { .in = "%{first | md5 % 30 | hex }", .out = "16", .ret = 0 },
+               { .in = "%{first | md5 % 30 | hex(2) }", .out = "16", .ret = 0 },
+               { .in = "%{first | md5 % 30 | hex(4) }", .out = "0016", .ret = 0 },
+               { .in = "%{first | md5 % 30 | hex(-4) }", .out = "1600", .ret = 0 },
+       };
+
+       const struct var_expand_params params = {
+               .table = table,
+       };
+
+       run_var_expand_tests(&params, tests, N_ELEMENTS(tests));
+
+       test_end();
+}
+
+static void test_var_expand_new_math(void) {
+       test_begin("var_expand(math)");
+
+       const struct var_expand_table table[] = {
+               { .key = "first", .value = "1", },
+               { .key = "second", .value = "2", },
+               { .key = "third", .value = "pointer" },
+               { .key = "pointer", .value = "4" },
+               { .key = "port", .value = "143", },
+               { .key = "nan", .value = "nanana" },
+               VAR_EXPAND_TABLE_END
+       };
+
+       const struct var_expand_test tests[] = {
+               { .in = "%{literal('1') + 1}", .out = "2", .ret = 0 },
+               { .in = "%{first + 1}", .out = "2", .ret = 0 },
+               { .in = "%{first * 10}", .out = "10", .ret = 0 },
+               { .in = "%{lookup(third) / 2}", .out = "2", .ret = 0 },
+               { .in = "%{nan - 1}", .out = "Input is not a number", .ret = -1 },
+               { .in = "%{literal('31') | unhexlify | text * 5}", .out = "5", .ret = 0 },
+               { .in = "%{literal('31') | unhex * 5}", .out = "245", .ret = 0 },
+               { .in = "%{port % 5}", .out = "3", .ret = 0 },
+               { .in = "%{port / 0}", .out = "calculate: Division by zero", .ret = -1 },
+               { .in = "%{port % 0}", .out = "calculate: Modulo by zero", .ret = -1 },
+       };
+
+       const struct var_expand_params params = {
+               .table = table,
+       };
+
+       run_var_expand_tests(&params, tests, N_ELEMENTS(tests));
+
+       test_end();
+}
+
+static void test_var_expand_new_if(void)
+{
+       test_begin("var_expand(if)");
+
+       const struct var_expand_table table[] = {
+               { .key = "alpha", .value = "alpha" },
+               { .key = "beta", .value = "beta" },
+               { .key = "one", .value = "1" },
+               { .key = "two", .value = "2" },
+               { .key = "evil1", .value = ";', ':" },
+               { .key = "evil2", .value = ";test;" },
+               VAR_EXPAND_TABLE_END
+       };
+
+       const struct var_expand_test tests[] = {
+               /* basic numeric operand test */
+               { .in = "%{if(1, '==', 1, 'yes', 'no')}", .out = "yes", .ret = 0 },
+               { .in = "%{literal('1') | if('==', 2, 'yes', 'no')}", .out = "no", .ret = 0 },
+               { .in = "%{literal('1') | if('<', 1, 'yes', 'no')}", .out = "no", .ret = 0 },
+               { .in = "%{literal('1') | if('<', 2, 'yes', 'no')}", .out = "yes", .ret = 0 },
+               { .in = "%{literal('1') | if('<=', 1, 'yes', 'no')}", .out = "yes", .ret = 0 },
+               { .in = "%{literal('1') | if('<=', 2, 'yes', 'no')}", .out = "yes", .ret = 0 },
+               { .in = "%{literal('1') | if('>', 1, 'yes', 'no')}", .out = "no", .ret = 0 },
+               { .in = "%{literal('1') | if('>', 2, 'yes', 'no')}", .out = "no", .ret = 0 },
+               { .in = "%{literal('1') | if('>=', 1, 'yes', 'no')}", .out = "yes", .ret = 0 },
+               { .in = "%{literal('1') | if('>=', 2, 'yes', 'no')}", .out = "no", .ret = 0 },
+               { .in = "%{literal('1') | if('!=', 1, 'yes', 'no')}", .out = "no", .ret = 0 },
+               { .in = "%{literal('1') | if('!=', 2, 'yes', 'no')}", .out = "yes", .ret = 0 },
+               /* basic string operand test */
+               { .in = "%{if('a', 'eq', 'a', 'yes', 'no')}", .out = "yes", .ret = 0 },
+               { .in = "%{literal('a') | if('eq', 'b', 'yes', 'no')}", .out = "no", .ret = 0 },
+               { .in = "%{literal('a') | if('lt', 'a', 'yes', 'no')}", .out = "no", .ret = 0 },
+               { .in = "%{literal('a') | if('lt', 'b', 'yes', 'no')}", .out = "yes", .ret = 0 },
+               { .in = "%{literal('a') | if('le', 'a', 'yes', 'no')}", .out = "yes", .ret = 0 },
+               { .in = "%{literal('a') | if('le', 'b', 'yes', 'no')}", .out = "yes", .ret = 0 },
+               { .in = "%{literal('a') | if('gt', 'a', 'yes', 'no')}", .out = "no", .ret = 0 },
+               { .in = "%{literal('a') | if('gt', 'b', 'yes', 'no')}", .out = "no", .ret = 0 },
+               { .in = "%{literal('a') | if('ge', 'a', 'yes', 'no')}", .out = "yes", .ret = 0 },
+               { .in = "%{literal('a') | if('ge', 'b', 'yes', 'no')}", .out = "no", .ret = 0 },
+               { .in = "%{literal('a') | if('ne', 'a', 'yes', 'no')}", .out = "no", .ret = 0 },
+               { .in = "%{literal('a') | if('ne', 'b', 'yes', 'no')}", .out = "yes", .ret = 0 },
+               { .in = "%{literal('a') | if('*', 'a', 'yes', 'no')}", .out = "yes", .ret = 0 },
+               { .in = "%{literal('a') | if('*', 'b', 'yes', 'no')}", .out = "no", .ret = 0 },
+               { .in = "%{literal('a') | if('*', '*a*', 'yes', 'no')}", .out = "yes", .ret = 0 },
+               { .in = "%{literal('a') | if('*', '*b*', 'yes', 'no')}", .out = "no", .ret = 0 },
+               { .in = "%{literal('a') | if('*', '*', 'yes', 'no')}", .out = "yes", .ret = 0 },
+               { .in = "%{literal('a') | if('!*', 'a', 'yes', 'no')}", .out = "no", .ret = 0 },
+               { .in = "%{literal('a') | if('!*', 'b', 'yes', 'no')}", .out = "yes", .ret = 0 },
+               { .in = "%{literal('a') | if('!*', '*a*', 'yes', 'no')}", .out = "no", .ret = 0 },
+               { .in = "%{literal('a') | if('!*', '*b*', 'yes', 'no')}", .out = "yes", .ret = 0 },
+               { .in = "%{literal('a') | if('!*', '*', 'yes', 'no')}", .out = "no", .ret = 0 },
+               { .in = "%{literal('a') | if('~', 'a', 'yes', 'no')}", .out = "yes", .ret = 0 },
+               { .in = "%{literal('a') | if('~', 'b', 'yes', 'no')}", .out = "no", .ret = 0 },
+               { .in = "%{literal('a') | if('~', '.*a.*', 'yes', 'no')}", .out = "yes", .ret = 0 },
+               { .in = "%{literal('a') | if('~', '.*b.*', 'yes', 'no')}", .out = "no", .ret = 0 },
+               { .in = "%{literal('a') | if('~', '.*', 'yes', 'no')}", .out = "yes", .ret = 0 },
+               { .in = "%{literal('a') | if('!~', 'a', 'yes', 'no')}", .out = "no", .ret = 0 },
+               { .in = "%{literal('a') | if('!~', 'b', 'yes', 'no')}", .out = "yes", .ret = 0 },
+               { .in = "%{literal('a') | if('!~', '.*a.*', 'yes', 'no')}", .out = "no", .ret = 0 },
+               { .in = "%{literal('a') | if('!~', '.*b.*', 'yes', 'no')}", .out = "yes", .ret = 0 },
+               { .in = "%{literal('a') | if('!~', '.*', 'yes', 'no')}", .out = "no", .ret = 0 },
+               { .in = "%{literal('this is test') | if('~', '^test', 'yes', 'no')}", .out = "no", .ret = 0 },
+               { .in = "%{literal('this is test') | if('~', '.*test', 'yes', 'no')}", .out = "yes", .ret = 0 },
+               /* variable expansion */
+               { .in = "%{alpha | if('eq', alpha, 'yes', 'no')}", .out = "yes", .ret = 0 },
+               { .in = "%{alpha | if('eq', beta, 'yes', 'no')}", .out = "no", .ret = 0 },
+               { .in = "%{one | if('eq', one, 'yes', 'no')}", .out = "yes", .ret = 0 },
+               { .in = "%{one | if('eq', two, 'yes', 'no')}", .out = "no", .ret = 0 },
+               { .in = "%{one | if('eq', one, one, two)}", .out = "1", .ret = 0 },
+               { .in = "%{one | if('gt', two, one, two)}", .out = "2", .ret = 0 },
+               { .in = "%{evil1 | if('eq', ';\\', \\':', evil2, 'no')}", .out = ";test;", .ret = 0 },
+               /* FIXME: add inner if support? */
+/*             { "%{if;%{if;%{one};eq;1;1;0};eq;%{if;%{two};eq;2;2;3};yes;no}", "no", 1 }, */
+               /* Errors */
+               { .in = "%{if('gt', two, one, two)}", .out = "if: Missing parameters", .ret = -1 },
+               { .in = "%{if(1, '', 1, 'yes', 'no')}", .out = "if: Unsupported comparator ''", .ret = -1 },
+               { .in = "%{if(missing, '==', 1, 'yes', 'no')}", .out = "if: Left-hand side: Unknown variable 'missing'", .ret = -1 },
+               { .in = "%{if(1, missing, 1, 'yes', 'no')}", .out = "if: Comparator: Unknown variable 'missing'", .ret = -1 },
+               { .in = "%{if(1, '==', missing, 'yes', 'no')}", .out = "if: Right-hand side: Unknown variable 'missing'", .ret = -1 },
+               { .in = "%{if(1, '==', 1, missing, 'no')}", .out = "if: True branch: Unknown variable 'missing'", .ret = -1 },
+               { .in = "%{if(1, '==', 0, 'yes', missing)}", .out = "if: False branch: Unknown variable 'missing'", .ret = -1 },
+               { .in = "%{if(1, '==', 1, 'yes', 'no', 'maybe')}", .out = "if: Too many positional parameters", .ret = -1 },
+               { .in = "%{if(fail=1)}", .out = "if: Unsupported key 'fail'", .ret = -1 },
+               { .in = "%{alpha|if('==', two, one, two)}", .out = "if: Input is not a number", .ret = -1 },
+       };
+
+       const struct var_expand_params params = {
+               .table = table,
+       };
+
+       run_var_expand_tests(&params, tests, N_ELEMENTS(tests));
+
+       test_end();
+}
+
+static int test_custom_provider(const char *key, const char **value_r, void *context,
+                               const char **error_r)
+{
+       if (strcmp(key, "value") == 0)
+               *value_r = context;
+       else {
+               test_assert_strcmp(key, "null");
+               ERROR_UNSUPPORTED_KEY(key);
+       }
+       return 0;
+}
+
+static void test_var_expand_new_providers(void) {
+       test_begin("var_expand(providers)");
+       int ncpus;
+       const char *error ATTR_UNUSED;
+       const char *user = getenv("USER");
+       struct timeval tv;
+       struct tm tm;
+       i_gettimeofday(&tv);
+       if (localtime_r(&tv.tv_sec, &tm) == NULL)
+               i_panic("localtime_r() failed: %m");
+
+       int ret = cpu_count_get(&ncpus, &error);
+       if (user == NULL)
+               user = "";
+       const struct var_expand_test tests[] = {
+               { .in = "%{process:pid}", .out = my_pid, },
+               { .in = "%{system:cpu_count}", .out = dec2str(ncpus), .ret = ret },
+               { .in = "%{dovecot:name}", .out = PACKAGE_NAME, .ret = 0 },
+               { .in = "%{dovecot:version}", .out = PACKAGE_VERSION, .ret = 0 },
+               { .in = "%{dovecot:support-url}", .out = PACKAGE_WEBPAGE, .ret = 0 },
+               { .in = "%{dovecot:support-email}", .out = PACKAGE_BUGREPORT, .ret = 0 },
+               { .in = "%{dovecot:revision}", .out = DOVECOT_REVISION, .ret = 0 },
+               { .in = "%{env:USER}", .out = user, .ret = 0 },
+               { .in = "%{env:missing}", .out = "", .ret = 0 },
+               { .in = "%{dovecot:invalid}", .out = "Unsupported dovecot key 'invalid'", .ret = -1 },
+               { .in = "%{invalid:whatever}", .out = "Unsupported prefix 'invalid'", .ret = -1 },
+               { .in = "%{custom:value}", .out = "test", .ret = 0 },
+               { .in = "%{custom:null}", .out = "Unsupported key 'null'", .ret = -1 },
+               { .in = "%{event:string}", .out = "event", .ret = 0 },
+               { .in = "%{event:missing}", .out = "No such field 'missing' in event", .ret = -1 },
+               { .in = "%{event:missing|default}", .out = "", .ret = 0 },
+               { .in = "%{event:magic}", .out = "42", .ret = 0 },
+       };
+
+       struct event *event = event_create(NULL);
+       event_add_str(event, "string", "event");
+       event_add_int(event, "magic", 42);
+
+       const struct var_expand_params params = {
+               .event = event,
+               .providers = (const struct var_expand_provider[]) {
+                       { .key = "custom", .func = test_custom_provider, },
+                       VAR_EXPAND_TABLE_END
+               },
+               .context = "test",
+       };
+
+       run_var_expand_tests(&params, tests, N_ELEMENTS(tests));
+
+       /* Test time expansion separately */
+       const char *datetimetpl = "%{date:year}%{date:month}%{date:day}T"
+                                 "%{time:hour}%{time:min}%{time:sec}";
+       const char *datetime;
+       time_t t0 = time(NULL);
+       ret = t_var_expand(datetimetpl, &params, &datetime, &error);
+       test_out_reason_quiet("t_var_expand()", ret == 0, error);
+
+       if (ret == 0) {
+               /* try to parse result */
+               struct tm tm;
+               i_zero(&tm);
+               if (strptime(datetime, "%Y%m%dT%H%M%S", &tm) == NULL) {
+                       test_failed(t_strdup_printf("strptime() failed: %m"));
+               } else {
+                       /* Ensure the time is within 10 seconds */
+                       time_t t1 = mktime(&tm);
+                       test_assert_cmp(labs(t0 - t1), <, 10);
+               }
+       }
+
+       /* Check the expansion of os/os-version depending on whether uname()
+           succeeds. */
+
+       struct utsname utsname_result;
+       if (uname(&utsname_result) == 0) {
+               string_t *dest = t_str_new(32);
+               str_truncate(dest, 0);
+               test_assert(var_expand_new(dest, "%{system:os}", &params, &error) == 0);
+               test_assert_strcmp(utsname_result.sysname, str_c(dest));
+
+               str_truncate(dest, 0);
+               test_assert(var_expand_new(dest, "%{system:os-version}", &params, &error) == 0);
+               test_assert_strcmp(utsname_result.release, str_c(dest));
+       }
+
+       event_push_global(event);
+
+       /* test with global event */
+       const struct var_expand_test tests_global_event[] = {
+               { .in = "%{event:string}", .out = "event", .ret = 0 },
+               { .in = "%{event:missing}", .out = "No such field 'missing' in event", .ret = -1 },
+               { .in = "%{event:missing|default}", .out = "", .ret = 0 },
+               { .in = "%{event:magic}", .out = "42", .ret = 0 },
+       };
+
+       run_var_expand_tests(NULL, tests_global_event,
+                            N_ELEMENTS(tests_global_event));
+
+       event_pop_global(event);
+
+       event_unref(&event);
+
+       /* test without event */
+       const struct var_expand_test tests_no_event[] = {
+               { .in = "%{event:anything}", .out = "No event available", .ret = -1 },
+               { .in = "%{event:anything|default}", .out = "", .ret = 0 },
+       };
+
+       run_var_expand_tests(NULL, tests_no_event, N_ELEMENTS(tests_no_event));
+
+       test_end();
+}
+
+static void test_var_expand_new_provider_arr(void)
+{
+       test_begin("var_expand(provider arr)");
+       const struct var_expand_test tests[] = {
+               { .in = "%{custom:value}", .out = "context1", .ret = 0 },
+               { .in = "%{custom2:value}", .out = "context2", .ret = 0 },
+       };
+
+       const struct var_expand_provider prov1[] = {
+               { .key = "custom", .func = test_custom_provider, },
+               VAR_EXPAND_TABLE_END
+       };
+
+       const struct var_expand_provider prov2[] = {
+               { .key = "custom2", .func = test_custom_provider, },
+               VAR_EXPAND_TABLE_END
+       };
+
+       const struct var_expand_params params = {
+               .providers_arr = (const struct var_expand_provider*[]) {
+                       prov1,
+                       prov2,
+                       NULL,
+               },
+               .contexts = (void *const[]) {
+                       "context1",
+                       "context2",
+                       VAR_EXPAND_CONTEXTS_END
+               },
+       };
+
+       run_var_expand_tests(&params, tests, N_ELEMENTS(tests));
+       test_end();
+}
+
+static void test_var_expand_new_tables_arr(void)
+{
+       test_begin("var_expand(tables_arr)");
+
+       const struct var_expand_table table1[] = {
+               { .key = "name", .value = "first" },
+               VAR_EXPAND_TABLE_END
+       };
+
+       const struct var_expand_table table2[] = {
+               { .key = "age", .value = "20" },
+               VAR_EXPAND_TABLE_END
+       };
+
+       const struct var_expand_table *const tables[] = {
+               table1,
+               table2,
+               NULL
+       };
+
+       const struct var_expand_params params = {
+               .tables_arr = tables,
+       };
+
+       string_t *dest = t_str_new(32);
+       const char *error;
+       int ret = var_expand_new(dest, "I am %{name} and %{age} years old",
+                                &params, &error);
+
+       test_assert(ret == 0);
+       test_assert_strcmp(str_c(dest), "I am first and 20 years old");
+
+       test_end();
+}
+
+static const char *test_escape(const char *str, void *context)
+{
+       const char *escape_chars = context;
+       string_t *dest = t_str_new(strlen(str) + 2);
+       str_append_c(dest, '\'');
+       if (strpbrk(str, escape_chars) == NULL) {
+               str_append(dest, str);
+       } else {
+               for (const char *ptr = str; *ptr != '\0'; ptr++) {
+                       if (strchr(escape_chars, *ptr) != NULL)
+                               str_append_c(dest, '\\');
+                       str_append_c(dest, *ptr);
+               }
+       }
+       str_append_c(dest, '\'');
+       return str_c(dest);
+}
+
+static void test_var_expand_new_escape(void)
+{
+       const struct var_expand_table table[] = {
+               { .key = "clean", .value = "hello world", },
+               { .key = "escape", .value = "'hello' \"world\"", },
+               { .key = "first", .value = "bobby" },
+               { .key = "nasty", .value = "\';-- SELECT * FROM bobby.tables" },
+               VAR_EXPAND_TABLE_END
+       };
+
+       const struct var_expand_test tests[] = {
+               { .in = "%{clean}", .out = "'hello world'", .ret = 0, },
+               { .in = "%{escape}", .out = "'\\'hello\\' \"world\"'", .ret = 0 },
+               {
+                       .in = "SELECT * FROM bobby.tables WHERE name = %{first}",
+                       .out = "SELECT * FROM bobby.tables WHERE name = 'bobby'",
+                       .ret = 0,
+               },
+               {
+                       .in = "SELECT * FROM bobby.tables WHERE name = %{nasty}",
+                       .out = "SELECT * FROM bobby.tables WHERE name = "
+                              "'\\';-- SELECT * FROM bobby.tables'",
+                       .ret = 0,
+               },
+               { .in = "no variables", .out = "no variables", .ret = 0 },
+               { .in = "%{literal('hello')}", .out = "'hello'", .ret = 0 },
+               { .in = "hello\\tworld", .out = "hello\\tworld", .ret = 0 },
+               { .in = "%{literal('hello\r\n\tworld')}", .out = "'hello\r\n\tworld'", .ret = 0 },
+               /* Hello */
+               { .in = "\\110\\145\\154\\154\\157", .out = "\\110\\145\\154\\154\\157", .ret = 0},
+               { .in = "%{literal('\\110\\145\\154\\154\\157')}", .out = "'\110\145\154\154\157'", .ret = 0 },
+               /* Hex / oct escapes */
+               { .in = "\\x20\\x21", .out = "\\x20\\x21", .ret = 0 },
+               { .in = "%{literal('\\x20\\x21')}", .out = "' !'", .ret = 0 },
+               { .in = "\\\\x20", .out = "\\\\x20", .ret = 0 },
+               { .in = "%{literal('\\\\x20')}", .out = "'\\x20'", .ret = 0 },
+               /* Bad hex / oct */
+               { .in = "\\xgg", .out = "\\xgg", .ret = 0 },
+               { .in = "%{literal('\\xgg')}", .out = "syntax error, unexpected end of file, expecting MINUS or NAME or VALUE or NUMBER", .ret = -1 },
+               { .in = "\\999", .out = "\\999", .ret = 0 },
+               { .in = "%{literal('\\999')}", .out = "syntax error, unexpected end of file, expecting MINUS or NAME or VALUE or NUMBER", .ret = -1 },
+               /* List test */
+               { .in = "%{literal('one\ttwo\tthree') | list}", .out="'one,two,three'", .ret = 0 },
+               /* Escape escape */
+               { .in = "\\hello\\world", .out = "\\hello\\world", .ret = 0 },
+               { .in = "%{literal('\\'\\\\hello\\\\world\\'')}", .out = "'\\'\\hello\\world\\''", .ret = 0 },
+               { .in = "%{literal(\"\\\"\\\\hello\\\\world\\\"\")}", .out = "'\"\\hello\\world\"'", .ret = 0 },
+       };
+
+       const struct var_expand_params params = {
+               .table = table,
+               .escape_func = test_escape,
+               .escape_context = "'",
+       };
+
+       test_begin("var_expand_new(escape)");
+
+       run_var_expand_tests(&params, tests, N_ELEMENTS(tests));
+
+       test_end();
+}
+
+static int test_value(const char *key, const char **value_r, void *context,
+                     const char **error_r)
+{
+       const char *ctx = context;
+       test_assert_strcmp(ctx, "test");
+
+       if (strcmp(key, "third") == 0) {
+               *error_r = "expected failure";
+               return -1;
+       }
+       if (strcmp(key, "fourth") == 0) {
+               *value_r = context;
+               return 0;
+       }
+       test_assert_strcmp(key, "second");
+       *value_r = "world";
+       return 0;
+}
+
+static int test_value2(const char *key, const char **value_r, void *context,
+                      const char **error_r)
+{
+       const char *ctx = context;
+       test_assert_strcmp(ctx, "test2");
+
+       if (strcmp(key, "third") == 0) {
+               *error_r = "expected failure";
+               return -1;
+       }
+       if (strcmp(key, "fourth") == 0) {
+               *value_r = context;
+               return 0;
+       }
+       test_assert_strcmp(key, "second");
+       *value_r = "world";
+       return 0;
+}
+
+static void test_var_expand_new_value_func(void)
+{
+       const struct var_expand_table table[] = {
+               { .key = "first", .value = "hello", },
+               { .key = "second", .func = test_value, },
+               { .key = "third", .func = test_value, },
+               { .key = "fourth", .func = test_value, },
+               VAR_EXPAND_TABLE_END
+       };
+
+       const struct var_expand_test tests[] = {
+               { .in = "%{first} %{second}", .out = "hello world", .ret = 0, },
+               { .in = "%{first} %{third}", .out = "expected failure", .ret = -1 },
+               { .in = "%{first} %{fourth}", .out = "hello test", .ret = 0, },
+       };
+
+       const struct var_expand_params params = {
+               .table = table,
+               .context = "test",
+       };
+
+       test_begin("var_expand_new(value func)");
+
+       run_var_expand_tests(&params, tests, N_ELEMENTS(tests));
+
+       test_end();
+}
+
+static void test_var_expand_new_value_func_arr(void)
+{
+       const struct var_expand_table table[] = {
+               { .key = "first", .value = "hello", },
+               { .key = "second", .func = test_value, },
+               VAR_EXPAND_TABLE_END
+       };
+
+       const struct var_expand_table table2[] = {
+               { .key = "third", .func = test_value2, },
+               { .key = "fourth", .func = test_value2, },
+               VAR_EXPAND_TABLE_END
+       };
+
+       const struct var_expand_test tests[] = {
+               { .in = "%{first} %{second}", .out = "hello world", .ret = 0, },
+               { .in = "%{first} %{third}", .out = "expected failure", .ret = -1 },
+               { .in = "%{first} %{fourth}", .out = "hello test2", .ret = 0, },
+       };
+
+       const struct var_expand_params params = {
+               .table = NULL,
+               .tables_arr = (const struct var_expand_table*[]) {
+                       table,
+                       table2,
+                       NULL
+               },
+               .contexts = (void *const[]) {
+                       "test",
+                       "test2",
+                       VAR_EXPAND_CONTEXTS_END
+               },
+       };
+
+       test_begin("var_expand(value func_arr)");
+
+       run_var_expand_tests(&params, tests, N_ELEMENTS(tests));
+
+       test_end();
+}
+
+static void test_var_expand_merge_tables(void)
+{
+       const struct var_expand_table one[] = {
+               { .key = "alpha", .value = "1" },
+               { .key = "beta", .value = "2" },
+               VAR_EXPAND_TABLE_END
+       },
+       two[] = {
+               { .key = "theta", .value = "3" },
+               { .key = "phi", .value = "4" },
+               VAR_EXPAND_TABLE_END
+       },
+       *merged = NULL;
+
+       test_begin("var_expand_merge_tables");
+
+       merged = var_expand_merge_tables_new(pool_datastack_create(), one, two);
+
+       test_assert(var_expand_table_size_new(merged) == 4);
+       for (unsigned int i = 0; i < var_expand_table_size_new(merged); i++) {
+               if (i < 2) {
+                       test_assert_idx(merged[i].value == one[i].value || strcmp(merged[i].value, one[i].value) == 0, i);
+                       test_assert_idx(merged[i].key == one[i].key || strcmp(merged[i].key, one[i].key) == 0, i);
+               } else if (i < 4) {
+                       test_assert_idx(merged[i].value == two[i-2].value || strcmp(merged[i].value, two[i-2].value) == 0, i);
+                       test_assert_idx(merged[i].key == two[i-2].key || strcmp(merged[i].key, two[i-2].key) == 0, i);
+               } else {
+                       break;
+               }
+       }
+       test_end();
+}
+
+static void test_var_expand_new_variables(void)
+{
+       test_begin("var_expand(variables)");
+
+       /* build a program */
+       struct var_expand_program *prog;
+       const char *error;
+       int ret = var_expand_program_create("%{foo} %{bar} %{baz} %{first} "
+                                           "%{env:foo} %{provider:value}",
+                                           &prog, &error);
+       test_assert(ret == 0);
+       if (ret != 0)
+               i_error("%s", error);
+
+       const char *const *variables = var_expand_program_variables(prog);
+       test_assert_strcmp_idx(variables[0], "bar", 0);
+       test_assert_strcmp_idx(variables[1], "baz", 1);
+       test_assert_strcmp_idx(variables[2], "env:foo", 2);
+       test_assert_strcmp_idx(variables[3], "first", 3);
+       test_assert_strcmp_idx(variables[4], "foo", 4);
+       test_assert_strcmp_idx(variables[5], "provider:value", 5);
+       test_assert_idx(variables[6] == NULL, 6);
+
+       var_expand_program_free(&prog);
+
+       test_end();
+}
+
+/* test that keys are in correct order */
+static int test_filter(const struct var_expand_statement *stmt,
+                      struct var_expand_state *state,
+                      const char **error_r ATTR_UNUSED)
+{
+       const struct var_expand_parameter *par = stmt->params;
+       int previdx = -1;
+       const char *prevkey = NULL;
+       bool allow_idx = TRUE;
+
+       for (; par != NULL; par = par->next) {
+               test_assert(par->idx == -1 || (allow_idx && par->idx > previdx));
+               test_assert(par->idx != -1 || prevkey == NULL ||
+                           strcmp(par->key, prevkey) > 0);
+               if (par->idx == -1) {
+                       allow_idx = FALSE;
+                       prevkey = par->key;
+               } else
+                       previdx = par->idx;
+       }
+
+       var_expand_state_set_transfer(state, "done");
+
+       return 0;
+}
+
+
+static void test_var_expand_new_parameter_sorted(void)
+{
+       const struct var_expand_test tests[] = {
+               { .in = "%{test_filter}", .out ="done", .ret = 0 },
+               { .in = "%{test_filter(1, 2, 3)}", .out ="done", .ret = 0 },
+               { .in = "%{test_filter('1', '2', '3')}", .out ="done", .ret = 0 },
+               { .in = "%{test_filter(a='1', b='2', c='3')}", .out ="done", .ret = 0 },
+               { .in = "%{test_filter(c='3', b='2', a='1')}", .out ="done", .ret = 0 },
+               { .in = "%{test_filter('1', '2', '3', a='1', b='2', c='3')}", .out ="done", .ret = 0 },
+               { .in = "%{test_filter(c='3', b='2', a='1', '1', '2', '3')}", .out ="done", .ret = 0 },
+               { .in = "%{test_filter(C='3', B='2', a='1', '1', '2', '3')}", .out ="done", .ret = 0 },
+       };
+
+       test_begin("var_expand(sorted parameters)");
+
+       var_expand_register_filter("test_filter",test_filter);
+
+       const struct var_expand_params params = {
+       };
+
+       run_var_expand_tests(&params, tests, N_ELEMENTS(tests));
+
+       test_end();
+}
+
+static void test_var_expand_new_perc(void)
+{
+       test_begin("var_expand(percentage handling)");
+
+       const struct var_expand_test tests[] = {
+               { .in = "%%{test}", .out = "%{test}", .ret = 0 },
+               { .in = "%Lu", .out = "%Lu", .ret = 0 },
+               { .in = "%%Lu", .out = "%%Lu", .ret = 0 },
+               { .in = "%{test}", .out = "value", .ret = 0 },
+               { .in = "%%Lu", .out = "%%Lu", .ret = 0 },
+               { .in = "%%", .out = "%%", .ret = 0 },
+               { .in = "%", .out = "%", .ret = 0 },
+               { .in = "%%-%%{test}-%%{test}", .out = "%%-%{test}-%{test}", .ret = 0 },
+       };
+
+       const struct var_expand_params params = {
+               .table = (const struct var_expand_table[]){
+                       { .key = "test", .value = "value" },
+                       VAR_EXPAND_TABLE_END
+               },
+       };
+
+       run_var_expand_tests(&params, tests, N_ELEMENTS(tests));
+
+       test_end();
+}
+
+static void test_var_expand_new_set_copy(void)
+{
+       test_begin("var_expand(set, copy)");
+       struct var_expand_table tab[] = {
+               { .key = "one", .value = NULL },
+               { .key = "two", .value = NULL },
+               { .key = "three", .value = NULL },
+               { .key = "four", .value = NULL },
+               VAR_EXPAND_TABLE_END
+       };
+
+       var_expand_table_set_value(tab, "one", "value");
+       var_expand_table_copy(tab, "two", "one");
+
+       var_expand_table_set_func(tab, "three", test_value);
+       var_expand_table_copy(tab, "four", "three");
+
+       test_assert(tab[0].value == tab[1].value);
+       test_assert(tab[2].func == tab[3].func);
+
+       test_end();
+}
+
+int main(void)
+{
+       void (*const tests[])(void) = {
+               test_var_expand_merge_tables,
+               test_var_expand_new_builtin_filters,
+               test_var_expand_new_math,
+               test_var_expand_new_if,
+               test_var_expand_new_providers,
+               test_var_expand_new_provider_arr,
+               test_var_expand_new_tables_arr,
+               test_var_expand_new_escape,
+               test_var_expand_new_value_func,
+               test_var_expand_new_value_func_arr,
+               test_var_expand_new_variables,
+               test_var_expand_new_parameter_sorted,
+               test_var_expand_new_perc,
+               test_var_expand_new_set_copy,
+               NULL
+       };
+
+       return test_run(tests);
+}
diff --git a/src/lib-var-expand/var-expand-lexer.l b/src/lib-var-expand/var-expand-lexer.l
new file mode 100644 (file)
index 0000000..fd1eabf
--- /dev/null
@@ -0,0 +1,167 @@
+/* Copyright (c) 2024 Dovecot authors, see the included COPYING file */
+
+%option nounput
+%option noinput
+%option noyywrap
+%option noyyalloc noyyrealloc noyyfree
+%option reentrant
+%option bison-locations
+%option bison-bridge
+%option never-interactive
+%option prefix="var_expand_parser_"
+%option stack
+
+%{
+
+#include "lib.h"
+#include "str.h"
+#include "var-expand-private.h"
+#include "var-expand-parser-private.h"
+#include "var-expand-parser.h"
+
+#pragma GCC diagnostic push
+
+/* ignore strict bool warnings in generated code */
+#ifdef HAVE_STRICT_BOOL
+#  pragma GCC diagnostic ignored "-Wstrict-bool"
+#endif
+/* ignore sign comparison errors (buggy flex) */
+#pragma GCC diagnostic ignored "-Wsign-compare"
+/* ignore unused functions */
+#pragma GCC diagnostic ignored "-Wunused-function"
+/* ignore unused parameters */
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+
+/* mimic renaming done by bison's api.prefix %define */
+#define YYSTYPE         VAR_EXPAND_PARSER_STYPE
+#define YYLTYPE         VAR_EXPAND_PARSER_LTYPE
+
+static size_t input_proc(char *buf, size_t size, yyscan_t scanner);
+#define YY_INPUT(buf, result, max_size) \
+       result = input_proc(buf, max_size, yyscanner);
+
+
+#define INIT_STR STMT_START { \
+       YYSTYPE *state = yyget_extra(yyscanner); \
+       yylval->str = str_new(state->pool, 32); \
+       } STMT_END
+
+%}
+
+%x stringsq
+%x stringdq
+%x quote
+%x expr
+
+%%
+
+<quote>{
+  [\\]        { yy_pop_state(yyscanner); str_append_c(yylval->str, '\\'); }
+  x[0-9a-fA-F]{2}    {
+    unsigned int c;
+    if (str_to_uint_hex(yytext+1, &c) < 0 || c > 255) {
+      YYSTYPE *state = yyget_extra(yyscanner);
+      state->error = "Invalid character escape";
+      yyterminate();
+    }
+    yy_pop_state(yyscanner); str_append_c(yylval->str, c);
+  }
+  [0-9]{1,3}   {
+    unsigned int c;
+    if (str_to_uint_oct(yytext, &c) < 0 || c > 255) {
+      YYSTYPE *state = yyget_extra(yyscanner);
+      state->error = "Invalid character escape";
+      yyterminate();
+    }
+    yy_pop_state(yyscanner); str_append_c(yylval->str, c);
+  }
+  t       { yy_pop_state(yyscanner); str_append_c(yylval->str, '\t'); }
+  r       { yy_pop_state(yyscanner); str_append_c(yylval->str, '\r'); }
+  n       { yy_pop_state(yyscanner); str_append_c(yylval->str, '\n'); }
+  ["]     { yy_pop_state(yyscanner); str_append_c(yylval->str, '"'); }
+  [']      { yy_pop_state(yyscanner); str_append_c(yylval->str, '\''); }
+  . { YYSTYPE *state = yyget_extra(yyscanner); state->error = "Invalid character escape"; }
+}
+
+<stringdq>{
+  [\\]    { yy_push_state(quote, yyscanner); }
+  ["]     { yy_pop_state(yyscanner); return VALUE; }
+  [^"\\]* { str_append(yylval->str, yytext); }
+}
+
+<stringsq>{
+  [\\]    { yy_push_state(quote, yyscanner); }
+  [']     { yy_pop_state(yyscanner); return VALUE; }
+  [^'\\]* { str_append(yylval->str, yytext); }
+}
+
+<expr>{
+  "}"      { yy_pop_state(yyscanner); return CCBRACE; }
+  [ \t]     { /* ignore */ }
+  \%        { return PERC; }
+  [a-zA-Z\x80-\xff][a-zA-Z0-9_/;:.\x80-\xff-]* { INIT_STR; str_append(yylval->str, yytext); return NAME; }
+  \|        { return PIPE; }
+  \(        { return OBRACE; }
+  \)        { return CBRACE; }
+  \,        { return COMMA; }
+  =        { return EQ; }
+  \+        { return PLUS; }
+  -         { return MINUS; }
+  \*        { return STAR; }
+  \/        { return SLASH; }
+  [']       { yy_push_state(stringsq, yyscanner); INIT_STR; }
+  ["]       { yy_push_state(stringdq, yyscanner); INIT_STR; }
+  [0-9]+    { INIT_STR; str_append(yylval->str, yytext); return NUMBER; }
+}
+
+"%{"     { yy_push_state(expr, yyscanner); return OCBRACE; }
+"%%{"     { INIT_STR; str_append(yylval->str, "%{"); return VALUE; }
+"%"      { return PERC; }
+[^%]+     { INIT_STR; str_append(yylval->str, yytext); return VALUE; }
+
+%%
+
+extern void var_expand_parser_error(YYLTYPE *loc, YYSTYPE *state, const char *error);
+void var_expand_parser_error(YYLTYPE *loc, YYSTYPE *state, const char *error)
+{
+       state->failed = TRUE;
+       state->error = p_strdup(state->pool, error);
+}
+
+void *yyalloc(size_t bytes, void* yyscanner)
+{
+       YYSTYPE *state = yyget_extra(yyscanner);
+       return  p_malloc(state->pool, bytes);
+}
+
+void *yyrealloc (void *ptr, size_t bytes, void *yyscanner)
+{
+       YYSTYPE *state = yyget_extra(yyscanner);
+       return p_realloc(state->pool, ptr, SIZE_MAX, bytes);
+}
+
+void yyfree(void *ptr, void *yyscanner)
+{
+       YYSTYPE *state = yyget_extra(yyscanner);
+       p_free(state->pool, ptr);
+}
+
+#define INPUT_POS(state) (state->input + state->input_pos)
+
+static size_t input_proc(char *buf, size_t size, yyscan_t scanner)
+{
+       YYSTYPE *state = yyget_extra(scanner);
+       size_t ret = 0;
+       if (size > state->left) {
+               memcpy(buf, INPUT_POS(state), state->left);
+               state->input_pos += state->left;
+               ret = state->left;
+               state->left = 0;
+       } else {
+               memcpy(buf, INPUT_POS(state), size);
+               state->left -= size;
+               state->input_pos += size;
+               ret = size;
+       }
+       return ret;
+}
diff --git a/src/lib-var-expand/var-expand-new.h b/src/lib-var-expand/var-expand-new.h
new file mode 100644 (file)
index 0000000..eb05bbd
--- /dev/null
@@ -0,0 +1,172 @@
+#ifndef VAR_EXPAND_NEW_H
+#define VAR_EXPAND_NEW_H
+
+/* Used for getting either prefix:key values, or dynamic values for keys
+   in value tables.
+
+   Gets key and context, needs to return -1 on error (with error_r set)
+   or 0 on success. value_r *must* be non-null on success.
+
+   Prefix is removed before calling the function.
+*/
+typedef int value_provider_func_t(const char *key, const char **value_r,
+                                 void *context, const char **error_r);
+/* Used for escaping values, gets given string to escape and context,
+   must return escaped string. */
+typedef const char *var_expand_escape_func_t(const char *str, void *context);
+
+struct var_expand_parser_state;
+struct var_expand_program;
+
+#define VAR_EXPAND_TABLE_END { .key = NULL }
+#define VAR_EXPAND_CONTEXTS_END (void*)var_expand_contexts_end
+
+struct var_expand_table_new {
+       /* Key name, as in %{key} */
+       const char *key;
+       /* Value to expand into */
+       const char *value;
+       /* Or function that provides the value */
+       value_provider_func_t *func;
+};
+#define var_expand_table var_expand_table_new
+
+struct var_expand_provider {
+       /* key as in %{key:name} */
+       const char *key;
+       /* function to call to get value */
+       value_provider_func_t *func;
+};
+
+extern const void *const var_expand_contexts_end;
+
+struct var_expand_params_new {
+       /* Variables to use, must end with VAR_EXPAND_TABLE_END,
+          asserts that tables_arr is non-NULL. */
+       const struct var_expand_table *table;
+       /* Providers to use, must end with VAR_EXPAND_TABLE_END,
+          asserts that providers_arr is non-NULL. */
+       const struct var_expand_provider *providers;
+       /* Multiple var expand tables, must be NULL terminated */
+       const struct var_expand_table *const *tables_arr;
+       /* Multiple var expand providers, must be NULL terminated */
+       const struct var_expand_provider *const *providers_arr;
+       /* Function that gets called to escape values */
+       var_expand_escape_func_t *escape_func;
+       /* Context for escape function */
+       void *escape_context;
+       /* Contexts for table functions and providers, can be
+          set to NULL if no multiple contexts are needed, then context
+          is defaulted to.
+
+          Asserts that contexts ends with VAR_EXPAND_CONTEXTS_END.
+       */
+       void *const *contexts;
+       /* Context for table functions and providers. */
+       void *context;
+       /* Event for %{event:} expansion, can be NULL. Global event
+          will be attempted if this is NULL. */
+       struct event *event;
+};
+#define var_expand_params var_expand_params_new
+
+/* Creates a new expansion program for reusing */
+int var_expand_program_create(const char *str, struct var_expand_program **program_r,
+                             const char **error_r);
+/* Lists all seen variables in a program */
+const char *const *var_expand_program_variables(const struct var_expand_program *program);
+/* Dumps the program into a dest for debugging */
+void var_expand_program_dump(const struct var_expand_program *program, string_t *dest);
+/* Executes the program with given params. Params can be left NULL, in which case
+   empty parameters are used. */
+int var_expand_program_execute(string_t *dest, const struct var_expand_program *program,
+                              const struct var_expand_params *params,
+                              const char **error_r) ATTR_NULL(3);
+/* Free up program */
+void var_expand_program_free(struct var_expand_program **_program);
+
+/* Creates a new program, executes it and frees it. Params can be left NULL, in which
+   case empty parameters are used. */
+int var_expand_new(string_t *dest, const char *str, const struct var_expand_params *params,
+                  const char **error_r) ATTR_NULL(3);
+
+/* Wrapper for var_expand(), places the result into result_r. */
+int t_var_expand(const char *str, const struct var_expand_params *params,
+                const char **result_r, const char **error_r);
+
+/* Merge two tables together, keys in table a will be overwritten with keys
+ * from table b in collision. */
+struct var_expand_table *
+var_expand_merge_tables_new(pool_t pool, const struct var_expand_table *a,
+                           const struct var_expand_table *b);
+
+/* Returns true if provider is a built-in provider */
+bool var_expand_provider_is_builtin(const char *prefix);
+
+/* Provides size of a table */
+static inline size_t ATTR_PURE
+var_expand_table_size_new(const struct var_expand_table *table)
+{
+       size_t n = 0;
+       while (table != NULL && table[n].key != NULL)
+                n++;
+       return n;
+}
+
+/* Get table entry by name. Returns NULL if not found. */
+static inline struct var_expand_table *
+var_expand_table_get(struct var_expand_table *table, const char *key)
+{
+       for (size_t i = 0; table[i].key != NULL; i++) {
+               if (strcmp(table[i].key, key) == 0) {
+                       return &(table[i]);
+               }
+       }
+       return NULL;
+}
+
+/* Set table variable to value. Asserts that key is found. */
+static inline void var_expand_table_set_value(struct var_expand_table *table,
+                                             const char *key, const char *value,
+                                             const char *file, unsigned int line)
+{
+       struct var_expand_table *entry = var_expand_table_get(table, key);
+       if (entry != NULL) {
+               i_assert(entry->func == NULL);
+               entry->value = value;
+       } else
+               i_panic("%s:%u No key '%s' in table", file, line, key);
+}
+#define var_expand_table_set_value(table, key, value) \
+       var_expand_table_set_value((table), (key), (value), __FILE__, __LINE__);
+
+/* Set table variable function. Asserts that key is found. */
+static inline void var_expand_table_set_func(struct var_expand_table *table,
+                                            const char *key,
+                                            value_provider_func_t *func,
+                                            const char *file, unsigned int line)
+{
+       struct var_expand_table *entry = var_expand_table_get(table, key);
+       if (entry != NULL) {
+               i_assert(entry->value == NULL);
+               entry->func = func;
+       } else
+               i_panic("%s:%u No key '%s' in table", file, line, key);
+}
+#define var_expand_table_set_func(table, key, func) \
+       var_expand_table_set_func((table), (key), (func), __FILE__, __LINE__);
+
+/* Set key_b variable to key_a. Copies func and value.
+   Asserts that both are found. */
+static inline void var_expand_table_copy(struct var_expand_table *table,
+                                        const char *key_b, const char *key_a)
+{
+       struct var_expand_table *entry_a = var_expand_table_get(table, key_a);
+       struct var_expand_table *entry_b = var_expand_table_get(table, key_b);
+
+       i_assert(entry_a != NULL && entry_b != NULL);
+       entry_b->value = entry_a->value;
+       entry_b->func = entry_a->func;
+}
+
+#endif
diff --git a/src/lib-var-expand/var-expand-parser-private.h b/src/lib-var-expand/var-expand-parser-private.h
new file mode 100644 (file)
index 0000000..2b7d389
--- /dev/null
@@ -0,0 +1,30 @@
+#ifndef VAR_EXPAND_PARSER_PRIVATE_H
+#define VAR_EXPAND_PARSER_PRIVATE_H 1
+
+#define VAR_EXPAND_PARSER_STYPE struct var_expand_parser_state
+struct var_expand_parser_state {
+       pool_t pool;
+        const char *input;
+       size_t left;
+        size_t input_pos;
+       string_t *str;
+       void* scanner;
+
+       struct var_expand_program *plist;
+       struct var_expand_program *pp;
+       struct var_expand_program *p;
+       bool failed;
+       const char *error;
+
+       /* temp vars */
+       struct var_expand_parameter *params;
+       int idx;
+       const char *funcname;
+       const char *key;
+       const char *value;
+       enum var_expand_statement_operator oper;
+       intmax_t number;
+       ARRAY_TYPE(const_string) variables;
+};
+
+#endif
diff --git a/src/lib-var-expand/var-expand-parser.y b/src/lib-var-expand/var-expand-parser.y
new file mode 100644 (file)
index 0000000..108b6a7
--- /dev/null
@@ -0,0 +1,293 @@
+/* Copyright (c) 2024 Dovecot authors, see the included COPYING file */
+
+%define api.pure full
+%define api.prefix {var_expand_parser_}
+%define parse.error verbose
+%lex-param {void *scanner}
+%parse-param {struct var_expand_parser_state *state}
+%locations
+%defines
+
+%{
+
+#include "lib.h"
+#include "strnum.h"
+#include "str.h"
+#include "array.h"
+
+#include "var-expand-private.h"
+#include "var-expand-parser-private.h"
+#include "var-expand-parser.h"
+#include "expansion.h"
+
+#pragma GCC diagnostic push
+
+/* ignore strict bool warnings in generated code */
+#ifdef HAVE_STRICT_BOOL
+#  pragma GCC diagnostic ignored "-Wstrict-bool"
+#endif
+#pragma GCC diagnostic ignored "-Wpragmas"
+#pragma GCC diagnostic ignored "-Wunknown-warning-option"
+/* ignore sign comparison errors (buggy flex) */
+#pragma GCC diagnostic ignored "-Wsign-compare"
+/* ignore unused functions */
+#pragma GCC diagnostic ignored "-Wunused-function"
+/* ignore unused parameters */
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-but-set-variable"
+
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdint.h>
+
+void yyerror(YYLTYPE *loc, struct var_expand_parser_state *state, const char *error);
+
+extern int yylex(void *, void *, void *);
+extern void var_expand_parser_lex_init_extra(void*, void*);
+
+#define scanner state->scanner
+
+/* List of likely non-variable names */
+static const char *const filter_var_names[] = {
+       "literal",
+       "lookup",
+       "if",
+       "calculate",
+       NULL
+};
+
+static void register_variable(VAR_EXPAND_PARSER_STYPE *state, const char *name,
+                             bool maybe_func)
+{
+       /* When parsing the first item on the line, we end up here because
+          it could be function or variable. This list is the most common likely
+          function names that we exclude, and avoid them getting mistakenly added
+          into list of variables. We can't exclude every function, because there
+          are some functions that can also be variables, like domain. */
+       if (maybe_func && str_array_find(filter_var_names, name))
+               return;
+
+       /* see if it is there yet */
+       if (array_bsearch(&state->variables, &name, i_strcmp_p) != NULL)
+               return;
+       array_push_back(&state->variables, &name);
+       array_sort(&state->variables, i_strcmp_p);
+}
+
+static void
+link_argument(VAR_EXPAND_PARSER_STYPE *state, struct var_expand_parameter *par)
+{
+       /* First argument, just put it here */
+       if (state->params == NULL) {
+               state->params = par;
+               return;
+       }
+
+       struct var_expand_parameter *ptr = state->params;
+       struct var_expand_parameter *prev = NULL;
+
+       if (par->idx > -1) {
+               /* Parameters with index number go first, and are sorted by idx */
+               while (ptr != NULL && ptr->idx > -1 && ptr->idx < par->idx) {
+                       prev = ptr;
+                       ptr = ptr->next;
+               }
+       } else {
+               /* Named ones go after, and are sorted by key */
+               while (ptr != NULL && (ptr->idx > -1 ||
+                                      strcmp(ptr->key, par->key) < 0)) {
+                       prev = ptr;
+                       ptr = ptr->next;
+               }
+       }
+
+       /* We should now have a position where to place the key, */
+       if (ptr != NULL && par->idx == -1 && strcmp(ptr->key, par->key) >= 0) {
+               if (prev == NULL) {
+                       /* prepend it as first */
+                       par->next = state->params;
+                       state->params = par;
+               } else {
+                       /* prepend it to previous item */
+                       par->next = prev->next;
+                       prev->next = par;
+               }
+       } else if (ptr == NULL) {
+               /* append it at end of list */
+               i_assert(prev != NULL);
+               prev->next = par;
+       } else if ((ptr->idx == -1 && par->idx > -1) ||
+                  (ptr->idx > -1 && par->idx < ptr->idx)) {
+               if (prev == NULL) {
+                       /* prepend it as first */
+                       par->next = state->params;
+                       state->params = par;
+               } else {
+                       /* prepend it to previous item */
+                       par->next = prev->next;
+                       prev->next = par;
+               }
+       } else {
+               /* prepend it to current item */
+               par->next = ptr->next;
+               ptr->next = par;
+       }
+}
+
+static void
+push_named_argument(VAR_EXPAND_PARSER_STYPE *state, const char *name,
+                   enum var_expand_parameter_value_type type,
+                   const union var_expand_parameter_value *value)
+{
+       struct var_expand_parameter *par =
+               p_new(state->plist->pool, struct var_expand_parameter, 1);
+       par->idx = -1;
+       /* Ensure keys are always lowercased */
+       par->key = p_strdup(state->plist->pool, t_str_lcase(name));
+       par->value_type = type;
+       par->value = *value;
+       if (type != VAR_EXPAND_PARAMETER_VALUE_TYPE_INT)
+               par->value.str = p_strdup(state->plist->pool, value->str);
+       if (type == VAR_EXPAND_PARAMETER_VALUE_TYPE_VARIABLE)
+               register_variable(state, par->value.str, FALSE);
+       link_argument(state, par);
+}
+
+static void
+push_argument(VAR_EXPAND_PARSER_STYPE *state,
+             enum var_expand_parameter_value_type type,
+             const union var_expand_parameter_value *value)
+{
+       struct var_expand_parameter *par =
+               p_new(state->plist->pool, struct var_expand_parameter, 1);
+       par->idx = state->idx++;
+       par->value_type = type;
+       par->value = *value;
+       if (type != VAR_EXPAND_PARAMETER_VALUE_TYPE_INT)
+               par->value.str = p_strdup(state->plist->pool, value->str);
+       if (type == VAR_EXPAND_PARAMETER_VALUE_TYPE_VARIABLE)
+               register_variable(state, par->value.str, FALSE);
+       link_argument(state, par);
+}
+
+static void make_new_program(VAR_EXPAND_PARSER_STYPE *pstate)
+{
+       struct var_expand_program *p =
+               p_new(pstate->plist->pool, struct var_expand_program, 1);
+       p->pool = pstate->plist->pool;
+       pstate->pp->next = p;
+       pstate->p = p;
+}
+
+static void push_function(VAR_EXPAND_PARSER_STYPE *state, const char *func)
+{
+       if (state->p == NULL)
+               make_new_program(state);
+       struct var_expand_statement *f =
+               p_new(state->plist->pool, struct var_expand_statement, 1);
+       f->function = func;
+       if (state->p->first == NULL)
+               register_variable(state, func, TRUE);
+       f->params = state->params;
+       if (state->p->first == NULL)
+               state->p->first = f;
+       else {
+               struct var_expand_statement *ptr = state->p->first;
+               while (ptr->next != NULL)
+                       ptr = ptr->next;
+               ptr->next = f;
+       }
+       state->params = NULL;
+       state->idx = 0;
+}
+
+static void push_new_program(VAR_EXPAND_PARSER_STYPE *pstate)
+{
+       pstate->pp = pstate->p;
+       pstate->p = NULL;
+}
+
+static union var_expand_parameter_value tmp_value;
+
+%}
+
+%token PERC OCBRACE CCBRACE PIPE OBRACE CBRACE COMMA DOT QUOTE EQ PLUS MINUS STAR SLASH
+%token <str> NAME
+%token <str> VALUE
+%token <str> NUMBER
+
+%type <oper> operator
+%type <number> number
+%type <key> key
+%type <funcname> funcname
+
+%%
+
+var : expression_list
+    ;
+
+expression_list:
+              | expression_list expression { push_new_program(state); }
+              ;
+
+expression: VALUE { i_zero(&tmp_value); tmp_value.str = str_c($1); push_argument(state, VAR_EXPAND_PARAMETER_VALUE_TYPE_STRING, &tmp_value); push_function(state, "literal"); state->p->only_literal = TRUE;}
+          | OCBRACE filter_list CCBRACE
+         | PERC { i_zero(&tmp_value); tmp_value.str = "%"; push_argument(state, VAR_EXPAND_PARAMETER_VALUE_TYPE_STRING, &tmp_value); push_function(state, "literal"); state->p->only_literal = TRUE; }
+         ;
+
+filter_list: filter_list PIPE filter
+          | filter
+          ;
+
+filter: func math_list
+       |
+       ;
+
+
+math_list:
+        | math
+        ;
+
+math: operator number { i_zero(&tmp_value); tmp_value.num = $1; push_argument(state, VAR_EXPAND_PARAMETER_VALUE_TYPE_INT, &tmp_value); i_zero(&tmp_value); tmp_value.num = $2; push_argument(state, VAR_EXPAND_PARAMETER_VALUE_TYPE_INT, &tmp_value); push_function(state, "calculate"); }
+    | operator NAME { i_zero(&tmp_value); tmp_value.num = $1; push_argument(state, VAR_EXPAND_PARAMETER_VALUE_TYPE_INT, &tmp_value); i_zero(&tmp_value); tmp_value.str = str_c($2); push_argument(state, VAR_EXPAND_PARAMETER_VALUE_TYPE_VARIABLE, &tmp_value); push_function(state, "calculate"); }
+    ;
+
+number: MINUS NUMBER { str_insert($2, 0, "-"); if (str_to_intmax(str_c($2), &$$) < 0) { yyerror (&yylloc, state, YY_("Not a number")); YYERROR; }; }
+      | NUMBER { if (str_to_intmax(str_c($1), &$$) < 0) { yyerror (&yylloc, state, YY_("Not a number")); YYERROR; }; }
+      ;
+
+operator: PLUS { $$ = VAR_EXPAND_STATEMENT_OPER_PLUS; }
+       | MINUS { $$ = VAR_EXPAND_STATEMENT_OPER_MINUS; }
+       | STAR { $$ = VAR_EXPAND_STATEMENT_OPER_STAR; }
+       | SLASH { $$ = VAR_EXPAND_STATEMENT_OPER_SLASH; }
+       | PERC { $$ = VAR_EXPAND_STATEMENT_OPER_MODULO; }
+       ;
+
+func  : funcname arguments { push_function(state, $1); }
+      ;
+
+funcname : NAME { $$ = p_strdup(state->plist->pool, str_c($1)); }
+       ;
+
+arguments:
+        | OBRACE argument_list CBRACE
+        ;
+
+argument_list: argument_list COMMA argument
+            | argument
+            ;
+
+argument : VALUE { i_zero(&tmp_value); tmp_value.str = str_c($1); push_argument(state, VAR_EXPAND_PARAMETER_VALUE_TYPE_STRING, &tmp_value); }
+        | NAME { i_zero(&tmp_value); tmp_value.str = str_c($1); push_argument(state, VAR_EXPAND_PARAMETER_VALUE_TYPE_VARIABLE, &tmp_value); }
+        | number { i_zero(&tmp_value); tmp_value.num = $1; push_argument(state, VAR_EXPAND_PARAMETER_VALUE_TYPE_INT, &tmp_value); }
+        | key EQ number { i_zero(&tmp_value); tmp_value.num = $3; push_named_argument(state, $1, VAR_EXPAND_PARAMETER_VALUE_TYPE_INT, &tmp_value); }
+        | key EQ NAME { i_zero(&tmp_value); tmp_value.str = str_c($3); push_named_argument(state, $1, VAR_EXPAND_PARAMETER_VALUE_TYPE_VARIABLE, &tmp_value); }
+        | key EQ VALUE { i_zero(&tmp_value); tmp_value.str = str_c($3); push_named_argument(state, $1, VAR_EXPAND_PARAMETER_VALUE_TYPE_STRING, &tmp_value); }
+        ;
+
+key : NAME { $$ = p_strdup(state->plist->pool, str_c($1)); }
+    ;
+
+%%
diff --git a/src/lib-var-expand/var-expand-private.h b/src/lib-var-expand/var-expand-private.h
new file mode 100644 (file)
index 0000000..6897f6d
--- /dev/null
@@ -0,0 +1,146 @@
+#ifndef VAR_EXPAND_PRIVATE_H
+#define VAR_EXPAND_PRIVATE_H 1
+
+#include "var-expand-new.h"
+
+/* Macro for filters to error our with unsupported key */
+#define ERROR_UNSUPPORTED_KEY(key) STMT_START { \
+       *error_r = t_strdup_printf("Unsupported key '%s'", key); \
+       return -1; \
+} STMT_END
+
+/* Macro for filters to error out with too many positional parameters */
+#define ERROR_TOO_MANY_UNNAMED_PARAMETERS STMT_START { \
+       *error_r = "Too many positional parameters"; \
+       return -1; \
+} STMT_END
+
+/* Error out if filter did not get parameters at all */
+#define ERROR_IF_NO_PARAMETERS \
+STMT_START { if (stmt->params == NULL) { \
+       *error_r = "Missing parameters"; \
+       return -1; \
+} } STMT_END
+
+/* Error out if filter got any parameters */
+#define ERROR_IF_ANY_PARAMETERS \
+STMT_START { if (stmt->params != NULL) { \
+       const char *key = var_expand_parameter_key(stmt->params); \
+       if (key != NULL) { \
+               ERROR_UNSUPPORTED_KEY(key); \
+       } else { \
+               ERROR_TOO_MANY_UNNAMED_PARAMETERS; \
+       } \
+} } STMT_END
+
+/* Error out if transfer is not set. */
+#define ERROR_IF_NO_TRANSFER_TO(action) \
+STMT_START { if (!state->transfer_set) { \
+       *error_r = t_strdup_printf("No value to %s", action); \
+       return -1; \
+} } STMT_END
+
+struct var_expand_state;
+struct var_expand_parameter_iter_context;
+struct var_expand_statement;
+struct var_expand_parameter;
+
+enum var_expand_statement_operator {
+       VAR_EXPAND_STATEMENT_OPER_PLUS = 0,
+       VAR_EXPAND_STATEMENT_OPER_MINUS,
+       VAR_EXPAND_STATEMENT_OPER_STAR,
+       VAR_EXPAND_STATEMENT_OPER_SLASH,
+       VAR_EXPAND_STATEMENT_OPER_MODULO,
+       VAR_EXPAND_STATEMENT_OPER_COUNT
+};
+
+struct var_expand_program {
+       pool_t pool;
+       struct var_expand_program *next;
+       struct var_expand_statement *first;
+       const char *const *variables;
+       bool only_literal:1;
+};
+
+struct var_expand_state {
+       /* Parameters for var_expand_program_execute */
+       const struct var_expand_params *params;
+       string_t *result;
+       /* used for delayed first variable error */
+       char *delayed_error;
+
+       /* use transfer helpers */
+       string_t *transfer;
+       bool transfer_set:1;
+       bool transfer_binary:1;
+};
+
+struct var_expand_statement {
+       struct var_expand_statement *next;
+       const char *function;
+       const struct var_expand_parameter *params, *ptr;
+};
+
+typedef int var_expand_filter_func_t(const struct var_expand_statement *stmt,
+                                struct var_expand_state *state, const char **error_r);
+
+/* Parameter accessors */
+const char *var_expand_parameter_key(const struct var_expand_parameter *param);
+int var_expand_parameter_idx(const struct var_expand_parameter *param);
+
+int var_expand_parameter_number(const struct var_expand_parameter *param,
+                               bool convert, intmax_t *value_r);
+int var_expand_parameter_string(const struct var_expand_parameter *param,
+                               bool convert, const char **value_r);
+int var_expand_parameter_from_state(struct var_expand_state *state, bool number,
+                                   const struct var_expand_parameter **param_r);
+
+/* Require number or variable containing number */
+int var_expand_parameter_number_or_var(const struct var_expand_state *state,
+                                      const struct var_expand_parameter *param,
+                                      intmax_t *value_r, const char **error_r);
+int var_expand_parameter_bool_or_var(const struct var_expand_state *state,
+                                    const struct var_expand_parameter *param,
+                                    bool *value_r, const char **error_r);
+
+/* Require string or variable containing string */
+int var_expand_parameter_string_or_var(const struct var_expand_state *state,
+                                      const struct var_expand_parameter *param,
+                                      const char **value_r, const char **error_r);
+
+/* Get string (or number as string) or variable as string */
+int var_expand_parameter_any_or_var(const struct var_expand_state *state,
+                                   const struct var_expand_parameter *param,
+                                   const char **value_r, const char **error_r);
+
+/* Iterator for accessing parameters */
+struct var_expand_parameter_iter_context *
+var_expand_parameter_iter_init(const struct var_expand_statement *stmt);
+bool var_expand_parameter_iter_more(struct var_expand_parameter_iter_context *ctx);
+const struct var_expand_parameter *
+var_expand_parameter_iter_next(struct var_expand_parameter_iter_context *ctx);
+
+void var_expand_parameter_dump(string_t *dest, const struct var_expand_parameter *par);
+
+/* Looks up variable from state, if not found, returns -1 */
+int var_expand_state_lookup_variable(const struct var_expand_state *state,
+                                    const char *name, const char **result_r,
+                                    const char **error_r);
+/* Sets the transfer data in state */
+void var_expand_state_set_transfer_data(struct var_expand_state *state,
+                                       const void *value, size_t len);
+/* Sets the transfer data in state as binary */
+void var_expand_state_set_transfer_binary(struct var_expand_state *state,
+                                         const void *value, size_t len);
+
+/* Sets the transfer data to provided string in state */
+void var_expand_state_set_transfer(struct var_expand_state *state, const char *value);
+
+/* Unsets transfer data */
+void var_expand_state_unset_transfer(struct var_expand_state *state);
+
+void var_expand_register_filter(const char *name, var_expand_filter_func_t *const filter);
+bool var_expand_is_filter(const char *name);
+void var_expand_unregister_filter(const char *name);
+
+#endif
diff --git a/src/lib-var-expand/var-expand.c b/src/lib-var-expand/var-expand.c
new file mode 100644 (file)
index 0000000..a9c1e1b
--- /dev/null
@@ -0,0 +1,464 @@
+/* Copyright (c) 2024 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "cpu-count.h"
+#include "hostpid.h"
+#include "str.h"
+#include "time-util.h"
+#include "var-expand-private.h"
+#include "var-expand-parser.h"
+#include "expansion.h"
+#include "dovecot-version.h"
+
+#include <unistd.h>
+#include <stdint.h>
+#include <time.h>
+
+#ifdef HAVE_SYS_UTSNAME_H
+#  include <sys/utsname.h>
+#endif
+
+#define ENV_CPU_COUNT "NCPU"
+enum os_default_type {
+       OS_DEFAULT_TYPE_SYSNAME,
+       OS_DEFAULT_TYPE_RELEASE,
+};
+
+const void *const var_expand_contexts_end = POINTER_CAST(UINTPTR_MAX);
+
+static int
+var_expand_process(const char *field, const char **result_r,
+                  void *context ATTR_UNUSED, const char **error_r)
+{
+       if (strcmp(field, "pid") == 0)
+               *result_r = my_pid;
+       else if (strcmp(field, "uid") == 0)
+               *result_r = dec2str(geteuid());
+       else if (strcmp(field, "gid") == 0)
+               *result_r = dec2str(getegid());
+       else {
+               *error_r = t_strdup_printf("Unsupported process field '%s'",
+                                          field);
+               return -1;
+       }
+       return 0;
+}
+
+static struct utsname utsname_result;
+static bool utsname_set = FALSE;
+
+static int
+var_expand_system_os(enum os_default_type type,
+                    const char **value_r, const char **error_r)
+{
+       if (!utsname_set) {
+               utsname_set = TRUE;
+
+               if (uname(&utsname_result) < 0) {
+                       *error_r = t_strdup_printf("uname() failed: %m");
+                       i_zero(&utsname_result);
+                       return -1;
+               }
+       }
+
+       switch (type) {
+       case OS_DEFAULT_TYPE_SYSNAME:
+               *value_r = utsname_result.sysname;
+               return 0;
+       case OS_DEFAULT_TYPE_RELEASE:
+               *value_r = utsname_result.release;
+               return 0;
+       default:
+               break;
+       }
+
+       i_unreached();
+}
+
+static int
+var_expand_system(const char *field, const char **result_r,
+                 void *context ATTR_UNUSED, const char **error_r)
+{
+       if (strcmp(field, "cpu_count") == 0) {
+               int ncpus;
+               const char *cpuenv = getenv(ENV_CPU_COUNT);
+               if (cpuenv != NULL) {
+                       *result_r = cpuenv;
+                       return 0;
+               }
+               if (cpu_count_get(&ncpus, error_r) < 0)
+                       return -1;
+               *result_r = dec2str(ncpus);
+               return 0;
+       } else if (strcmp(field, "hostname") == 0) {
+               *result_r = my_hostname;
+               return 0;
+       } else if (strcmp(field, "os") == 0)
+               return var_expand_system_os(OS_DEFAULT_TYPE_SYSNAME, result_r,
+                                           error_r);
+       else if (strcmp(field, "os-version") == 0)
+               return var_expand_system_os(OS_DEFAULT_TYPE_RELEASE, result_r,
+                                           error_r);
+       *error_r = t_strdup_printf("Unsupported system key '%s'", field);
+       return -1;
+}
+
+static int
+var_expand_dovecot(const char *field, const char **result_r,
+                  void *context ATTR_UNUSED, const char **error_r)
+{
+       if (strcmp(field, "name") == 0) {
+               *result_r = PACKAGE_NAME;
+               return 0;
+       } else if (strcmp(field, "version") == 0) {
+               *result_r = PACKAGE_VERSION;
+               return 0;
+       } else if (strcmp(field, "support-url") == 0) {
+               *result_r = PACKAGE_WEBPAGE;
+               return 0;
+       } else if (strcmp(field, "support-email") == 0) {
+               *result_r = PACKAGE_BUGREPORT;
+               return 0;
+       } else if (strcmp(field, "revision") == 0) {
+               *result_r = DOVECOT_REVISION;
+               return 0;
+       }
+
+       *error_r = t_strdup_printf("Unsupported dovecot key '%s'", field);
+       return -1;
+}
+
+static int var_expand_env(const char *key, const char **value_r,
+                         void *context ATTR_UNUSED, const char **error_r)
+{
+       if (*key == '\0') {
+               *error_r = "Missing key";
+               return -1;
+       }
+
+       const char *value = getenv(key);
+
+       /* never fail with env, it would make code too hard */
+       if (value == NULL)
+               value = "";
+
+       *value_r = value;
+       return 0;
+}
+
+static int var_expand_event(const char *key, const char **value_r, void *context,
+                           const char **error_r)
+{
+       const struct var_expand_params *params = context;
+       struct event *event = params->event;
+
+       if (event == NULL)
+               event = event_get_global();
+       if (event == NULL) {
+               *error_r = "No event available";
+               return -1;
+       }
+
+       const char *value = event_find_field_recursive_str(event, key);
+
+       if (value == NULL) {
+               *error_r = t_strdup_printf("No such field '%s' in event", key);
+               return -1;
+       }
+
+       *value_r = value;
+
+       return 0;
+}
+
+static int var_expand_date(const char *key, const char **value_r,
+                          void *context ATTR_UNUSED, const char **error_r)
+{
+       struct tm tm;
+       struct timeval tv;
+       i_gettimeofday(&tv);
+       if (unlikely(localtime_r(&tv.tv_sec, &tm) == NULL))
+               i_panic("localtime_r() failed: %m");
+
+       if (strcmp(key, "year") == 0)
+               *value_r = t_strftime("%Y", &tm);
+       else if (strcmp(key, "month") == 0)
+               *value_r = t_strftime("%m", &tm);
+       else if (strcmp(key, "day") == 0)
+               *value_r = t_strftime("%d", &tm);
+       else
+               ERROR_UNSUPPORTED_KEY(key);
+       return 0;
+}
+
+static int var_expand_time(const char *key, const char **value_r,
+                          void *context ATTR_UNUSED, const char **error_r)
+{
+       struct tm tm;
+       struct timeval tv;
+       i_gettimeofday(&tv);
+       if (unlikely(localtime_r(&tv.tv_sec, &tm) == NULL))
+               i_panic("localtime_r() failed: %m");
+
+       if (strcmp(key, "hour") == 0)
+               *value_r = t_strftime("%H", &tm);
+       else if (strcmp(key, "min") == 0 ||
+                strcmp(key, "minute") == 0)
+               *value_r = t_strftime("%M", &tm);
+       else if (strcmp(key, "sec") == 0 ||
+                strcmp(key, "second") == 0)
+               *value_r = t_strftime("%S", &tm);
+       else if (strcmp(key, "us") == 0 ||
+                strcmp(key, "usec") == 0)
+               *value_r = dec2str(tv.tv_usec);
+       else
+               ERROR_UNSUPPORTED_KEY(key);
+       return 0;
+}
+
+static const struct var_expand_provider internal_providers[] = {
+       { .key = "process", .func = var_expand_process },
+       { .key = "system", .func = var_expand_system  },
+       { .key = "dovecot", .func = var_expand_dovecot  },
+       { .key = "env", .func = var_expand_env },
+       { .key = "event", .func = var_expand_event },
+       { .key = "date", .func = var_expand_date },
+       { .key = "time", .func = var_expand_time },
+       VAR_EXPAND_TABLE_END
+};
+
+bool var_expand_provider_is_builtin(const char *prefix)
+{
+       for (size_t i = 0; internal_providers[i].key != NULL; i++)
+               if (strcmp(prefix, internal_providers[i].key) == 0)
+                       return TRUE;
+       return FALSE;
+}
+
+static int var_expand_table_key_cmp(const char *key,
+                                   const struct var_expand_table *elem)
+{
+       return strcmp(key, elem->key);
+}
+
+struct var_expand_table *
+var_expand_merge_tables_new(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_new(a);
+       size_t b_size = var_expand_table_size_new(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->value = p_strdup(pool, a[i].value);
+               entry->key = p_strdup(pool, a[i].key);
+       }
+       for (size_t i = 0; i < b_size; i++) {
+               /* check if it's there first */
+               struct var_expand_table *entry =
+                       array_lsearch_modifiable(&table, b[i].key,
+                                                var_expand_table_key_cmp);
+               if (entry != NULL) {
+                       entry->value = b->value;
+                       continue;
+               }
+
+               entry = array_append_space(&table);
+               entry->value = p_strdup(pool, b[i].value);
+               entry->key = p_strdup(pool, b[i].key);
+       }
+       array_append_zero(&table);
+       return array_front_modifiable(&table);
+}
+
+void var_expand_state_set_transfer_data(struct var_expand_state *state,
+                                       const void *value, size_t len)
+{
+       i_assert(value != NULL || len == 0);
+       str_truncate(state->transfer, 0);
+       str_append_data(state->transfer, value, len);
+       state->transfer_set = TRUE;
+}
+
+void var_expand_state_set_transfer_binary(struct var_expand_state *state,
+                                         const void *value, size_t len)
+{
+       i_assert(value != NULL || len == 0);
+       str_truncate(state->transfer, 0);
+       str_append_data(state->transfer, value, len);
+       state->transfer_set = TRUE;
+       state->transfer_binary = TRUE;
+}
+
+void var_expand_state_set_transfer(struct var_expand_state *state, const char *value)
+{
+       size_t len;
+       if (value == NULL)
+               len = 0;
+       else
+               len = strlen(value);
+       var_expand_state_set_transfer_data(state, value, len);
+       state->transfer_binary = FALSE;
+}
+
+void var_expand_state_unset_transfer(struct var_expand_state *state)
+{
+       str_truncate(state->transfer, 0);
+       state->transfer_set = FALSE;
+}
+
+static int call_provider_table(const struct var_expand_provider *prov,
+                              void *prov_context,
+                              const char *prefix, const char *key,
+                              const char **value_r, bool *found_r,
+                              const char **error_r)
+{
+       i_assert(prov_context != var_expand_contexts_end);
+       for (; prov != NULL && prov->key != NULL; prov++) {
+               if (strcmp(prov->key, prefix) == 0) {
+                       *found_r = TRUE;
+                       return prov->func(key, value_r, prov_context, error_r);
+               }
+       }
+       *found_r = FALSE;
+       return -1;
+}
+
+static int call_value_provider(const struct var_expand_state *state,
+                              const char *prefix, const char *key,
+                              const char **value_r, const char **error_r)
+{
+       bool found;
+       int ret = call_provider_table(internal_providers, (void*)state->params, prefix, key, value_r,
+                                     &found, error_r);
+       if (found)
+               ; /* pass */
+       else if (state->params->providers_arr != NULL) {
+               void *context = state->params->context;
+               void *const *contexts = state->params->contexts;
+               for (const struct var_expand_provider *const *prov = state->params->providers_arr;
+                    *prov != NULL; prov++) {
+                       if (contexts != NULL) {
+                               context = *contexts;
+                               contexts++;
+                       }
+                       ret = call_provider_table(*prov, context, prefix, key, value_r,
+                                                 &found, error_r);
+                       if (found)
+                               break;
+               }
+       } else {
+               ret = call_provider_table(state->params->providers,
+                                         state->params->context,
+                                         prefix, key, value_r, &found, error_r);
+       }
+
+       if (!found) {
+               *error_r = t_strdup_printf("Unsupported prefix '%s'", prefix);
+               ret = -1;
+       }
+
+       return ret;
+}
+
+static int lookup_table(const struct var_expand_table *table,
+                       void *context, const char *name,
+                       const char **result_r, bool *found_r, const char **error_r)
+{
+       i_assert(context != var_expand_contexts_end);
+       for (size_t i = 0; table != NULL && table[i].key != NULL; i++) {
+               if (strcmp(table[i].key, name) == 0) {
+                       *found_r = TRUE;
+                       if (table[i].func != NULL) {
+                               int ret = table[i].func(name, result_r,
+                                                       context, error_r);
+                               i_assert(ret >= 0 || *error_r != NULL);
+                               return ret >= 0 ? 0 : -1;
+                       } else
+                               *result_r = table[i].value == NULL ? "" : table[i].value;
+                       return 0;
+               }
+       };
+
+       *error_r = t_strdup_printf("Unknown variable '%s'", name);
+       return -1;
+}
+
+static int lookup_tables(const struct var_expand_state *state, const char *name,
+                        const char **result_r, const char **error_r)
+{
+       int ret;
+       bool found;
+
+       if (state->params->tables_arr != NULL) {
+               void *context = state->params->context;
+               void *const *contexts = state->params->contexts;
+               for (const struct var_expand_table *const *table = state->params->tables_arr;
+                    *table != NULL; table++) {
+                       if (contexts != NULL) {
+                               context = *contexts;
+                               contexts++;
+                       }
+                       found = FALSE;
+                       ret = lookup_table(*table, context, name, result_r, &found, error_r);
+                       if (found)
+                               return ret;
+               }
+               *error_r = t_strdup_printf("Unknown variable '%s'", name);
+               return -1;
+       }
+
+       return lookup_table(state->params->table, state->params->context,
+                           name, result_r, &found, error_r);
+}
+
+int var_expand_state_lookup_variable(const struct var_expand_state *state,
+                                    const char *name, const char **result_r,
+                                    const char **error_r)
+{
+       const char *prefix = name;
+       name = strchr(name, ':');
+
+       if (name == NULL) {
+               name = prefix;
+               prefix = NULL;
+       } else {
+               prefix = t_strdup_until(prefix, name);
+               name++;
+       }
+
+       if (prefix != NULL) {
+               return call_value_provider(state, prefix, name, result_r, error_r);
+       } else {
+               return lookup_tables(state, name, result_r, error_r);
+       }
+}
+
+int var_expand_new(string_t *dest, const char *str,
+                  const struct var_expand_params *params,
+                  const char **error_r)
+{
+       struct var_expand_program *program = NULL;
+       if (var_expand_program_create(str, &program, error_r) != 0)
+               return -1;
+       i_assert(program != NULL);
+       int ret = var_expand_program_execute(dest, program, params, error_r);
+       var_expand_program_free(&program);
+
+       return ret;
+}
+
+int t_var_expand(const char *str, const struct var_expand_params *params,
+                const char **result_r, const char **error_r)
+{
+
+       string_t *dest = t_str_new(32);
+       int ret = var_expand(dest, str, params, error_r);
+       if (ret < 0)
+               return ret;
+       *result_r = str_c(dest);
+       return 0;
+}