]> git.ipfire.org Git - thirdparty/dovecot/core.git/commitdiff
lib-settings: Add settings_hash() and settings_equal()
authorTimo Sirainen <timo.sirainen@open-xchange.com>
Fri, 1 Mar 2024 12:38:00 +0000 (14:38 +0200)
committerAki Tuomi <aki.tuomi@open-xchange.com>
Wed, 12 Feb 2025 10:34:12 +0000 (12:34 +0200)
src/lib-settings/settings-parser.c
src/lib-settings/settings-parser.h
src/lib-settings/test-settings-parser.c

index b18e81523b58da0ebddb48f1556704d53f8eee23..64d813c1e25ddcef5f63af124f2807fb8c4f549a 100644 (file)
@@ -2,6 +2,7 @@
 
 #include "lib.h"
 #include "array.h"
+#include "crc32.h"
 #include "str.h"
 #include "str-parse.h"
 #include "read-full.h"
@@ -915,6 +916,160 @@ bool settings_parser_check(struct setting_parser_context *ctx, pool_t pool,
                              ctx->set_struct, error_r);
 }
 
+unsigned int settings_hash(const struct setting_parser_info *info,
+                          const void *set, const char *const *except_fields)
+{
+       unsigned int crc = 0;
+
+       for (unsigned int i = 0; info->defines[i].key != NULL; i++) {
+               if (except_fields != NULL &&
+                   str_array_find(except_fields, info->defines[i].key))
+                       continue;
+
+               const void *p = CONST_PTR_OFFSET(set, info->defines[i].offset);
+               switch (info->defines[i].type) {
+               case SET_BOOL: {
+                       const bool *b = p;
+                       crc = crc32_data_more(crc, b, sizeof(*b));
+                       break;
+               }
+               case SET_UINT:
+               case SET_UINT_OCT:
+               case SET_TIME:
+               case SET_TIME_MSECS: {
+                       const unsigned int *i = p;
+                       crc = crc32_data_more(crc, i, sizeof(*i));
+                       break;
+               }
+               case SET_SIZE: {
+                       const uoff_t *s = p;
+                       crc = crc32_data_more(crc, s, sizeof(*s));
+                       break;
+               }
+               case SET_IN_PORT: {
+                       const in_port_t *port = p;
+                       crc = crc32_data_more(crc, port, sizeof(*port));
+                       break;
+               }
+               case SET_STR:
+               case SET_STR_NOVARS:
+               case SET_ENUM: {
+                       const char *const *str = p;
+                       crc = crc32_str_more(crc, *str);
+                       break;
+               }
+               case SET_FILE: {
+                       const char *const *str = p;
+                       const char *lf = strchr(*str, '\n');
+                       if (lf == NULL)
+                               i_panic("Settings file value is missing LF");
+                       if (lf == *str) {
+                               /* no filename - need to hash the content */
+                               crc = crc32_str_more(crc, *str + 1);
+                       } else {
+                               /* hashing the filename is enough */
+                               crc = crc32_data_more(crc, *str, lf - *str);
+                       }
+                       break;
+               }
+               case SET_STRLIST:
+               case SET_BOOLLIST:
+               case SET_FILTER_ARRAY: {
+                       const ARRAY_TYPE(const_string) *list = p;
+                       if (array_is_created(list)) {
+                               const char *str;
+                               array_foreach_elem(list, str)
+                                       crc = crc32_str_more(crc, str);
+                       }
+                       break;
+               }
+               case SET_ALIAS:
+               case SET_FILTER_NAME:
+                       break;
+               }
+       }
+       return crc;
+}
+
+bool settings_equal(const struct setting_parser_info *info,
+                   const void *set1, const void *set2,
+                   const char *const *except_fields)
+{
+       for (unsigned int i = 0; info->defines[i].key != NULL; i++) {
+               if (except_fields != NULL &&
+                   str_array_find(except_fields, info->defines[i].key))
+                       continue;
+
+               const void *p1 = CONST_PTR_OFFSET(set1, info->defines[i].offset);
+               const void *p2 = CONST_PTR_OFFSET(set2, info->defines[i].offset);
+               switch (info->defines[i].type) {
+               case SET_BOOL: {
+                       const bool *b1 = p1, *b2 = p2;
+                       if (*b1 != *b2)
+                               return FALSE;
+                       break;
+               }
+               case SET_UINT:
+               case SET_UINT_OCT:
+               case SET_TIME:
+               case SET_TIME_MSECS: {
+                       const unsigned int *i1 = p1, *i2 = p2;
+                       if (*i1 != *i2)
+                               return FALSE;
+                       break;
+               }
+               case SET_SIZE: {
+                       const uoff_t *s1 = p1, *s2 = p2;
+                       if (*s1 != *s2)
+                               return FALSE;
+                       break;
+               }
+               case SET_IN_PORT: {
+                       const in_port_t *port1 = p1, *port2 = p2;
+                       if (*port1 != *port2)
+                               return FALSE;
+                       break;
+               }
+               case SET_STR:
+               case SET_STR_NOVARS:
+               case SET_ENUM:
+               case SET_FILE: {
+                       const char *const *str1 = p1, *const *str2 = p2;
+                       if (strcmp(*str1, *str2) != 0)
+                               return FALSE;
+                       break;
+               }
+               case SET_STRLIST:
+               case SET_BOOLLIST:
+               case SET_FILTER_ARRAY: {
+                       const ARRAY_TYPE(const_string) *list1 = p1, *list2 = p2;
+                       if (array_is_empty(list1)) {
+                               if (!array_is_empty(list2))
+                                       return FALSE;
+                               break;
+                       }
+                       if (array_is_empty(list2))
+                               return FALSE;
+
+                       unsigned int i, count1, count2;
+                       const char *const *str1 = array_get(list1, &count1);
+                       const char *const *str2 = array_get(list2, &count2);
+                       if (count1 != count2)
+                               return FALSE;
+                       for (i = 0; i < count1; i++) {
+                               if (strcmp(str1[i], str2[i]) != 0)
+                                       return FALSE;
+                       }
+                       break;
+               }
+               case SET_ALIAS:
+               case SET_FILTER_NAME:
+                       break;
+               }
+       }
+       return TRUE;
+}
+
 const char *settings_section_escape(const char *name)
 {
 #define CHAR_NEED_ESCAPE(c) \
index ac7730a3ae65a02f7f3475f41756be9011cc0eca..1fe7c8dd9149d62a218ef92cac44f664210d39dc 100644 (file)
@@ -264,6 +264,16 @@ bool settings_file_has_path(const char *value);
 const char *settings_file_get_value(pool_t pool,
                                    const struct settings_file *file);
 
+/* Return hash of all the settings, except the specified fields
+   (NULL = no exceptions). */
+unsigned int settings_hash(const struct setting_parser_info *info,
+                          const void *set, const char *const *except_fields);
+/* Returns TRUE if the two settings structs are equal, except for the
+   specified fields (NULL = no exceptions). */
+bool settings_equal(const struct setting_parser_info *info,
+                   const void *set1, const void *set2,
+                   const char *const *except_fields);
+
 /* Return section name escaped */
 const char *settings_section_escape(const char *name);
 const char *settings_section_unescape(const char *name);
index e4cb89c2424ecaa80719286134c8dc0fbb4442ec..8791932e3770dc51b597f1db6eb33310a5105a4a 100644 (file)
@@ -221,12 +221,173 @@ static void test_settings_section_escape(void)
        test_end();
 }
 
