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
$(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 \
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
LIBDOVECOT_SUBDIRS = \
lib-test \
lib \
+ lib-var-expand \
lib-settings \
lib-otp \
lib-auth \
/* 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 }
};
--- /dev/null
+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
--- /dev/null
+/* 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(®, rhs, REG_EXTENDED)) != 0) {
+ size_t size;
+ char *errbuf;
+ size = regerror(ec, ®, NULL, 0);
+ errbuf = t_malloc_no0(size);
+ (void)regerror(ec, ®, errbuf, size);
+ *error_r = t_strdup_printf("regexp() failed: %s",
+ errbuf);
+ return -1;
+ }
+ if ((ec = regexec(®, lhs, 0, 0, 0)) != 0) {
+ i_assert(ec == REG_NOMATCH);
+ res = FALSE;
+ } else {
+ res = TRUE;
+ }
+ regfree(®);
+ /* 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;
+}
--- /dev/null
+/* 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(®);
+ i_zero(&matches);
+ if ((ret = regcomp(®, pat, REG_EXTENDED)) != 0) {
+ char errbuf[1024] = {0};
+ (void)regerror(ret, ®, errbuf, sizeof(errbuf));
+ regfree(®);
+ *error_r = t_strdup(errbuf);
+ return -1;
+ }
+
+ ret = regexec(®, input, N_ELEMENTS(matches), matches, 0);
+ if (ret == REG_NOMATCH) {
+ /* no match, do not modify */
+ regfree(®);
+ 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(®);
+
+ 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;
+}
--- /dev/null
+/* 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;
+}
--- /dev/null
+/* 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);
+}
--- /dev/null
+/* 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;
+}
--- /dev/null
+#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
--- /dev/null
+/* 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(¶ms, 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(¶ms, 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(¶ms, 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(¶ms, 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, ¶ms, &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}", ¶ms, &error) == 0);
+ test_assert_strcmp(utsname_result.sysname, str_c(dest));
+
+ str_truncate(dest, 0);
+ test_assert(var_expand_new(dest, "%{system:os-version}", ¶ms, &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(¶ms, 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",
+ ¶ms, &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(¶ms, 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(¶ms, 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(¶ms, 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(¶ms, 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(¶ms, 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);
+}
--- /dev/null
+/* 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;
+}
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+/* 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)); }
+ ;
+
+%%
--- /dev/null
+#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
--- /dev/null
+/* 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;
+}