]> git.ipfire.org Git - thirdparty/dovecot/core.git/commitdiff
lib-var-expand: Add support for switch(lhs, op, cond, value, ...,default)
authorAki Tuomi <aki.tuomi@open-xchange.com>
Tue, 30 Dec 2025 09:44:12 +0000 (11:44 +0200)
committeraki.tuomi <aki.tuomi@open-xchange.com>
Fri, 23 Jan 2026 18:08:47 +0000 (18:08 +0000)
Operator that allows multiple if statements to be evaluated,
this works like if statement, but allows multiple condition-value pairs
with optional default. Each cond if used as right hand side for
comparison with lhs and op.

src/lib-var-expand/expansion-filter-if.c
src/lib-var-expand/expansion-filter.c
src/lib-var-expand/expansion.h
src/lib-var-expand/test-var-expand.c

index 5866efe805d939f80f06a010fb0e3b7d256c5a70..afc02c840a71c5608ccc1e0410c51d66b7d51048 100644 (file)
@@ -1,6 +1,7 @@
 /* Copyright (c) 2024 Dovecot authors, see the included COPYING file */
 
 #include "lib.h"
+#include "array.h"
 #include "var-expand-private.h"
 #include "expansion.h"
 #include "dregex.h"
@@ -250,3 +251,147 @@ int expansion_filter_if(const struct var_expand_statement *stmt,
 
        return 0;
 }
+
+struct switch_cond {
+       const struct var_expand_parameter *cond;
+       const struct var_expand_parameter *value;
+};
+
+/* Accepts switch('lhs', 'op', 'cond', 'value, 'cond, 'value' ..., 'default')
+ *
+ * default is optional. lhs can be input. */
+int expansion_filter_switch(const struct var_expand_statement *stmt,
+                           struct var_expand_state *state,
+                           const char **error_r)
+{
+       ARRAY(struct switch_cond) conds;
+       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_cond = NULL;
+
+       enum {
+               STATE_LHS = 0,
+               STATE_OP,
+               STATE_COND,
+               STATE_VALUE,
+               STATE_DONE,
+       } parse_state;
+
+       if (use_first_as_lhs)
+               parse_state = STATE_LHS;
+       else
+               parse_state = STATE_OP;
+
+       t_array_init(&conds, 8);
+
+       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);
+               struct switch_cond *cond;
+               if (key != NULL)
+                       ERROR_UNSUPPORTED_KEY(key);
+               switch (parse_state) {
+               case STATE_LHS:
+                       p_lhs = par;
+                       parse_state = STATE_OP;
+                       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;
+                       }
+                       parse_state = STATE_COND;
+                       break;
+               case STATE_COND:
+                       p_cond = par;
+                       parse_state = STATE_VALUE;
+                       break;
+               case STATE_VALUE:
+                       cond = array_append_space(&conds);
+                       cond->cond = p_cond;
+                       cond->value = par;
+                       p_cond = NULL;
+                       parse_state = STATE_COND;
+                       break;
+               case STATE_DONE:
+                       ERROR_TOO_MANY_UNNAMED_PARAMETERS;
+               }
+       }
+
+       if (parse_state > STATE_OP && array_is_empty(&conds)) {
+               *error_r = "At least one condition-value pair is required";
+               return -1;
+       } else if (parse_state == STATE_VALUE && p_cond != NULL) {
+               struct switch_cond *cond = array_append_space(&conds);
+               cond->value = p_cond;
+       } else if (parse_state == STATE_COND) {
+               /* no default, use empty */
+               array_append_zero(&conds);
+       } else 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 switch";
+                       return -1;
+               }
+       }
+
+       i_assert(p_lhs != NULL);
+
+       /* then start matching */
+       struct switch_cond *cond;
+       const struct var_expand_parameter *res = NULL;
+       unsigned int cond_num = 0;
+       /* There has to be at least one cond */
+       array_foreach_modifiable(&conds, cond) {
+               bool match;
+               if (cond->cond == NULL) {
+                       res = cond->value;
+                       break;
+               }
+               int ret = fn_if_cmp(state, p_lhs, op, cond->cond, &match, error_r);
+               if (ret < 0)
+                       return -1;
+               if (match) {
+                       res = cond->value;
+                       break;
+               }
+               cond_num++;
+       }
+
+       const char *value;
+
+       if (res == NULL) {
+               /* Since there was no default, leave it unset */
+               *error_r = "No default value provided";
+               var_expand_state_unset_transfer(state);
+               return -1;
+       } else if (var_expand_parameter_any_or_var(state, res, &value, error_r) < 0) {
+               *error_r = t_strdup_printf("in condition #%u: %s",
+                                          cond_num, *error_r);
+               return -1;
+       } else {
+               var_expand_state_set_transfer(state, value);
+       }
+
+       return 0;
+}
index a72bd1e132b31e8cc3d4c953f992c6ca526ba9a4..53cf149034dbebccea67c5866ffd53d9c84f6342 100644 (file)
@@ -1147,6 +1147,7 @@ static const struct var_expand_filter var_expand_builtin_filters[] = {
        { .name = "text", .filter = fn_text },
        { .name = "encrypt", .filter = expansion_filter_encrypt },
        { .name = "decrypt", .filter = expansion_filter_decrypt },
+       { .name = "switch", .filter = expansion_filter_switch },
        { .name = NULL }
 };
 
