]> git.ipfire.org Git - thirdparty/dovecot/core.git/commitdiff
lib-settings: settings - Add support for sorting filter array fields
authorStephan Bosch <stephan.bosch@open-xchange.com>
Tue, 3 Sep 2024 00:10:48 +0000 (02:10 +0200)
committerAki Tuomi <aki.tuomi@open-xchange.com>
Wed, 12 Feb 2025 10:34:14 +0000 (12:34 +0200)
src/lib-settings/settings-parser.h
src/lib-settings/settings.c
src/lib-settings/settings.h
src/lib-settings/test-settings.c

index 3c131701ce2c6c25c768a31b1f35df779a7eba41..176ff242797f85c951da9e800173923d42c253dd 100644 (file)
@@ -55,7 +55,13 @@ enum setting_apply_flags {
        SETTING_APPLY_FLAG_NO_EXPAND = BIT(1),
 };
 
-#define SETTING_DEFINE_LIST_END { 0, 0, NULL, 0, NULL, NULL }
+#define SETTING_DEFINE_LIST_END { 0, 0, NULL, 0, NULL, NULL, NULL }
+
+struct setting_filter_array_order {
+       const struct setting_parser_info *info;
+       const char *field_name;
+       bool reverse;
+};
 
 struct setting_define {
        enum setting_type type;
@@ -64,6 +70,7 @@ struct setting_define {
 
        size_t offset;
        const char *filter_array_field_name;
+       const struct setting_filter_array_order *filter_array_order;
        const char *required_setting;
 };
 
index fee3935d31e65c1a5c9690fdf3c9f3b07a629107..79e65023bc441a54033a9c9acbf1e84333f6c0af 100644 (file)
@@ -1891,7 +1891,7 @@ settings_instance_get(struct settings_apply_ctx *ctx,
 }
 
 static int
-settings_get_full(struct event *event,
+settings_get_real(struct event *event,
                  const char *filter_key, const char *filter_value,
                  const struct setting_parser_info *info,
                  const struct settings_get_params *params,
@@ -1977,11 +1977,8 @@ settings_get_full(struct event *event,
                .filter_name_required = filter_name_required,
        };
 
-       int ret;
-       T_BEGIN {
-               ret = settings_instance_get(&ctx, source_filename, source_linenum,
-                                           set_r, error_r);
-       } T_END_PASS_STR_IF(ret < 0, error_r);
+       int ret = settings_instance_get(&ctx, source_filename, source_linenum,
+                                       set_r, error_r);
        if (ret < 0) {
                *error_r = t_strdup_printf("%s settings%s: %s", info->name,
                        filter_name == NULL ? "" :
@@ -1996,6 +1993,225 @@ settings_get_full(struct event *event,
        return ret;
 }
 
+struct settings_filter_array_item {
+       union {
+               unsigned int uint;
+               const char *str;
+       } order;
+       const char *value;
+};
+
+static int
+settings_filter_array_uint_cmp(const struct settings_filter_array_item *item1,
+                              const struct settings_filter_array_item *item2)
+{
+       if (item1->order.uint < item2->order.uint)
+               return -1;
+       if (item1->order.uint > item2->order.uint)
+               return 1;
+       return 0;
+}
+
+static int
+settings_filter_array_str_cmp(const struct settings_filter_array_item *item1,
+                             const struct settings_filter_array_item *item2)
+{
+       return strcmp(item1->order.str, item2->order.str);
+}
+
+static int
+settings_sort_filter_array(struct event *event, const char *field_name,
+                          const struct setting_filter_array_order *order,
+                          ARRAY_TYPE(const_string) *fvals_arr,
+                          const struct settings_get_params *params,
+                          const char *source_filename,
+                          unsigned int source_linenum, const char **error_r)
+{
+       const char **fvals;
+       unsigned int fidx, count, i;
+
+       if (!array_is_created(fvals_arr))
+               return 0;
+
+       /* Find definition index of order field */
+       if (!setting_parser_info_find_key(order->info, order->field_name,
+                                         &fidx))
+               i_panic("BUG: Order field %s for filter array %s in %s "
+                       "not found",
+                       order->field_name, field_name, order->info->name);
+
+       /* Resolve alias */
+       while (fidx > 0 && order->info->defines[fidx].type == SET_ALIAS)
+               fidx--;
+
+       /* Determine type of field */
+       enum {
+               _ITEM_TYPE_UINT,
+               _ITEM_TYPE_STR,
+       } item_type;
+
+       const struct setting_define *order_def = &order->info->defines[fidx];
+       switch (order_def->type) {
+       case SET_UINT:
+       case SET_UINT_OCT:
+       case SET_TIME:
+       case SET_TIME_MSECS:
+               item_type = _ITEM_TYPE_UINT;
+               break;
+       case SET_STR:
+       case SET_STR_NOVARS:
+               item_type = _ITEM_TYPE_STR;
+               break;
+       case SET_ALIAS:
+               i_panic("BUG: Order field %s for filter array %s in %s "
+                       "is an invalid alias",
+                       order->field_name, field_name, order->info->name);
+       default:
+               i_panic("BUG: Order field %s for filter array %s in %s "
+                       "has a type unsuitable for defining an order",
+                       order->field_name, field_name, order->info->name);
+       }
+
+       /* Read all order fields from filter array items */
+       ARRAY(struct settings_filter_array_item) items_arr;
+
+       fvals = array_get_modifiable(fvals_arr, &count);
+       t_array_init(&items_arr, count);
+       for (i = 0; i < count; i++) {
+               const void *set;
+               int ret;
+
+               T_BEGIN {
+                       ret = settings_get_real(event, field_name, fvals[i],
+                                               order->info, params,
+                                               source_filename, source_linenum,
+                                               &set, error_r);
+               } T_END_PASS_STR_IF(ret < 0, error_r);
+               if (ret < 0)
+                       return -1;
+               if (ret == 0) {
+                       *error_r = t_strdup_printf(
+                               "Filter %s=%s unexpectedly not found while sorting "
+                               "(invalid userdb or -o override settings?)",
+                               field_name, fvals[i]);
+                       return -1;
+               }
+
+               pool_t *pool_p = PTR_OFFSET(set, order->info->pool_offset1 - 1);
+               pool_t pool = *pool_p;
+               struct settings_filter_array_item *item;
+
+               item = array_append_space(&items_arr);
+               item->value = fvals[i];
+
+               switch (item_type) {
+               case _ITEM_TYPE_UINT:
+                       item->order.uint = *((unsigned int *)
+                               PTR_OFFSET(set, order_def->offset));
+                       break;
+               case _ITEM_TYPE_STR:
+                       item->order.str = *((const char **)
+                               PTR_OFFSET(set, order_def->offset));
+                       item->order.str = t_strdup(item->order.str);
+                       break;
+               default:
+                       i_unreached();
+               }
+
+               pool_unref(&pool);
+       }
+
+       /* Sort the filter array items based on order field */
+       switch (item_type) {
+       case _ITEM_TYPE_UINT:
+               array_sort(&items_arr, settings_filter_array_uint_cmp);
+               break;
+       case _ITEM_TYPE_STR:
+               array_sort(&items_arr, settings_filter_array_str_cmp);
+               break;
+       default:
+               i_unreached();
+       }
+
+       /* Move the items in the settings */
+       const struct settings_filter_array_item *items;
+       unsigned int items_count;
+
+       items = array_get(&items_arr, &items_count);
+       i_assert(items_count == count);
+       for (i = 0; i < count; i++) {
+               if (!order->reverse)
+                       fvals[i] = items[i].value;
+               else
+                       fvals[i] = items[count - 1 - i].value;
+       }
+       return 0;
+}
+
+static int
+settings_get_full(struct event *event,
+                 const char *filter_key, const char *filter_value,
+                 const struct setting_parser_info *info,
+                 const struct settings_get_params *params,
+                 const char *source_filename,
+                 unsigned int source_linenum,
+                 const void **set_r, const char **error_r)
+{
+       const void *set;
+       int ret;
+
+       *set_r = NULL;
+       T_BEGIN {
+               ret = settings_get_real(event, filter_key, filter_value, info,
+                                       params, source_filename, source_linenum,
+                                       &set, error_r);
+       } T_END_PASS_STR_IF(ret < 0, error_r);
+       if (ret <= 0)
+               return ret;
+       if ((params->flags & SETTINGS_GET_FLAG_SORT_FILTER_ARRAYS) == 0) {
+               /* Sorting filter arrays disabled */
+               *set_r = set;
+               return 1;
+       }
+
+       const struct setting_define *def = info->defines;
+
+       /* Sort filter arrays when order is defined */
+       for (def = info->defines; def->key != NULL; def++) {
+               int ret;
+
+               if (def->type != SET_FILTER_ARRAY) {
+                       /* Not a filter array */
+                       continue;
+               }
+               if (def->filter_array_order == NULL) {
+                       /* No order defined */
+                       continue;
+               }
+
+               ARRAY_TYPE(const_string) *fvals_arr =
+                       PTR_OFFSET(set, def->offset);
+               i_assert(fvals_arr != NULL);
+
+               /* Sort this array */
+               T_BEGIN {
+                       ret = settings_sort_filter_array(
+                               event, def->key, def->filter_array_order,
+                               fvals_arr, params,
+                               source_filename, source_linenum, error_r);
+               } T_END_PASS_STR_IF(ret < 0, error_r);
+               if (ret < 0) {
+                       pool_t *pool_p = PTR_OFFSET(set, info->pool_offset1 - 1);
+                       pool_t pool = *pool_p;
+                       pool_unref(&pool);
+                       return -1;
+               }
+       }
+
+       *set_r = set;
+       return 1;
+}
+
 #undef settings_get
 int settings_get(struct event *event,
                 const struct setting_parser_info *info,
index d514c3ac35f2b72e3135490b8e7e26b063f8baf4..bac21f7a6e0848ab57405f3bdc273308a5839c20 100644 (file)
@@ -48,6 +48,8 @@ enum settings_get_flags {
        /* For unit tests: Don't validate that settings struct keys match
           th binary config file. */
        SETTINGS_GET_NO_KEY_VALIDATION = BIT(3),
+       /* Sort filter arrays with defined ordering */
+       SETTINGS_GET_FLAG_SORT_FILTER_ARRAYS = BIT(4),
 };
 
 struct settings_get_params {
index 7d51bfa6af26f745f52a68cd5ea490c61b0d0d29..321a4f5a4e59fc8462f9816b463bf160f88b0853 100644 (file)
@@ -3,11 +3,16 @@
 #include "lib.h"
 #include "array.h"
 #include "net.h"
+#include "settings.h"
 #include "settings-legacy.h"
 #include "istream.h"
 #include "ostream.h"
 #include "test-common.h"
 
+/*
+ * settings_read_nosection()
+ */
+
 #define TEST_SETTING_FILE ".test_settings.conf"
 
 static const char *config_contents =
@@ -155,10 +160,263 @@ static void test_settings_read_nosection(void)
        test_end();
 }
 
+/*
+ * settings_get()
+ */
+
+struct test2_fruit_settings {
+       pool_t pool;
+
+       const char *name;
+       bool eat;
+       unsigned int preference;
+};
+
+struct test2_settings {
+       pool_t pool;
+
+       const char *title;
+       ARRAY_TYPE(const_string) fruits;
+};
+
+#undef DEF
+#define DEF(type, name) \
+       SETTING_DEFINE_STRUCT_##type("test2_fruit_"#name, name, \
+                                    struct test2_fruit_settings)
+
+static const struct setting_define test2_fruit_setting_defines[] = {
+       DEF(STR, name),
+       DEF(BOOL, eat),
+       DEF(UINT, preference),
+       SETTING_DEFINE_LIST_END
+};
+
+static struct test2_fruit_settings test2_fruit_default_settings = {
+       .name = "",
+       .eat = FALSE,
+       .preference = UINT_MAX,
+};
+
+static const struct setting_parser_info test2_fruit_setting_parser_info = {
+       .name = "test2_fruit",
+
+       .defines = test2_fruit_setting_defines,
+       .defaults = &test2_fruit_default_settings,
+
+       .struct_size = sizeof(struct test2_fruit_settings),
+       .pool_offset1 = 1 + offsetof(struct test2_fruit_settings, pool),
+};
+
+#undef DEF
+#define DEF(type, name) \
+       SETTING_DEFINE_STRUCT_##type("test2_"#name, name, struct test2_settings)
+
+static struct setting_define test2_setting_defines[] = {
+       DEF(STR, title),
+
+       { .type = SET_FILTER_ARRAY, .key = "test2_fruit",
+          .offset = offsetof(struct test2_settings, fruits),
+          .filter_array_field_name = "test2_fruit_name" },
+       SETTING_DEFINE_LIST_END
+};
+
+static struct test2_settings test2_default_settings = {
+       .title = "",
+       .fruits = ARRAY_INIT,
+};
+
+static const struct setting_parser_info test2_setting_parser_info = {
+       .name = "test2",
+
+       .defines = test2_setting_defines,
+       .defaults = &test2_default_settings,
+
+       .struct_size = sizeof(struct test2_settings),
+       .pool_offset1 = 1 + offsetof(struct test2_settings, pool),
+};
+
+static void test_settings_get_scenario(
+       const char *scenario_name,
+       const struct setting_filter_array_order *order, const char *result[])
+{
+       static const char *const settings[] = {
+               "test2_title=Fruit Preferences",
+               "test2_fruit+=Orange",
+               "test2_fruit/Orange/name=Orange", // FIXME: why is this not applied implicitly?
+               "test2_fruit/Orange/eat=no",
+               "test2_fruit/Orange/preference=1",
+               "test2_fruit+=Apple",
+               "test2_fruit/Apple/name=Apple",
+               "test2_fruit/Apple/eat=yes",
+               "test2_fruit/Apple/preference=2",
+               "test2_fruit+=Grapefruit",
+               "test2_fruit/Grapefruit/name=Grapefruit",
+               "test2_fruit/Grapefruit/eat=no",
+               "test2_fruit/Grapefruit/preference=0",
+               "test2_fruit+=Mulberry",
+               "test2_fruit/Mulberry/name=Mulberry",
+               "test2_fruit/Mulberry/eat=yes",
+               "test2_fruit/Mulberry/preference=6",
+               "test2_fruit+=Lemon",
+               "test2_fruit/Lemon/name=Lemon",
+               "test2_fruit/Lemon/eat=yes",
+               "test2_fruit/Lemon/preference=4",
+               "test2_fruit+=Fig",
+               "test2_fruit/Fig/name=Fig",
+               "test2_fruit/Fig/eat=no",
+               "test2_fruit/Fig/preference=5",
+               "test2_fruit+=PineApple",
+               "test2_fruit/PineApple/name=PineApple",
+               "test2_fruit/PineApple/eat=yes",
+               "test2_fruit/PineApple/preference=3",
+               NULL
+       };
+       const char *error = NULL;
+       pool_t pool;
+       static struct test2_settings *set;
+       static struct settings_root *set_root;
+       int ret;
+
+       /* Modified like this only for testing; normally this is all const */
+       test2_setting_defines[1].filter_array_order = order;
+
+       test_begin(t_strdup_printf("settings_get - %s", scenario_name));
+
+       pool = pool_alloconly_create(MEMPOOL_GROWING"test2 pool", 2048);
+
+       set_root = settings_root_init();
+       for (unsigned int i = 0; settings[i] != NULL; i++) {
+               const char *key, *value;
+               t_split_key_value_eq(settings[i], &key, &value);
+               settings_root_override(set_root, key, value,
+                                      SETTINGS_OVERRIDE_TYPE_CODE);
+       }
+       struct event *event = event_create(NULL);
+       event_set_ptr(event, SETTINGS_EVENT_ROOT, set_root);
+
+       ret = settings_get(event, &test2_setting_parser_info,
+                          (order != NULL ?
+                           SETTINGS_GET_FLAG_SORT_FILTER_ARRAYS : 0),
+                          &set, &error);
+       test_assert(ret == 0);
+       test_assert(error == NULL);
+       if (error != NULL)
+               i_error("%s", error);
+       if (ret == 0) {
+               test_assert(array_is_created(&set->fruits));
+               test_assert_strcmp(set->title, "Fruit Preferences");
+       }
+       if (ret == 0 && array_is_created(&set->fruits)) {
+               unsigned int count, i;
+               const char *const *fruits = array_get(&set->fruits, &count);
+               for (i = 0; i < count; i++) {
+                       struct test2_fruit_settings *fruit_set;
+                       ret = settings_get_filter(
+                               event, "test2_fruit", fruits[i],
+                               &test2_fruit_setting_parser_info, 0,
+                               &fruit_set, &error);
+                       test_assert(ret == 0);
+                       test_assert(error == NULL);
+
+                       test_assert_strcmp(fruit_set->name, result[i]);
+
+                       settings_free(fruit_set);
+               }
+       }
+
+       settings_free(set);
+       settings_root_deinit(&set_root);
+       pool_unref(&pool);
+       event_unref(&event);
+
+       test_end();
+}
+
+static void test_settings_get(void)
+{
+       static const struct setting_filter_array_order order_by_preference = {
+               .info = &test2_fruit_setting_parser_info,
+               .field_name = "test2_fruit_preference",
+       };
+       static struct setting_filter_array_order order_by_name = {
+               .info = &test2_fruit_setting_parser_info,
+               .field_name = "test2_fruit_name",
+       };
+       static const struct setting_filter_array_order
+               order_by_preference_reverse = {
+               .info = &test2_fruit_setting_parser_info,
+               .field_name = "test2_fruit_preference",
+               .reverse = TRUE,
+       };
+       static struct setting_filter_array_order order_by_name_reverse = {
+               .info = &test2_fruit_setting_parser_info,
+               .field_name = "test2_fruit_name",
+               .reverse = TRUE,
+       };
+       static const char *result_unsorted[] = {
+               "Orange",
+               "Apple",
+               "Grapefruit",
+               "Mulberry",
+               "Lemon",
+               "Fig",
+               "PineApple",
+       };
+       static const char *result_sorted_preference[] = {
+               "Grapefruit",
+               "Orange",
+               "Apple",
+               "PineApple",
+               "Lemon",
+               "Fig",
+               "Mulberry",
+       };
+       static const char *result_sorted_name[] = {
+               "Apple",
+               "Fig",
+               "Grapefruit",
+               "Lemon",
+               "Mulberry",
+               "Orange",
+               "PineApple",
+       };
+       static const char *result_sorted_preference_reverse[] = {
+               "Mulberry",
+               "Fig",
+               "Lemon",
+               "PineApple",
+               "Apple",
+               "Orange",
+               "Grapefruit",
+       };
+       static const char *result_sorted_name_reverse[] = {
+               "PineApple",
+               "Orange",
+               "Mulberry",
+               "Lemon",
+               "Grapefruit",
+               "Fig",
+               "Apple",
+       };
+
+       test_settings_get_scenario("not sorted", NULL, result_unsorted);
+       test_settings_get_scenario("sort by preference", &order_by_preference,
+                                  result_sorted_preference);
+       test_settings_get_scenario("sort by name", &order_by_name,
+                                  result_sorted_name);
+       test_settings_get_scenario("sort by preference (reverse)",
+                                  &order_by_preference_reverse,
+                                  result_sorted_preference_reverse);
+       test_settings_get_scenario("sort by name (reverse)",
+                                  &order_by_name_reverse,
+                                  result_sorted_name_reverse);
+}
+
 int main(void)
 {
        static void (*const test_functions[])(void) = {
                test_settings_read_nosection,
+               test_settings_get,
                NULL
        };
        return test_run(test_functions);