+struct test_settings {
+       bool b;
+       unsigned int i;
+       uoff_t size;
+       in_port_t port;
+       const char *str;
+       const char *file;
+       ARRAY_TYPE(const_string) strlist;
+};
+
+static void test_settings_hash_equals(void)
+{
+#undef DEF
+#define DEF(type, name) \
+       SETTING_DEFINE_STRUCT_##type(#name, name, struct test_settings)
+       const struct setting_define defines[] = {
+               DEF(BOOL, b),
+               DEF(UINT, i),
+               DEF(SIZE, size),
+               DEF(IN_PORT, port),
+               DEF(STR, str),
+               DEF(FILE, file),
+               DEF(STRLIST, strlist),
+
+               SETTING_DEFINE_LIST_END
+       };
+       const struct setting_parser_info info = {
+               .defines = defines,
+       };
+       struct test_settings set1 = {
+               .str = "",
+               .file = "\n",
+       };
+       struct test_settings set2 = set1;
+
+       test_begin("settings_hash() and settings_equal()");
+
+       test_assert(settings_equal(&info, &set1, &set2, NULL));
+
+       /* boolean */
+       set1.b = TRUE;
+       test_assert(settings_hash(&info, &set1, NULL) !=
+                   settings_hash(&info, &set2, NULL));
+       test_assert(!settings_equal(&info, &set1, &set2, NULL));
+       set2.b = TRUE;
+       test_assert(settings_hash(&info, &set1, NULL) ==
+                   settings_hash(&info, &set2, NULL));
+       test_assert(settings_equal(&info, &set1, &set2, NULL));
+
+       /* uint */
+       set1.i = 1234567;
+       set2.i = 1234568;
+       test_assert(settings_hash(&info, &set1, NULL) !=
+                   settings_hash(&info, &set2, NULL));
+       test_assert(!settings_equal(&info, &set1, &set2, NULL));
+       set2.i = 1234567;
+       test_assert(settings_hash(&info, &set1, NULL) ==
+                   settings_hash(&info, &set2, NULL));
+       test_assert(settings_equal(&info, &set1, &set2, NULL));
+
+       /* size */
+       set1.size = 0x500000000ULL;
+       set2.size = 0x600000000ULL;
+       test_assert(settings_hash(&info, &set1, NULL) !=
+                   settings_hash(&info, &set2, NULL));
+       test_assert(!settings_equal(&info, &set1, &set2, NULL));
+       set2.size = 0x500000000ULL;
+       test_assert(settings_hash(&info, &set1, NULL) ==
+                   settings_hash(&info, &set2, NULL));
+       test_assert(settings_equal(&info, &set1, &set2, NULL));
+
+       /* port */
+       set1.port = 65535;
+       set2.port = 65534;
+       test_assert(settings_hash(&info, &set1, NULL) !=
+                   settings_hash(&info, &set2, NULL));
+       test_assert(!settings_equal(&info, &set1, &set2, NULL));
+       set2.port = 65535;
+       test_assert(settings_hash(&info, &set1, NULL) ==
+                   settings_hash(&info, &set2, NULL));
+       test_assert(settings_equal(&info, &set1, &set2, NULL));
+
+       /* string */
+       set1.str = "foo1";
+       set2.str = "foo2";
+       test_assert(settings_hash(&info, &set1, NULL) !=
+                   settings_hash(&info, &set2, NULL));
+       test_assert(!settings_equal(&info, &set1, &set2, NULL));
+       set2.str = "foo1";
+       test_assert(settings_hash(&info, &set1, NULL) ==
+                   settings_hash(&info, &set2, NULL));
+       test_assert(settings_equal(&info, &set1, &set2, NULL));
+
+       /* file with filename */
+       set1.file = "fname\ncontent";
+       set2.file = "fname2\ncontent";
+       test_assert(settings_hash(&info, &set1, NULL) !=
+                   settings_hash(&info, &set2, NULL));
+       test_assert(!settings_equal(&info, &set1, &set2, NULL));
+       set2.file = "fname\ncontent-with-different";
+       test_assert(settings_hash(&info, &set1, NULL) ==
+                   settings_hash(&info, &set2, NULL));
+       test_assert(!settings_equal(&info, &set1, &set2, NULL));
+       set2.file = "fname\ncontent";
+       test_assert(settings_hash(&info, &set1, NULL) ==
+                   settings_hash(&info, &set2, NULL));
+       test_assert(settings_equal(&info, &set1, &set2, NULL));
+
+       /* file without filename */
+       set1.file = "\ncontent";
+       set2.file = "\ncontent2";
+       test_assert(settings_hash(&info, &set1, NULL) !=
+                   settings_hash(&info, &set2, NULL));
+       test_assert(!settings_equal(&info, &set1, &set2, NULL));
+       set2.file = "\ncontent";
+       test_assert(settings_hash(&info, &set1, NULL) ==
+                   settings_hash(&info, &set2, NULL));
+       test_assert(settings_equal(&info, &set1, &set2, NULL));
+
+       /* string list */
+       const char *str;
+       t_array_init(&set1.strlist, 4);
+       str = "list1"; array_push_back(&set1.strlist, &str);
+       str = "list2"; array_push_back(&set1.strlist, &str);
+       str = "list3"; array_push_back(&set1.strlist, &str);
+       str = "list4"; array_push_back(&set1.strlist, &str);
+       t_array_init(&set2.strlist, 4);
+       str = "list1"; array_push_back(&set2.strlist, &str);
+       str = "list2"; array_push_back(&set2.strlist, &str);
+       test_assert(settings_hash(&info, &set1, NULL) !=
+                   settings_hash(&info, &set2, NULL));
+       test_assert(!settings_equal(&info, &set1, &set2, NULL));
+       str = "list3"; array_push_back(&set2.strlist, &str);
+       str = "list4"; array_push_back(&set2.strlist, &str);
+       test_assert(settings_hash(&info, &set1, NULL) ==
+                   settings_hash(&info, &set2, NULL));
+       test_assert(settings_equal(&info, &set1, &set2, NULL));
+
+       /* test exceptions */
+       const char *const except_fields1[] = { "b", NULL };
+       set1.b = FALSE;
+       test_assert(settings_hash(&info, &set1, NULL) !=
+                   settings_hash(&info, &set2, NULL));
+       test_assert(!settings_equal(&info, &set1, &set2, NULL));
+       test_assert(settings_hash(&info, &set1, except_fields1) ==
+                   settings_hash(&info, &set2, except_fields1));
+       test_assert(settings_equal(&info, &set1, &set2, except_fields1));
+
+       const char *const except_fields2[] = { "b", "i", NULL };
+       set1.i = 3535;
+       test_assert(settings_hash(&info, &set1, except_fields1) !=
+                   settings_hash(&info, &set2, except_fields1));
+       test_assert(!settings_equal(&info, &set1, &set2, except_fields1));
+       test_assert(settings_hash(&info, &set1, except_fields2) ==
+                   settings_hash(&info, &set2, except_fields2));
+       test_assert(settings_equal(&info, &set1, &set2, except_fields2));
+
+       test_end();
+}
+
 int main(void)
 {
        static void (*const test_functions[])(void) = {
                test_settings_parser,
                test_settings_parse_boollist_string,
                test_settings_section_escape,
+               test_settings_hash_equals,
                NULL
        };
        return test_run(test_functions);