index 403d483398a0cefd456b7c0a47c4420c411d5314..b5791744371cbb13587f4f8ae611881ef824f70a 100644 (file)
@@ -33,6 +33,9 @@ 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);
+int expansion_filter_switch(const struct var_expand_statement *stmt,
+                           struct var_expand_state *state,
+                           const char **error_r);
 int
 expansion_filter_encrypt(const struct var_expand_statement *stmt,
                         struct var_expand_state *state, const char **error_r);
index 59633bcfbbbfcc4384b19ac77a03805784ff7383..77e575220187282f73186794d1df7191e853e8f5 100644 (file)
@@ -404,6 +404,108 @@ static void test_var_expand_if(void)
        test_end();
 }
 
+/* This basically uses the same things if does, so we can do slightly relaxed
+   testing */
+static void test_var_expand_switch(void)
+{
+       test_begin("var_expand(switch)");
+
+       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[] = {
+               {
+                       .in = "%{switch(alpha, \"eq\", alpha, \"yes\", beta, \"no\", one, \"other\", two)}",
+                       .out = "yes",
+                       .ret = 0,
+               },
+               {
+                       .in = "%{beta | switch(\"eq\", alpha, \"yes\", beta, \"no\", one, \"other\", two)}",
+                       .out = "no",
+                       .ret = 0,
+               },
+               {
+                       .in = "%{one | switch(\"eq\", alpha, \"yes\", beta, \"no\", one, \"other\", two)}",
+                       .out = "other",
+                       .ret = 0,
+               },
+               {
+                       .in = "%{evil2 | switch(\"eq\", alpha, \"yes\", beta, \"no\", one, \"other\", two)}",
+                       .out = "2",
+                       .ret = 0,
+               },
+               {
+                       .in = "%{one | switch(\"==\", 1, 2, 2, 3, 3, 4, -1)}",
+                       .out = "2",
+                       .ret = 0,
+               },
+               /* No match, no default */
+               {
+                       .in = "%{literal('0') | switch(\"==\", 1, 2, 2, 3, 3, 4)}",
+                       .out = "switch: No default value provided",
+                       .ret = -1,
+               },
+               /* No match, missing variable */
+               {
+                       .in = "%{literal('0') | switch(\"==\", 1, 2, 2, 3, 3, 4, four)}",
+                       .out = "switch: in condition #3: Unknown variable 'four'",
+                       .ret = -1,
+               },
+               /* No match, no default, but fix with default filter */
+               {
+                       .in = "%{literal('0') | switch(\"==\", 1, 2, 2, 3, 3, 4) | default}",
+                       .out = "",
+                       .ret = 0,
+               },
+               /* Errors */
+               {
+                       .in = "%{one | switch(\"==\", 1)}",
+                       .out = "switch: At least one condition-value pair is required",
+                       .ret = -1,
+               },
+               {
+                       .in = "%{switch}",
+                       .out = "switch: Missing parameters",
+                       .ret = -1,
+               },
+               {
+                       .in = "%{switch(\"==\")}",
+                       .out = "switch: Missing parameters",
+                       .ret = -1,
+               },
+               {
+                       .in = "%{switch(alpha, \"==\")}",
+                       .out = "switch: At least one condition-value pair is required",
+                       .ret = -1,
+               },
+               {
+                       .in = "%{alpha | switch(\"==\")}",
+                       .out = "switch: At least one condition-value pair is required",
+                       .ret = -1,
+               },
+               {
+                       .in = "%{switch(alpha, \"eq\", \"default\")}",
+                       .out = "switch: At least one condition-value pair is required",
+                       .ret = -1,
+               },
+       };
+
+       const struct var_expand_params params = {
+               .table = table,
+       };
+
+       run_var_expand_tests(&params, tests, N_ELEMENTS(tests));
+
+       test_end();
+}
+
 static int test_custom_provider(const char *key, const char **value_r, void *context,
                                const char **error_r)
 {
@@ -1226,6 +1328,7 @@ int main(int argc, char *const argv[])
                test_var_expand_builtin_filters,
                test_var_expand_math,
                test_var_expand_if,
+               test_var_expand_switch,
                test_var_expand_providers,
                test_var_expand_provider_arr,
                test_var_expand_tables_arr,